201 lines
5.1 KiB
TypeScript
201 lines
5.1 KiB
TypeScript
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: [],
|
|
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"
|
|
};
|
|
}
|