617 lines
19 KiB
TypeScript
617 lines
19 KiB
TypeScript
import "@testing-library/jest-dom/vitest";
|
|
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { RoutePlanner } from "../src/components/RoutePlanner";
|
|
import { DEFAULT_BOAT_PROFILE } from "../src/lib/boat-profile";
|
|
import type { RouteWeatherReport } from "../src/routeWeatherReport";
|
|
|
|
afterEach(() => cleanup());
|
|
|
|
describe("RoutePlanner", () => {
|
|
it("uses the current boat while submitting route coordinates", () => {
|
|
const onSubmit = vi.fn();
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={{ lat: 54.18, lon: 12.08 }}
|
|
gpsPosition={{ lat: 54.18, lon: 12.08 }}
|
|
destination={{ lat: 54.2, lon: 12.1 }}
|
|
result={null}
|
|
routeOptions={[]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
boatProfile={{
|
|
...DEFAULT_BOAT_PROFILE,
|
|
name: "Seestern",
|
|
category: "sailboat",
|
|
lengthM: 9.4,
|
|
draughtM: 1.8,
|
|
airDraftM: 13.2,
|
|
beamM: 3.3,
|
|
cruiseSpeedKn: 6.5
|
|
}}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={onSubmit}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Route berechnen" }));
|
|
|
|
expect(onSubmit).toHaveBeenCalledWith({
|
|
start: { lat: 54.18, lon: 12.08 },
|
|
destination: { lat: 54.2, lon: 12.1 },
|
|
waypoints: [],
|
|
departureTime: expect.any(String)
|
|
});
|
|
expect(screen.getByRole("region", { name: "Aktuelles Boot: Seestern" })).toBeVisible();
|
|
expect(screen.queryByLabelText("Tiefgang (m)")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("submits ordered waypoints together with the selected departure time", () => {
|
|
const onSubmit = vi.fn();
|
|
const waypoints = [
|
|
{ lat: 52.4, lon: 7.1 },
|
|
{ lat: 51.9, lon: 7.45 }
|
|
];
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={{ lat: 53.344167, lon: 7.186111 }}
|
|
gpsPosition={null}
|
|
destination={{ lat: 51.6814536, lon: 7.8042615 }}
|
|
waypoints={waypoints}
|
|
result={null}
|
|
routeOptions={[]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={onSubmit}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onPickWaypoint={vi.fn()}
|
|
onRemoveWaypoint={vi.fn()}
|
|
onMoveWaypoint={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
const localDeparture = "2026-07-20T08:30";
|
|
|
|
fireEvent.change(screen.getByLabelText("Abfahrt"), { target: { value: localDeparture } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Route berechnen" }));
|
|
|
|
expect(onSubmit).toHaveBeenCalledWith({
|
|
start: { lat: 53.344167, lon: 7.186111 },
|
|
destination: { lat: 51.6814536, lon: 7.8042615 },
|
|
waypoints,
|
|
departureTime: new Date(localDeparture).toISOString()
|
|
});
|
|
});
|
|
|
|
it("offers an explicit start picking action", () => {
|
|
const onPickStart = vi.fn();
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={null}
|
|
gpsPosition={{ lat: 54.18, lon: 12.08 }}
|
|
destination={null}
|
|
result={null}
|
|
routeOptions={[]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={onPickStart}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Start auf Karte setzen" }));
|
|
|
|
expect(onPickStart).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("offers an explicit destination picking action", () => {
|
|
const onPickDestination = vi.fn();
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={null}
|
|
gpsPosition={{ lat: 54.18, lon: 12.08 }}
|
|
destination={null}
|
|
result={null}
|
|
routeOptions={[]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={onPickDestination}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Ziel auf Karte setzen" }));
|
|
|
|
expect(onPickDestination).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not offer Borkum or Hamm as direct destination shortcuts", () => {
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={null}
|
|
gpsPosition={null}
|
|
destination={null}
|
|
result={null}
|
|
routeOptions={[]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByRole("button", { name: /Borkum/i })).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: /Hamm/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("lets the skipper switch between returned route alternatives", () => {
|
|
const onSelectRoute = vi.fn();
|
|
const primary = routeOption("primary", "Hauptroute", 12);
|
|
const alternative = routeOption("alternative", "Alternative 1", 14.5);
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={{ lat: 52, lon: 7 }}
|
|
gpsPosition={null}
|
|
destination={{ lat: 52, lon: 7.1 }}
|
|
result={primary}
|
|
routeOptions={[primary, alternative]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={onSelectRoute}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /Alternative 1/ }));
|
|
|
|
expect(onSelectRoute).toHaveBeenCalledWith("alternative");
|
|
});
|
|
|
|
it("can collapse the route planner", () => {
|
|
const onCollapse = vi.fn();
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={null}
|
|
gpsPosition={null}
|
|
destination={null}
|
|
result={null}
|
|
routeOptions={[]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={onCollapse}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Routenfenster ausblenden" }));
|
|
|
|
expect(onCollapse).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("controls mobile sheet heights and leaves compact mode when the desktop layout starts", () => {
|
|
let desktopLayout = false;
|
|
let changeListener: (() => void) | null = null;
|
|
const originalMatchMedia = window.matchMedia;
|
|
const mediaQuery = {
|
|
get matches() {
|
|
return desktopLayout;
|
|
},
|
|
media: "(min-width: 720px)",
|
|
onchange: null,
|
|
addEventListener: vi.fn((_type: string, listener: () => void) => {
|
|
changeListener = listener;
|
|
}),
|
|
removeEventListener: vi.fn(),
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
dispatchEvent: vi.fn()
|
|
} as unknown as MediaQueryList;
|
|
Object.defineProperty(window, "matchMedia", {
|
|
configurable: true,
|
|
value: vi.fn(() => mediaQuery)
|
|
});
|
|
|
|
const { container, unmount } = render(
|
|
<RoutePlanner
|
|
startPoint={null}
|
|
gpsPosition={null}
|
|
destination={null}
|
|
result={null}
|
|
routeOptions={[]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
const panel = container.querySelector("aside.route-panel");
|
|
const body = container.querySelector(".route-panel-body");
|
|
|
|
expect(panel).toHaveAttribute("data-sheet-state", "half");
|
|
fireEvent.click(screen.getByRole("button", { name: "Routenfenster auf volle Höhe vergrößern" }));
|
|
expect(panel).toHaveAttribute("data-sheet-state", "full");
|
|
fireEvent.click(screen.getByRole("button", { name: "Routenfenster auf kompakte Höhe verkleinern" }));
|
|
expect(panel).toHaveAttribute("data-sheet-state", "compact");
|
|
expect(body).toHaveAttribute("hidden");
|
|
|
|
desktopLayout = true;
|
|
act(() => changeListener?.());
|
|
expect(panel).toHaveAttribute("data-sheet-state", "half");
|
|
expect(body).not.toHaveAttribute("hidden");
|
|
|
|
unmount();
|
|
Object.defineProperty(window, "matchMedia", {
|
|
configurable: true,
|
|
value: originalMatchMedia
|
|
});
|
|
});
|
|
|
|
it("offers the course assistant only after a route was planned", () => {
|
|
const onStartGuidance = vi.fn();
|
|
const { rerender } = render(
|
|
<RoutePlanner
|
|
startPoint={{ lat: 52, lon: 7 }}
|
|
gpsPosition={null}
|
|
destination={{ lat: 52, lon: 7.1 }}
|
|
result={null}
|
|
routeOptions={[]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onStartGuidance={onStartGuidance}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByRole("button", { name: "Navigation starten" })).not.toBeInTheDocument();
|
|
|
|
rerender(
|
|
<RoutePlanner
|
|
startPoint={{ lat: 52, lon: 7 }}
|
|
gpsPosition={null}
|
|
destination={{ lat: 52, lon: 7.1 }}
|
|
result={routeOption("primary", "Hauptroute", 4)}
|
|
routeOptions={[routeOption("primary", "Hauptroute", 4)]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onStartGuidance={onStartGuidance}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
fireEvent.click(screen.getByRole("button", { name: "Navigation starten" }));
|
|
expect(onStartGuidance).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("opens the compact result view and keeps critical warnings above secondary route details", () => {
|
|
const result = {
|
|
...routeOption("primary", "Hauptroute", 4),
|
|
warnings: [
|
|
{ code: "weather", severity: "caution" as const, message: "Wind aufmerksam beobachten." },
|
|
{ code: "bridge", severity: "critical" as const, message: "Brücke ist zu niedrig." }
|
|
]
|
|
};
|
|
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={{ lat: 52, lon: 7 }}
|
|
gpsPosition={null}
|
|
destination={{ lat: 52, lon: 7.1 }}
|
|
result={result}
|
|
routeOptions={[result]}
|
|
weatherReport={null}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Routenergebnis")).toBeVisible();
|
|
expect(screen.getByText("Brücke ist zu niedrig.")).toBeVisible();
|
|
expect(screen.getByText("Wind aufmerksam beobachten.")).toBeVisible();
|
|
expect(screen.queryByRole("button", { name: "Start auf Karte setzen" })).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Plan ändern" }));
|
|
expect(screen.getByText("Route planen")).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Start auf Karte setzen" })).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Zur Route" })).toBeVisible();
|
|
});
|
|
|
|
it("shows the route weather report after a route was calculated", () => {
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={{ lat: 54.18, lon: 12.08 }}
|
|
gpsPosition={{ lat: 54.18, lon: 12.08 }}
|
|
destination={{ lat: 54.2, lon: 12.1 }}
|
|
result={{
|
|
geometry: { type: "LineString", coordinates: [[12.08, 54.18], [12.1, 54.2]] },
|
|
distanceNm: 4.2,
|
|
eta: null,
|
|
warnings: [],
|
|
minKnownDepthM: null,
|
|
unknownDepthRatio: 1,
|
|
dataSources: [],
|
|
routingMode: "fairway"
|
|
}}
|
|
routeOptions={[]}
|
|
weatherReport={weatherReportFixture}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByRole("region", { name: "Fahrtbericht" })).toBeVisible();
|
|
expect(screen.getByText(/Aufmerksam fahren/)).toBeVisible();
|
|
expect(screen.getByText("18 kn 270°")).toBeVisible();
|
|
expect(screen.getByText("1.2 m 290°")).toBeVisible();
|
|
expect(screen.getByText(/Nicht passierbar/)).toBeVisible();
|
|
expect(screen.getByText("Niedrige Brücke")).toBeVisible();
|
|
});
|
|
|
|
it("keeps lock-delay planning and the current-adjusted ETA in the embedded route tool", () => {
|
|
const adjustedReport: RouteWeatherReport = {
|
|
...weatherReportFixture,
|
|
adjustedEta: "2026-07-13T14:45:00.000Z",
|
|
currentAdjustmentMinutes: 45,
|
|
averageAlongRouteCurrentKn: -0.6
|
|
};
|
|
const result = routeOption("primary", "Hauptroute", 18);
|
|
|
|
render(
|
|
<RoutePlanner
|
|
startPoint={{ lat: 53.3, lon: 7.2 }}
|
|
gpsPosition={null}
|
|
destination={{ lat: 52.9, lon: 7.4 }}
|
|
result={result}
|
|
routeOptions={[result]}
|
|
weatherReport={adjustedReport}
|
|
weatherLoading={false}
|
|
weatherError={null}
|
|
routeLocks={[
|
|
{
|
|
id: "lock-1",
|
|
name: "Testschleuse",
|
|
coordinate: { lat: 53.1, lon: 7.3 },
|
|
routeDistanceNm: 8,
|
|
distanceFromRouteNm: 0.02,
|
|
openingHours: "06:00-22:00",
|
|
phone: "+49 123 456",
|
|
vhf: "20",
|
|
website: null
|
|
}
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
pickMode={null}
|
|
onSubmit={vi.fn()}
|
|
onPickStart={vi.fn()}
|
|
onPickDestination={vi.fn()}
|
|
onClearStart={vi.fn()}
|
|
onClearDestination={vi.fn()}
|
|
onUseGpsAsStart={vi.fn()}
|
|
onSelectRoute={vi.fn()}
|
|
onCollapse={vi.fn()}
|
|
operationalPanelsVisible={false}
|
|
embedded
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText(/Strömungs-ETA/)).toBeVisible();
|
|
fireEvent.click(screen.getByText("Schleusenplanung · 1 auf der Route"));
|
|
expect(screen.getByRole("spinbutton", { name: /Pauschale/ })).toHaveValue(20);
|
|
expect(screen.getByText(/Plan-ETA inkl. Schleusenpuffer/)).toBeVisible();
|
|
expect(screen.queryByRole("region", { name: "Fahrtbericht" })).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
const weatherReportFixture: RouteWeatherReport = {
|
|
samples: [
|
|
{
|
|
label: "Start",
|
|
coordinate: { lat: 54.18, lon: 12.08 },
|
|
forecast: {
|
|
waveHeightM: 0.8,
|
|
waveDirectionDeg: 280,
|
|
wavePeriodS: 4,
|
|
windSpeed: 12,
|
|
windDirectionDeg: 260,
|
|
weatherCode: 2,
|
|
temperatureC: 18,
|
|
source: "Test",
|
|
updatedAt: "2026-07-13T12:00:00.000Z"
|
|
}
|
|
},
|
|
{
|
|
label: "Ziel",
|
|
coordinate: { lat: 54.2, lon: 12.1 },
|
|
forecast: {
|
|
waveHeightM: 1.2,
|
|
waveDirectionDeg: 290,
|
|
wavePeriodS: 5,
|
|
windSpeed: 18,
|
|
windDirectionDeg: 270,
|
|
weatherCode: 3,
|
|
temperatureC: 18,
|
|
source: "Test",
|
|
updatedAt: "2026-07-13T12:00:00.000Z"
|
|
}
|
|
}
|
|
],
|
|
maxWaveHeightM: 1.2,
|
|
maxWindSpeedKn: 18,
|
|
maxWavePeriodS: 5,
|
|
strongestWindDirectionDeg: 270,
|
|
highestWaveDirectionDeg: 290,
|
|
severity: "caution",
|
|
summary: "Aufmerksam fahren: bis 18 kn Wind, 1.2 m Welle.",
|
|
source: "Test",
|
|
updatedAt: "2026-07-13T12:00:00.000Z",
|
|
unavailableSamples: 0,
|
|
departureTime: "2026-07-13T12:00:00.000Z",
|
|
adjustedEta: null,
|
|
currentAdjustmentMinutes: null,
|
|
averageAlongRouteCurrentKn: null,
|
|
bridgeReport: {
|
|
bridges: [
|
|
{
|
|
id: "bridge-low",
|
|
name: "Niedrige Brücke",
|
|
label: "Niedrige Brücke H 2.2 m",
|
|
coordinate: { lat: 54.19, lon: 12.09 },
|
|
distanceNm: 0,
|
|
clearanceM: 2.2,
|
|
clearanceLabel: "H 2.2 m",
|
|
requiredAirDraftM: 2.5,
|
|
marginM: -0.3,
|
|
status: "too_low",
|
|
source: "OSM/Geofabrik"
|
|
}
|
|
],
|
|
requiredAirDraftM: 2.5,
|
|
checkedCount: 1,
|
|
unknownCount: 0,
|
|
tooLowCount: 1,
|
|
tightCount: 0,
|
|
minClearanceM: 2.2,
|
|
severity: "critical",
|
|
summary: "Nicht passierbar: 1 Brücke(n) niedriger als 2.5 m Bootshöhe.",
|
|
source: "OSM/Geofabrik",
|
|
updatedAt: "2026-07-13T12:00:00.000Z"
|
|
}
|
|
};
|
|
|
|
function routeOption(id: string, name: string, distanceNm: number) {
|
|
return {
|
|
id,
|
|
name,
|
|
geometry: { type: "LineString" as const, coordinates: [[7, 52], [7.1, 52]] as [number, number][] },
|
|
distanceNm,
|
|
eta: "2026-07-13T14:00:00.000Z",
|
|
warnings: [],
|
|
minKnownDepthM: null,
|
|
unknownDepthRatio: 1,
|
|
dataSources: [],
|
|
routingMode: "fairway" as const
|
|
};
|
|
}
|