Initial Watermaps import
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildVoyagePlan,
|
||||
harbourAmenitiesFromProperties,
|
||||
orderWaypointsAlongRoute,
|
||||
type RouteResult,
|
||||
type VoyageHarbour
|
||||
} from "../src/index.js";
|
||||
|
||||
function eastboundRoute(lengthDegrees = 1.8): Pick<RouteResult, "geometry" | "distanceNm"> {
|
||||
return {
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[lengthDegrees / 2, 0],
|
||||
[lengthDegrees, 0]
|
||||
]
|
||||
},
|
||||
distanceNm: lengthDegrees * 60
|
||||
};
|
||||
}
|
||||
|
||||
function harbour(
|
||||
id: string,
|
||||
lon: number,
|
||||
amenities: VoyageHarbour["amenities"] = {}
|
||||
): VoyageHarbour {
|
||||
return {
|
||||
id,
|
||||
name: `Hafen ${id}`,
|
||||
kind: "marina",
|
||||
coordinate: { lat: 0.005, lon },
|
||||
amenities
|
||||
};
|
||||
}
|
||||
|
||||
describe("voyage planning", () => {
|
||||
it("orders named waypoints by their position along the route", () => {
|
||||
const waypoints = orderWaypointsAlongRoute(
|
||||
[
|
||||
{ id: "late", name: "Später", coordinate: { lat: 0.01, lon: 1.2 } },
|
||||
{ id: "early", name: "Früher", coordinate: { lat: -0.01, lon: 0.3 } }
|
||||
],
|
||||
eastboundRoute()
|
||||
);
|
||||
|
||||
expect(waypoints.map(({ id }) => id)).toEqual(["early", "late"]);
|
||||
expect(waypoints.map(({ sequence }) => sequence)).toEqual([1, 2]);
|
||||
expect(waypoints[0]!.coordinate).toEqual({ lat: -0.01, lon: 0.3 });
|
||||
expect(waypoints[0]!.routeDistanceNm).toBeCloseTo(18, 0);
|
||||
expect(waypoints[0]!.distanceFromRouteNm).toBeCloseTo(0.6, 0);
|
||||
});
|
||||
|
||||
it("creates daily stages at suitable nearby harbours and assigns crossed waypoints", () => {
|
||||
const plan = buildVoyagePlan({
|
||||
route: eastboundRoute(),
|
||||
cruiseSpeedKn: 10,
|
||||
maxCruisingHoursPerDay: 5,
|
||||
requiredAmenities: ["water", "overnight"],
|
||||
waypoints: [
|
||||
{ id: "second", name: "Zweiter Wegpunkt", coordinate: { lat: 0, lon: 1.2 } },
|
||||
{ id: "first", name: "Erster Wegpunkt", coordinate: { lat: 0, lon: 0.3 } }
|
||||
],
|
||||
harbours: [
|
||||
harbour("unsuitable", 0.8, { overnight: true, water: false }),
|
||||
harbour("day-one", 0.75, { overnight: true, water: true }),
|
||||
harbour("day-two", 1.5, { overnight: "available", water: "available" })
|
||||
]
|
||||
});
|
||||
|
||||
expect(plan.legs).toHaveLength(3);
|
||||
expect(plan.legs.map((leg) => leg.end.name)).toEqual([
|
||||
"Hafen day-one",
|
||||
"Hafen day-two",
|
||||
"Ziel"
|
||||
]);
|
||||
expect(plan.legs.every((leg) => leg.durationHours <= 5.01)).toBe(true);
|
||||
expect(plan.legs[0]!.waypoints.map(({ id }) => id)).toEqual(["first"]);
|
||||
expect(plan.legs[1]!.waypoints.map(({ id }) => id)).toEqual(["second"]);
|
||||
expect(plan.warnings.some(({ code }) => code === "NO_SUITABLE_HARBOUR")).toBe(false);
|
||||
expect(plan.totalDistanceNm).toBeGreaterThan(plan.totalRouteDistanceNm);
|
||||
});
|
||||
|
||||
it("uses an explicit route target and warns when no suitable harbour exists", () => {
|
||||
const plan = buildVoyagePlan({
|
||||
route: eastboundRoute(1.2),
|
||||
cruiseSpeedKn: 6,
|
||||
maxCruisingHoursPerDay: 5,
|
||||
requiredAmenities: ["fuel"],
|
||||
maxHarbourDetourNm: 1,
|
||||
harbours: [
|
||||
harbour("no-fuel", 0.4, { fuel: false }),
|
||||
{ ...harbour("too-far", 0.45, { fuel: true }), coordinate: { lat: 0.1, lon: 0.45 } }
|
||||
]
|
||||
});
|
||||
|
||||
expect(plan.legs).toHaveLength(3);
|
||||
expect(plan.legs[0]!.end.type).toBe("route");
|
||||
expect(plan.legs[1]!.end.type).toBe("route");
|
||||
expect(plan.legs[2]!.end.type).toBe("destination");
|
||||
expect(plan.warnings.filter(({ code }) => code === "NO_SUITABLE_HARBOUR")).toHaveLength(2);
|
||||
expect(plan.warnings[0]!.message).toContain("Treibstoff");
|
||||
expect(plan.warnings[0]!.message).toContain("kein bestätigter Liegeplatz");
|
||||
});
|
||||
|
||||
it("prefers a safe short harbour stage and reports why it ends early", () => {
|
||||
const plan = buildVoyagePlan({
|
||||
route: eastboundRoute(1.2),
|
||||
cruiseSpeedKn: 10,
|
||||
maxCruisingHoursPerDay: 5,
|
||||
harbours: [harbour("early", 0.2)]
|
||||
});
|
||||
|
||||
expect(plan.legs[0]!.end.name).toBe("Hafen early");
|
||||
expect(plan.warnings.some(({ code }) => code === "SHORT_STAGE_FOR_HARBOUR")).toBe(true);
|
||||
});
|
||||
|
||||
it("warns about waypoints that are implausibly far from the routed line", () => {
|
||||
const plan = buildVoyagePlan({
|
||||
route: eastboundRoute(0.2),
|
||||
cruiseSpeedKn: 6,
|
||||
maxCruisingHoursPerDay: 5,
|
||||
maxWaypointDistanceFromRouteNm: 1,
|
||||
waypoints: [
|
||||
{ id: "off-route", name: "Falscher Abzweig", coordinate: { lat: 0.1, lon: 0.1 } }
|
||||
]
|
||||
});
|
||||
|
||||
expect(plan.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ code: "WAYPOINT_OFF_ROUTE", severity: "caution" })
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes common OSM harbour amenity tags without treating missing data as available", () => {
|
||||
expect(
|
||||
harbourAmenitiesFromProperties({
|
||||
power_supply: "yes",
|
||||
drinking_water: true,
|
||||
"fuel:diesel": "no",
|
||||
amenity: "waste_disposal",
|
||||
guest_berths: "12"
|
||||
})
|
||||
).toEqual({
|
||||
electricity: "available",
|
||||
water: "available",
|
||||
fuel: "unavailable",
|
||||
waste: "available",
|
||||
overnight: "available"
|
||||
});
|
||||
|
||||
expect(harbourAmenitiesFromProperties({}).overnight).toBe("unknown");
|
||||
expect(harbourAmenitiesFromProperties({ fuel: "no", "fuel:diesel": "yes" }).fuel).toBe(
|
||||
"available"
|
||||
);
|
||||
expect(harbourAmenitiesFromProperties({ drinking_water: null }).water).toBe("unknown");
|
||||
});
|
||||
|
||||
it("rejects unsafe daily planning inputs", () => {
|
||||
expect(() =>
|
||||
buildVoyagePlan({
|
||||
route: eastboundRoute(),
|
||||
cruiseSpeedKn: 0,
|
||||
maxCruisingHoursPerDay: 5
|
||||
})
|
||||
).toThrow(/cruiseSpeedKn/);
|
||||
expect(() =>
|
||||
buildVoyagePlan({
|
||||
route: eastboundRoute(),
|
||||
cruiseSpeedKn: 6,
|
||||
maxCruisingHoursPerDay: Number.NaN
|
||||
})
|
||||
).toThrow(/maxCruisingHoursPerDay/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user