Initial Watermaps import
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type {
|
||||
RouteResult,
|
||||
VoyageHarbour
|
||||
} from "@watermaps/shared";
|
||||
import {
|
||||
nextRouteEventsByKind,
|
||||
upcomingRouteEvents
|
||||
} from "../src/routeEvents";
|
||||
import type { RouteBridgeAssessment } from "../src/routeWeatherReport";
|
||||
import type { RouteLock } from "../src/voyageHarbours";
|
||||
|
||||
const route: RouteResult = {
|
||||
id: "eastbound",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[1, 0]
|
||||
]
|
||||
},
|
||||
distanceNm: 60,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: ["test"]
|
||||
};
|
||||
|
||||
describe("upcomingRouteEvents", () => {
|
||||
it("projects mixed facilities, filters passed and off-corridor entries, and orders them along the route", () => {
|
||||
const events = upcomingRouteEvents({
|
||||
route,
|
||||
progressNm: 12,
|
||||
harbours: [
|
||||
harbour("passed-harbour", 0.1, 0),
|
||||
harbour("next-harbour", 0.6, 0),
|
||||
harbour("off-route-harbour", 0.5, 0.05)
|
||||
],
|
||||
locks: [
|
||||
lock("next-lock", 0.4, 0, 999, 0),
|
||||
lock("off-route-lock", 0.5, 0.01, 1, 0)
|
||||
],
|
||||
bridges: [
|
||||
bridge("next-bridge", 0.3, 0.0005, 99),
|
||||
bridge("off-route-bridge", 0.35, 0.01, 0.001)
|
||||
]
|
||||
});
|
||||
|
||||
expect(events.map((event) => `${event.kind}:${event.id}`)).toEqual([
|
||||
"bridge:next-bridge",
|
||||
"lock:next-lock",
|
||||
"harbour:next-harbour"
|
||||
]);
|
||||
expect(events.every((event) => event.routeDistanceNm >= 12)).toBe(true);
|
||||
expect(events.every((event) => event.remainingNm >= 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("reprojects locks and bridges instead of trusting their existing distance fields", () => {
|
||||
const [projectedLock, projectedBridge] = upcomingRouteEvents({
|
||||
route,
|
||||
locks: [lock("lock", 0.25, 0, 999, 777)],
|
||||
bridges: [bridge("bridge", 0.75, 0, 999)]
|
||||
});
|
||||
|
||||
expect(projectedLock?.kind).toBe("lock");
|
||||
expect(projectedLock?.routeDistanceNm).toBeCloseTo(15, 0);
|
||||
expect(projectedLock?.distanceFromRouteNm).toBeCloseTo(0, 6);
|
||||
expect(projectedBridge?.kind).toBe("bridge");
|
||||
expect(projectedBridge?.routeDistanceNm).toBeCloseTo(45, 0);
|
||||
expect(projectedBridge?.distanceFromRouteNm).toBeCloseTo(0, 6);
|
||||
});
|
||||
|
||||
it("uses a corridor override without changing the defaults for other kinds", () => {
|
||||
const events = upcomingRouteEvents({
|
||||
route,
|
||||
corridorsNm: { harbour: 4 },
|
||||
harbours: [harbour("detour", 0.5, 0.05)],
|
||||
locks: [lock("off-route-lock", 0.5, 0.01, 0, 0)]
|
||||
});
|
||||
|
||||
expect(events.map((event) => event.id)).toEqual(["detour"]);
|
||||
});
|
||||
|
||||
it("calculates ETA and retains both speed and reference-time provenance", () => {
|
||||
const [event] = upcomingRouteEvents({
|
||||
route,
|
||||
progressNm: 6,
|
||||
locks: [lock("lock", 0.5, 0, 0, 0)],
|
||||
etaBasis: {
|
||||
speedKn: 6,
|
||||
speedSource: "vessel-cruise-speed",
|
||||
referenceTime: "2026-07-23T08:00:00.000Z",
|
||||
referenceSource: "route-departure"
|
||||
}
|
||||
});
|
||||
|
||||
expect(event).toBeDefined();
|
||||
expect(event!.remainingNm).toBeCloseTo(24, 0);
|
||||
expect(event!.eta).toMatchObject({
|
||||
speedKn: 6,
|
||||
speedSource: "vessel-cruise-speed",
|
||||
referenceTime: "2026-07-23T08:00:00.000Z",
|
||||
referenceSource: "route-departure"
|
||||
});
|
||||
expect(event!.eta!.minutesFromProgress).toBeCloseTo(
|
||||
(event!.remainingNm / 6) * 60,
|
||||
8
|
||||
);
|
||||
expect(Date.parse(event!.eta!.estimatedAt)).toBeCloseTo(
|
||||
Date.parse("2026-07-23T08:00:00.000Z") +
|
||||
event!.eta!.minutesFromProgress * 60_000,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it("omits ETA for an invalid assumption rather than inventing a speed", () => {
|
||||
const [event] = upcomingRouteEvents({
|
||||
route,
|
||||
harbours: [harbour("harbour", 0.5, 0)],
|
||||
etaBasis: {
|
||||
speedKn: 0,
|
||||
speedSource: "gps-sog",
|
||||
referenceTime: "not-a-time",
|
||||
referenceSource: "current-time"
|
||||
}
|
||||
});
|
||||
|
||||
expect(event?.eta).toBeNull();
|
||||
});
|
||||
|
||||
it("selects the first upcoming event of each kind from an ordered list", () => {
|
||||
const events = upcomingRouteEvents({
|
||||
route,
|
||||
harbours: [
|
||||
harbour("harbour-2", 0.8, 0),
|
||||
harbour("harbour-1", 0.2, 0)
|
||||
],
|
||||
locks: [lock("lock-1", 0.3, 0, 0, 0)]
|
||||
});
|
||||
|
||||
const next = nextRouteEventsByKind(events);
|
||||
|
||||
expect(next.harbour?.id).toBe("harbour-1");
|
||||
expect(next.lock?.id).toBe("lock-1");
|
||||
expect(next.bridge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function harbour(
|
||||
id: string,
|
||||
lon: number,
|
||||
lat: number
|
||||
): VoyageHarbour {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
kind: "harbour",
|
||||
coordinate: { lon, lat }
|
||||
};
|
||||
}
|
||||
|
||||
function lock(
|
||||
id: string,
|
||||
lon: number,
|
||||
lat: number,
|
||||
routeDistanceNm: number,
|
||||
distanceFromRouteNm: number
|
||||
): RouteLock {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
coordinate: { lon, lat },
|
||||
routeDistanceNm,
|
||||
distanceFromRouteNm,
|
||||
openingHours: null,
|
||||
phone: null,
|
||||
vhf: null,
|
||||
website: null
|
||||
};
|
||||
}
|
||||
|
||||
function bridge(
|
||||
id: string,
|
||||
lon: number,
|
||||
lat: number,
|
||||
distanceNm: number
|
||||
): RouteBridgeAssessment {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
label: id,
|
||||
coordinate: { lon, lat },
|
||||
distanceNm,
|
||||
clearanceM: null,
|
||||
clearanceLabel: null,
|
||||
requiredAirDraftM: null,
|
||||
marginM: null,
|
||||
status: "unknown",
|
||||
source: "test"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user