Initial Watermaps import
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { MarineForecast, RouteResult } from "@watermaps/shared";
|
||||
import { createRouteWeatherReport, summarizeRouteWeather } from "../src/routeWeatherReport";
|
||||
|
||||
describe("route weather report", () => {
|
||||
it("samples start, middle and destination forecasts", async () => {
|
||||
const requested: Array<{ lat: number; lon: number }> = [];
|
||||
const report = await createRouteWeatherReport(routeFixture, async (coordinate) => {
|
||||
requested.push(coordinate);
|
||||
return forecast({
|
||||
windSpeed: coordinate.lon > 7.1 ? 18 : 10,
|
||||
waveHeightM: coordinate.lon > 7.1 ? 1.2 : 0.4
|
||||
});
|
||||
});
|
||||
|
||||
expect(requested).toHaveLength(3);
|
||||
expect(report.maxWindSpeedKn).toBe(18);
|
||||
expect(report.maxWaveHeightM).toBe(1.2);
|
||||
expect(report.severity).toBe("caution");
|
||||
expect(report.summary).toContain("Aufmerksam fahren");
|
||||
expect(report.bridgeReport).toBeNull();
|
||||
});
|
||||
|
||||
it("projects forecasts along the route timeline and adjusts ETA for along-route current", async () => {
|
||||
const requestedTimes: Array<string | undefined> = [];
|
||||
const departureTime = "2026-07-20T06:00:00.000Z";
|
||||
const timedRoute: RouteResult = {
|
||||
...routeFixture,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [[7, 52], [7.2, 52], [7.4, 52]]
|
||||
},
|
||||
distanceNm: 20,
|
||||
departureTime,
|
||||
durationMinutes: 240,
|
||||
eta: "2026-07-20T10:00:00.000Z"
|
||||
};
|
||||
|
||||
const report = await createRouteWeatherReport(
|
||||
timedRoute,
|
||||
{ draughtM: 1.4, safetyReserveM: 0.5, cruiseSpeedKn: 5 },
|
||||
async (_coordinate, at) => {
|
||||
requestedTimes.push(at);
|
||||
return forecast({
|
||||
oceanCurrentSpeedKn: 1,
|
||||
oceanCurrentDirectionDeg: 90,
|
||||
forecastTime: at
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
expect(requestedTimes).toEqual([
|
||||
"2026-07-20T06:00:00.000Z",
|
||||
"2026-07-20T08:00:00.000Z",
|
||||
"2026-07-20T10:00:00.000Z"
|
||||
]);
|
||||
expect(report.samples.map((sample) => sample.plannedTime)).toEqual(requestedTimes);
|
||||
expect(report.samples.map((sample) => sample.currentAlongRouteKn)).toEqual([1, 1, 1]);
|
||||
expect(report.departureTime).toBe(departureTime);
|
||||
expect(report.averageAlongRouteCurrentKn).toBe(1);
|
||||
expect(report.currentAdjustmentMinutes).toBe(-40);
|
||||
expect(report.adjustedEta).toBe("2026-07-20T09:20:00.000Z");
|
||||
});
|
||||
|
||||
it("adds a bridge report and marks a route as blocked by a low bridge", async () => {
|
||||
const report = await createRouteWeatherReport(
|
||||
routeFixture,
|
||||
{ draughtM: 1.4, safetyReserveM: 0.5, airDraftM: 3 },
|
||||
async () => forecast({}),
|
||||
async (params) => {
|
||||
expect(params.layers).toEqual(["bridges"]);
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
{
|
||||
type: "Feature",
|
||||
id: "bridge-low",
|
||||
properties: {
|
||||
layer: "bridges",
|
||||
name: "Niedrige Brücke",
|
||||
clearance_m: 2.7,
|
||||
clearance_label: "H 2.7 m",
|
||||
label: "Niedrige Brücke H 2.7 m",
|
||||
source: "OSM/Geofabrik"
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[6.99, 53.5],
|
||||
[7.21, 53.5]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "bridge-high",
|
||||
properties: {
|
||||
layer: "bridges",
|
||||
name: "Hohe Brücke",
|
||||
clearance_m: 4.2,
|
||||
clearance_label: "H 4.2 m",
|
||||
label: "Hohe Brücke H 4.2 m",
|
||||
source: "OSM/Geofabrik"
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[7.18, 53.68],
|
||||
[7.22, 53.68]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
expect(report.bridgeReport?.severity).toBe("critical");
|
||||
expect(report.bridgeReport?.tooLowCount).toBe(1);
|
||||
expect(report.bridgeReport?.checkedCount).toBe(2);
|
||||
expect(report.bridgeReport?.summary).toContain("Nicht passierbar");
|
||||
expect(report.bridgeReport?.bridges[0]?.name).toBe("Niedrige Brücke");
|
||||
});
|
||||
|
||||
it("marks critical weather when wind or wave thresholds are exceeded", () => {
|
||||
const report = summarizeRouteWeather([
|
||||
{
|
||||
label: "Mitte",
|
||||
coordinate: { lat: 53.5, lon: 7.1 },
|
||||
forecast: forecast({ windSpeed: 28, waveHeightM: 1.1 })
|
||||
}
|
||||
]);
|
||||
|
||||
expect(report.severity).toBe("critical");
|
||||
expect(report.summary).toContain("Kritische Bedingungen");
|
||||
});
|
||||
});
|
||||
|
||||
const routeFixture: RouteResult = {
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[7, 53.3],
|
||||
[7.1, 53.5],
|
||||
[7.2, 53.7]
|
||||
]
|
||||
},
|
||||
distanceNm: 25,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: [],
|
||||
routingMode: "fairway"
|
||||
};
|
||||
|
||||
function forecast(overrides: Partial<MarineForecast>): MarineForecast {
|
||||
return {
|
||||
waveHeightM: 0.4,
|
||||
waveDirectionDeg: 280,
|
||||
wavePeriodS: 4,
|
||||
windSpeed: 10,
|
||||
windDirectionDeg: 260,
|
||||
weatherCode: 2,
|
||||
temperatureC: 18,
|
||||
source: "Test",
|
||||
updatedAt: "2026-07-13T12:00:00.000Z",
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user