Hinweise optimiert
This commit is contained in:
+1
-10
@@ -56,16 +56,7 @@ const routeRequestSchema = z.object({
|
||||
airDraftM: z.number().positive().max(80).optional(),
|
||||
beamM: z.number().positive().max(80).optional(),
|
||||
cruiseSpeedKn: z.number().positive().max(80).optional()
|
||||
}),
|
||||
depthSamples: z
|
||||
.array(
|
||||
z.object({
|
||||
coordinate: coordinateSchema,
|
||||
depthM: z.number().nullable()
|
||||
})
|
||||
)
|
||||
.max(500)
|
||||
.optional()
|
||||
})
|
||||
}) satisfies z.ZodType<RouteRequest>;
|
||||
|
||||
const coordinateQuerySchema = z.object({
|
||||
|
||||
@@ -54,7 +54,6 @@ export type FairwayRow = {
|
||||
source: string;
|
||||
source_id: string | null;
|
||||
name: string | null;
|
||||
min_depth_m: string | number | null;
|
||||
properties?: Record<string, unknown> | null;
|
||||
geometry: {
|
||||
type: "LineString";
|
||||
@@ -165,7 +164,6 @@ export class FairwayService {
|
||||
source,
|
||||
source_id,
|
||||
name,
|
||||
min_depth_m,
|
||||
properties,
|
||||
ST_AsGeoJSON(geom)::json AS geometry
|
||||
FROM marine_fairway_edges
|
||||
@@ -328,7 +326,6 @@ export function fairwayRowsToGraph(rows: FairwayRow[], bbox: [number, number, nu
|
||||
id: `postgis-edge-${row.id}`,
|
||||
name: row.name ?? row.source_id ?? `PostGIS ${row.id}`,
|
||||
coordinates: row.geometry.coordinates.map(([lon, lat]) => ({ lon, lat })),
|
||||
minDepthM: parseNumeric(row.min_depth_m),
|
||||
source: `postgis-${row.source}`,
|
||||
...edgeRestrictions(tags)
|
||||
};
|
||||
@@ -352,7 +349,6 @@ export function overpassToGraph(response: OverpassResponse, bbox: [number, numbe
|
||||
id: `osm-way-${element.id}`,
|
||||
name: tags.name ?? tags.ref ?? `OSM ${element.id}`,
|
||||
coordinates,
|
||||
minDepthM: parseDepth(tags),
|
||||
source: sourceFor(tags),
|
||||
...edgeRestrictions(tags)
|
||||
};
|
||||
@@ -386,7 +382,6 @@ export function localFairwaysToGraph(
|
||||
id: `local-osm-way-${way.id}`,
|
||||
name: tags.name ?? tags.ref ?? `OSM ${way.id}`,
|
||||
coordinates,
|
||||
minDepthM: parseDepth(tags),
|
||||
source: `local-geofabrik-${source}`,
|
||||
...edgeRestrictions(tags)
|
||||
};
|
||||
@@ -443,7 +438,6 @@ function waysToGraph(
|
||||
id: string;
|
||||
name: string;
|
||||
coordinates: Coordinate[];
|
||||
minDepthM: number | null;
|
||||
source: string;
|
||||
maxAirDraftM?: number | null;
|
||||
maxBeamM?: number | null;
|
||||
@@ -473,7 +467,6 @@ function waysToGraph(
|
||||
from,
|
||||
to,
|
||||
coordinates: [start, end],
|
||||
minDepthM: way.minDepthM,
|
||||
source: way.source,
|
||||
maxAirDraftM: way.maxAirDraftM,
|
||||
maxBeamM: way.maxBeamM,
|
||||
@@ -579,28 +572,6 @@ function nodeIdFor(nodes: Map<string, FairwayNode>, coordinate: Coordinate) {
|
||||
return id;
|
||||
}
|
||||
|
||||
function parseDepth(tags: Record<string, string>) {
|
||||
const candidates = [
|
||||
tags["seamark:fairway:minimum_depth"],
|
||||
tags["seamark:recommended_track:minimum_depth"],
|
||||
tags["seamark:navigation_line:minimum_depth"],
|
||||
tags["depth"],
|
||||
tags["min_depth"]
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseNumeric(candidate);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function edgeRestrictions(tags: Record<string, string>) {
|
||||
return {
|
||||
maxAirDraftM: firstNumericTag(tags, [
|
||||
|
||||
@@ -36,7 +36,6 @@ const testFairwayGraph = (
|
||||
from: `${id}-start`,
|
||||
to: `${id}-destination`,
|
||||
coordinates: [start, destination],
|
||||
minDepthM: null,
|
||||
source: "test-fairway-source"
|
||||
}
|
||||
]
|
||||
@@ -68,7 +67,6 @@ const boundedSnapSearchFixture = () => {
|
||||
from: "junction",
|
||||
to: `trap-${index + 1}`,
|
||||
coordinates: [{ lat: 52, lon: 7 }, coordinate],
|
||||
minDepthM: 4,
|
||||
oneway: true as const,
|
||||
source: `api-bounded-trap-${index + 1}`
|
||||
})),
|
||||
@@ -78,7 +76,6 @@ const boundedSnapSearchFixture = () => {
|
||||
from: "junction",
|
||||
to: "destination",
|
||||
coordinates: [{ lat: 52, lon: 7 }, destination],
|
||||
minDepthM: 4,
|
||||
oneway: true as const,
|
||||
source: "api-ninth-main-route"
|
||||
}
|
||||
@@ -220,26 +217,6 @@ describe("Watermaps API", () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("returns critical route warnings for shallow samples", async () => {
|
||||
const app = await buildServer({ cache: createCache(), env: demoEnv });
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/routes",
|
||||
payload: {
|
||||
start: { lat: 53.344167, lon: 7.186111 },
|
||||
destination: { lat: 53.563776, lon: 6.750562 },
|
||||
vesselProfile: { draughtM: 1.5, safetyReserveM: 0.4 },
|
||||
depthSamples: [{ coordinate: { lat: 53.442996, lon: 6.833146 }, depthM: 1.6 }]
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json().warnings.some((warning: { severity: string }) => warning.severity === "critical")).toBe(
|
||||
true
|
||||
);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("rejects routes without a known fairway instead of returning a straight line", async () => {
|
||||
const app = await buildServer({
|
||||
cache: createCache(),
|
||||
@@ -443,7 +420,6 @@ describe("Watermaps API", () => {
|
||||
expect(body.routingMode).toBe("fairway");
|
||||
expect(body.dataSources).toContain("fairway-graph:ems-borkum-seed");
|
||||
expect(body.geometry.coordinates.length).toBeGreaterThan(20);
|
||||
expect(body.warnings.some((warning: { code: string }) => warning.code === "FAIRWAY_ROUTE")).toBe(true);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -469,7 +445,6 @@ describe("Watermaps API", () => {
|
||||
name: "Norddeich Fahrwasser",
|
||||
from: "norddeich",
|
||||
to: "fairway",
|
||||
minDepthM: null,
|
||||
source: "local-geofabrik-test",
|
||||
coordinates: [
|
||||
{ lat: 53.6234, lon: 7.1559 },
|
||||
@@ -481,7 +456,6 @@ describe("Watermaps API", () => {
|
||||
name: "Norderney Fahrwasser",
|
||||
from: "fairway",
|
||||
to: "norderney",
|
||||
minDepthM: null,
|
||||
source: "local-geofabrik-test",
|
||||
coordinates: [
|
||||
{ lat: 53.66, lon: 7.16 },
|
||||
@@ -605,7 +579,6 @@ describe("Watermaps API", () => {
|
||||
name: "AB",
|
||||
from: "a",
|
||||
to: "b",
|
||||
minDepthM: 4,
|
||||
source: "test-overpass",
|
||||
coordinates: [
|
||||
{ lat: 54, lon: 10 },
|
||||
@@ -617,7 +590,6 @@ describe("Watermaps API", () => {
|
||||
name: "BC",
|
||||
from: "b",
|
||||
to: "c",
|
||||
minDepthM: 4,
|
||||
source: "test-overpass",
|
||||
coordinates: [
|
||||
{ lat: 54.02, lon: 10.05 },
|
||||
@@ -679,7 +651,6 @@ describe("Watermaps API", () => {
|
||||
from: "start-decoy-a",
|
||||
to: "start-decoy-b",
|
||||
coordinates: [coordinate(53.3416, 7.186), coordinate(53.342, 7.187)],
|
||||
minDepthM: null,
|
||||
source: "closer-but-disconnected-start"
|
||||
},
|
||||
{
|
||||
@@ -688,7 +659,6 @@ describe("Watermaps API", () => {
|
||||
from: "destination-decoy-a",
|
||||
to: "destination-decoy-b",
|
||||
coordinates: [coordinate(53.3282, 6.9304), coordinate(53.3286, 6.9294)],
|
||||
minDepthM: null,
|
||||
source: "closer-but-disconnected-destination"
|
||||
},
|
||||
{
|
||||
@@ -697,7 +667,6 @@ describe("Watermaps API", () => {
|
||||
from: "shared-start",
|
||||
to: "shared-east",
|
||||
coordinates: [coordinate(53.34145, 7.18585), coordinate(53.3321722, 7.1329034)],
|
||||
minDepthM: null,
|
||||
source: "local-geofabrik-germany+netherlands"
|
||||
},
|
||||
{
|
||||
@@ -706,7 +675,6 @@ describe("Watermaps API", () => {
|
||||
from: "shared-east",
|
||||
to: "shared-south",
|
||||
coordinates: [coordinate(53.3321722, 7.1329034), coordinate(53.313849, 7.0011017)],
|
||||
minDepthM: null,
|
||||
source: "local-geofabrik-germany+netherlands"
|
||||
},
|
||||
{
|
||||
@@ -715,7 +683,6 @@ describe("Watermaps API", () => {
|
||||
from: "shared-south",
|
||||
to: "shared-destination",
|
||||
coordinates: [coordinate(53.313849, 7.0011017), coordinate(53.32805, 6.9302)],
|
||||
minDepthM: null,
|
||||
source: "local-geofabrik-germany+netherlands"
|
||||
}
|
||||
]
|
||||
@@ -765,7 +732,6 @@ describe("Watermaps API", () => {
|
||||
from,
|
||||
to,
|
||||
coordinates,
|
||||
minDepthM: 4,
|
||||
source: "test-alternatives"
|
||||
});
|
||||
const app = await buildServer({
|
||||
|
||||
@@ -378,7 +378,6 @@ describe("fairway graph extraction", () => {
|
||||
source: "osm",
|
||||
source_id: "way-1",
|
||||
name: "Harbour Reach",
|
||||
min_depth_m: "4.2",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
@@ -392,7 +391,6 @@ describe("fairway graph extraction", () => {
|
||||
source: "osm",
|
||||
source_id: "way-2",
|
||||
name: "Outer Reach",
|
||||
min_depth_m: 4.2,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
@@ -430,7 +428,6 @@ describe("fairway graph extraction", () => {
|
||||
source: "osm",
|
||||
source_id: "way-main",
|
||||
name: "Ems Fahrwasser",
|
||||
min_depth_m: null,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
@@ -447,7 +444,6 @@ describe("fairway graph extraction", () => {
|
||||
source: "osm",
|
||||
source_id: "way-branch",
|
||||
name: "Busetief",
|
||||
min_depth_m: null,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
@@ -495,8 +491,7 @@ describe("fairway graph extraction", () => {
|
||||
type: "way",
|
||||
id: 123,
|
||||
tags: {
|
||||
"seamark:type": "navigation_line",
|
||||
"seamark:navigation_line:minimum_depth": "3.5"
|
||||
"seamark:type": "navigation_line"
|
||||
},
|
||||
geometry: [
|
||||
{ lat: 54, lon: 10 },
|
||||
@@ -510,7 +505,6 @@ describe("fairway graph extraction", () => {
|
||||
|
||||
expect(graph).not.toBeNull();
|
||||
expect(graph?.edges[0]?.source).toBe("osm-overpass-seamarks");
|
||||
expect(graph?.edges[0]?.minDepthM).toBe(3.5);
|
||||
});
|
||||
|
||||
it("accepts navigable canals but rejects explicitly closed waterways", () => {
|
||||
@@ -553,7 +547,6 @@ describe("fairway graph extraction", () => {
|
||||
source: "osm",
|
||||
source_id: "north",
|
||||
name: "Dortmund-Ems-Kanal",
|
||||
min_depth_m: null,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
@@ -604,7 +597,6 @@ describe("fairway graph extraction", () => {
|
||||
source: "osm",
|
||||
source_id: "way-restricted",
|
||||
name: "Niedrige Durchfahrt",
|
||||
min_depth_m: "3.0",
|
||||
properties: { maxheight: "2.4 m", maxwidth: "3.2", maxdraft: "1.8", oneway: "yes" },
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
|
||||
@@ -44,6 +44,7 @@ import { useGeolocation } from "./hooks/useGeolocation";
|
||||
import { useMarineData } from "./hooks/useMarineData";
|
||||
import type { OfflineVoyage } from "./lib/offline-route";
|
||||
import { boatCategoryLabel, toVesselProfile } from "./lib/boat-profile";
|
||||
import { isOpenRouteWarning } from "./lib/route-warnings";
|
||||
import {
|
||||
upcomingRouteEvents,
|
||||
type RouteEventEtaBasis,
|
||||
@@ -300,7 +301,7 @@ export function App() {
|
||||
}, [route]);
|
||||
|
||||
const routeWarningCount = useMemo(
|
||||
() => route?.warnings.filter((warning) => warning.severity !== "info").length ?? 0,
|
||||
() => route?.warnings.filter(isOpenRouteWarning).length ?? 0,
|
||||
[route]
|
||||
);
|
||||
const handleMapReady = useCallback(() => setMapReady(true), []);
|
||||
@@ -487,9 +488,13 @@ export function App() {
|
||||
: route
|
||||
? "active"
|
||||
: "idle",
|
||||
route: guidanceAlarm || route?.warnings.some((warning) => warning.severity === "critical")
|
||||
route: guidanceAlarm || route?.warnings.some(
|
||||
(warning) => isOpenRouteWarning(warning) && warning.severity === "critical"
|
||||
)
|
||||
? "alarm"
|
||||
: route?.warnings.some((warning) => warning.severity === "caution")
|
||||
: route?.warnings.some(
|
||||
(warning) => isOpenRouteWarning(warning) && warning.severity === "caution"
|
||||
)
|
||||
? "caution"
|
||||
: courseAssistant.active || route
|
||||
? "active"
|
||||
@@ -1006,6 +1011,7 @@ export function App() {
|
||||
headingSource={guidanceHeadingSource}
|
||||
accuracyM={gps.accuracyM}
|
||||
fixStale={courseAssistant.fixStale}
|
||||
routeWarnings={route?.warnings ?? []}
|
||||
onStop={courseAssistant.stop}
|
||||
/>
|
||||
</LazyContent>
|
||||
|
||||
@@ -190,7 +190,7 @@ export function BoatProfilePanel({
|
||||
<legend>Abmessungen und Fahrtwerte</legend>
|
||||
<p>
|
||||
Bitte die Werte für den normalen Fahrzustand eintragen. Sie
|
||||
beeinflussen Durchfahrt, Tiefenprüfung und Fahrtdauer.
|
||||
beeinflussen bekannte Durchfahrtsbeschränkungen und Fahrtdauer.
|
||||
</p>
|
||||
|
||||
<div className="boat-profile-field-grid">
|
||||
@@ -232,7 +232,7 @@ export function BoatProfilePanel({
|
||||
/>
|
||||
<NumberField
|
||||
label="Sicherheitsreserve"
|
||||
help="Zusätzlicher Abstand zum Grund beim Routing."
|
||||
help="Wird im Bootsprofil gespeichert; eine automatische Routentiefenprüfung findet derzeit nicht statt."
|
||||
unit="m"
|
||||
min={0}
|
||||
max={10}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AlertTriangle, Navigation, Square } from "lucide-react";
|
||||
import type { RouteGuidanceResult } from "@watermaps/shared";
|
||||
import type { RouteGuidanceResult, RouteWarning } from "@watermaps/shared";
|
||||
import { RouteWarnings } from "./RouteWarnings";
|
||||
import "./CourseAssistantPanel.css";
|
||||
|
||||
export type CourseAssistantPanelProps = {
|
||||
@@ -9,6 +10,7 @@ export type CourseAssistantPanelProps = {
|
||||
headingSource: "COG" | "HDG" | "--";
|
||||
accuracyM: number | null;
|
||||
fixStale: boolean;
|
||||
routeWarnings?: RouteWarning[];
|
||||
onStop: () => void;
|
||||
};
|
||||
|
||||
@@ -19,6 +21,7 @@ export function CourseAssistantPanel({
|
||||
headingSource,
|
||||
accuracyM,
|
||||
fixStale,
|
||||
routeWarnings = [],
|
||||
onStop
|
||||
}: CourseAssistantPanelProps) {
|
||||
const waitingForGps = !guidance && !fixStale;
|
||||
@@ -47,6 +50,8 @@ export function CourseAssistantPanel({
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<RouteWarnings warnings={routeWarnings} />
|
||||
|
||||
<div className="course-assistant-main">
|
||||
<span
|
||||
className="course-assistant-arrow"
|
||||
|
||||
@@ -38,9 +38,11 @@ import {
|
||||
DEFAULT_BOAT_PROFILE,
|
||||
toVesselProfile
|
||||
} from "../lib/boat-profile";
|
||||
import { isOpenRouteWarning } from "../lib/route-warnings";
|
||||
import type { RouteLock } from "../voyageHarbours";
|
||||
import { LazyContent } from "./LazyContent";
|
||||
import { NavigationDataPanel } from "./NavigationDataPanel";
|
||||
import { RouteWarnings } from "./RouteWarnings";
|
||||
import { RouteTidePanel, type RouteTidePlan } from "./RouteTidePanel";
|
||||
import { VoyagePlan } from "./VoyagePlan";
|
||||
|
||||
@@ -173,10 +175,18 @@ export function RoutePlanner({
|
||||
if (!result) {
|
||||
return "idle";
|
||||
}
|
||||
if (result.warnings.some((warning) => warning.severity === "critical")) {
|
||||
if (
|
||||
result.warnings.some(
|
||||
(warning) => isOpenRouteWarning(warning) && warning.severity === "critical"
|
||||
)
|
||||
) {
|
||||
return "critical";
|
||||
}
|
||||
if (result.warnings.some((warning) => warning.severity === "caution")) {
|
||||
if (
|
||||
result.warnings.some(
|
||||
(warning) => isOpenRouteWarning(warning) && warning.severity === "caution"
|
||||
)
|
||||
) {
|
||||
return "caution";
|
||||
}
|
||||
return "ok";
|
||||
@@ -207,9 +217,6 @@ export function RoutePlanner({
|
||||
}
|
||||
return new Date(timestamp + routeLocks.length * lockDelayMinutes * 60_000).toISOString();
|
||||
}, [lockDelayMinutes, result?.eta, routeLocks.length, weatherReport?.adjustedEta]);
|
||||
const criticalWarnings = result?.warnings.filter((warning) => warning.severity === "critical") ?? [];
|
||||
const otherWarnings = result?.warnings.filter((warning) => warning.severity !== "critical") ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!result) {
|
||||
setPlannerView("input");
|
||||
@@ -554,9 +561,7 @@ export function RoutePlanner({
|
||||
<span>
|
||||
{result.routingMode === "fairway"
|
||||
? "Fahrwasser"
|
||||
: result.unknownDepthRatio > 0
|
||||
? `${Math.round(result.unknownDepthRatio * 100)}% unbekannt`
|
||||
: "Tiefe OK"}
|
||||
: "Direkte Wegpunkte"}
|
||||
</span>
|
||||
{(weatherReport?.adjustedEta ?? result.eta) && (
|
||||
<span>
|
||||
@@ -578,52 +583,7 @@ export function RoutePlanner({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{(error || criticalWarnings.length > 0) && (
|
||||
<section
|
||||
className="warning-list route-priority-warnings"
|
||||
aria-label="Kritische Hinweise"
|
||||
role="alert"
|
||||
>
|
||||
<strong>Kritische Hinweise</strong>
|
||||
{error && (
|
||||
<p>
|
||||
<AlertTriangle size={14} aria-hidden="true" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{criticalWarnings.map((warning, index) => (
|
||||
<p key={`${warning.code}-${index}`}>
|
||||
<AlertTriangle size={14} aria-hidden="true" />
|
||||
{warning.message}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{otherWarnings.length > 0 && (
|
||||
<section className="warning-list" aria-label="Weitere Hinweise">
|
||||
<strong>Weitere Hinweise</strong>
|
||||
{otherWarnings.slice(0, 3).map((warning, index) => (
|
||||
<p key={`${warning.code}-${index}`}>
|
||||
<AlertTriangle size={14} aria-hidden="true" />
|
||||
{warning.message}
|
||||
</p>
|
||||
))}
|
||||
{otherWarnings.length > 3 && (
|
||||
<details className="voyage-tools-disclosure">
|
||||
<summary>{otherWarnings.length - 3} weitere Hinweise</summary>
|
||||
<div className="warning-list">
|
||||
{otherWarnings.slice(3).map((warning, index) => (
|
||||
<p key={`${warning.code}-${index + 3}`}>
|
||||
<AlertTriangle size={14} aria-hidden="true" />
|
||||
{warning.message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
<RouteWarnings warnings={result.warnings} error={error} />
|
||||
|
||||
{routeOptions.length > 1 && (
|
||||
<section className="route-options" aria-label="Routenalternativen">
|
||||
|
||||
@@ -37,7 +37,10 @@ export function RouteTidePanel({ plan, loading, error }: RouteTidePanelProps) {
|
||||
<TideLocation label="Ziel" summary={plan.destination} />
|
||||
</div>
|
||||
)}
|
||||
<small>Stationsabstand und Bezugsnull beachten; Wasserstände ersetzen keine amtliche Tiefenprüfung.</small>
|
||||
<small>
|
||||
Stationsabstand und Bezugsnull beachten; angezeigte Wasserstände geben keine befahrbare
|
||||
Wassertiefe an.
|
||||
</small>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { AlertTriangle, Info } from "lucide-react";
|
||||
import type { RouteWarning } from "@watermaps/shared";
|
||||
import { isOpenRouteWarning, isRetainedRouteWarning } from "../lib/route-warnings";
|
||||
|
||||
export type RouteWarningsProps = {
|
||||
warnings: RouteWarning[];
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export function RouteWarnings({ warnings, error = null }: RouteWarningsProps) {
|
||||
const openWarnings = warnings.filter(isOpenRouteWarning);
|
||||
const criticalWarnings = openWarnings.filter((warning) => warning.severity === "critical");
|
||||
const cautionWarnings = openWarnings.filter((warning) => warning.severity === "caution");
|
||||
const informationalWarnings = warnings.filter(
|
||||
(warning) => isRetainedRouteWarning(warning) && warning.severity === "info"
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(error || openWarnings.length > 0) && (
|
||||
<section className="warning-list" aria-label="Routing-Hinweise">
|
||||
{openWarnings.length > 0 && <strong>Hinweise ({openWarnings.length})</strong>}
|
||||
{(error || criticalWarnings.length > 0) && (
|
||||
<div
|
||||
className="warning-list route-priority-warnings"
|
||||
aria-label="Kritische Hinweise"
|
||||
role="alert"
|
||||
>
|
||||
<strong>Kritische Hinweise</strong>
|
||||
{error && (
|
||||
<p>
|
||||
<AlertTriangle size={14} aria-hidden="true" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{criticalWarnings.map((warning, index) => (
|
||||
<p key={`${warning.code}-${index}`}>
|
||||
<AlertTriangle size={14} aria-hidden="true" />
|
||||
{warning.message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{cautionWarnings.map((warning, index) => (
|
||||
<p key={`${warning.code}-${index}`}>
|
||||
<AlertTriangle size={14} aria-hidden="true" />
|
||||
{warning.message}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{informationalWarnings.length > 0 && (
|
||||
<details className="voyage-tools-disclosure">
|
||||
<summary>Routingdetails ({informationalWarnings.length})</summary>
|
||||
<div className="warning-list">
|
||||
{informationalWarnings.map((warning, index) => (
|
||||
<p key={`${warning.code}-${index}`}>
|
||||
<Info size={14} aria-hidden="true" />
|
||||
{warning.message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RouteResult } from "@watermaps/shared";
|
||||
import { isRetainedRouteWarning } from "./route-warnings";
|
||||
|
||||
const DEFAULT_CREATOR = "Watermaps";
|
||||
|
||||
@@ -25,7 +26,10 @@ export function createRouteGpx(route: RouteResult, options: GpxExportOptions = {
|
||||
const createdAt = normalizeDate(options.createdAt);
|
||||
const source = cleanText(route.dataSources.join(", ") || DEFAULT_CREATOR, 500);
|
||||
const warningSummary = cleanText(
|
||||
route.warnings.map((warning) => warning.message).join(" · ") || "Nicht amtliche Routenplanung",
|
||||
route.warnings
|
||||
.filter(isRetainedRouteWarning)
|
||||
.map((warning) => warning.message)
|
||||
.join(" · ") || "Nicht amtliche Routenplanung",
|
||||
1_000
|
||||
);
|
||||
const bounds = routeBounds(coordinates);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
RouteWarning,
|
||||
VesselProfile
|
||||
} from "@watermaps/shared";
|
||||
import { isRetainedRouteWarning } from "./route-warnings";
|
||||
|
||||
export const OFFLINE_VOYAGES_STORAGE_KEY = "watermaps.offline-voyages.v1";
|
||||
export const LEGACY_OFFLINE_VOYAGES_STORAGE_KEY = "seacompass.offline-voyages.v1";
|
||||
@@ -211,8 +212,6 @@ function normalizeRoute(value: unknown): RouteResult {
|
||||
const coordinates = normalizeLineCoordinates(value.geometry.coordinates);
|
||||
const distanceNm = finiteNumber(value.distanceNm, 0, 100_000);
|
||||
const eta = value.eta === null ? null : limitedString(value.eta, 100);
|
||||
const minKnownDepthM = value.minKnownDepthM === null ? null : finiteNumber(value.minKnownDepthM, 0, 20_000);
|
||||
const unknownDepthRatio = finiteNumber(value.unknownDepthRatio, 0, 1);
|
||||
const warnings = normalizeWarnings(value.warnings);
|
||||
const dataSources = normalizeStrings(value.dataSources, 100, 500);
|
||||
const id = value.id === undefined ? undefined : limitedString(value.id, 160);
|
||||
@@ -232,8 +231,6 @@ function normalizeRoute(value: unknown): RouteResult {
|
||||
distanceNm,
|
||||
eta,
|
||||
warnings,
|
||||
minKnownDepthM,
|
||||
unknownDepthRatio,
|
||||
dataSources,
|
||||
...(departureTime ? { departureTime } : {}),
|
||||
...(durationMinutes !== undefined ? { durationMinutes } : {}),
|
||||
@@ -320,23 +317,25 @@ function normalizeWarnings(value: unknown): RouteWarning[] {
|
||||
if (!Array.isArray(value) || value.length > 500) {
|
||||
throw new OfflineVoyageStorageError("Die Routenwarnungen sind ungültig.");
|
||||
}
|
||||
return value.map((warning) => {
|
||||
if (!isRecord(warning)) {
|
||||
throw new OfflineVoyageStorageError("Eine Routenwarnung ist ungültig.");
|
||||
}
|
||||
const code = limitedString(warning.code, 100);
|
||||
const message = limitedString(warning.message, 1_000);
|
||||
const severity = warning.severity;
|
||||
if (!code || !message || (severity !== "info" && severity !== "caution" && severity !== "critical")) {
|
||||
throw new OfflineVoyageStorageError("Eine Routenwarnung ist ungültig.");
|
||||
}
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
severity,
|
||||
...(warning.coordinate === undefined ? {} : { coordinate: normalizeCoordinate(warning.coordinate) })
|
||||
};
|
||||
});
|
||||
return value
|
||||
.map((warning): RouteWarning => {
|
||||
if (!isRecord(warning)) {
|
||||
throw new OfflineVoyageStorageError("Eine Routenwarnung ist ungültig.");
|
||||
}
|
||||
const code = limitedString(warning.code, 100);
|
||||
const message = limitedString(warning.message, 1_000);
|
||||
const severity = warning.severity;
|
||||
if (!code || !message || (severity !== "info" && severity !== "caution" && severity !== "critical")) {
|
||||
throw new OfflineVoyageStorageError("Eine Routenwarnung ist ungültig.");
|
||||
}
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
severity,
|
||||
...(warning.coordinate === undefined ? {} : { coordinate: normalizeCoordinate(warning.coordinate) })
|
||||
};
|
||||
})
|
||||
.filter(isRetainedRouteWarning);
|
||||
}
|
||||
|
||||
function normalizeStrings(value: unknown, maxItems: number, maxLength: number): string[] {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { RouteWarning } from "@watermaps/shared";
|
||||
|
||||
const REMOVED_ROUTE_WARNING_CODES = new Set([
|
||||
"FAIRWAY_ROUTE",
|
||||
"FAIRWAY_DATA_NOT_OFFICIAL",
|
||||
"DEPTH_UNKNOWN",
|
||||
"DEPTH_PARTIAL",
|
||||
"NO_KNOWN_DEPTH",
|
||||
"DEPTH_TOO_SHALLOW"
|
||||
]);
|
||||
|
||||
export function isRetainedRouteWarning(warning: RouteWarning): boolean {
|
||||
return !REMOVED_ROUTE_WARNING_CODES.has(warning.code);
|
||||
}
|
||||
|
||||
export function isOpenRouteWarning(warning: RouteWarning): boolean {
|
||||
return isRetainedRouteWarning(warning) && warning.severity !== "info";
|
||||
}
|
||||
@@ -98,8 +98,6 @@ function route(id: string, coordinates: [number, number][]): RouteResult {
|
||||
distanceNm: 1,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: ["Test"],
|
||||
routingMode: "fairway"
|
||||
};
|
||||
|
||||
@@ -80,4 +80,52 @@ describe("CourseAssistantPanel", () => {
|
||||
expect(screen.getByText(/GPS-Fix ist veraltet/)).toBeVisible();
|
||||
expect(screen.getByText("---")).toBeVisible();
|
||||
});
|
||||
|
||||
it("keeps every open route warning visible while guidance is active", () => {
|
||||
render(
|
||||
<CourseAssistantPanel
|
||||
guidance={null}
|
||||
gpsStatus="tracking"
|
||||
headingDeg={null}
|
||||
headingSource="--"
|
||||
accuracyM={null}
|
||||
fixStale={false}
|
||||
routeWarnings={[
|
||||
{
|
||||
code: "ROUTING_SOURCE",
|
||||
severity: "info",
|
||||
message: "Routingquelle: lokaler Graph."
|
||||
},
|
||||
{
|
||||
code: "BRIDGE_TOO_LOW",
|
||||
severity: "caution",
|
||||
message: "Eine Brücke ist zu niedrig."
|
||||
},
|
||||
{
|
||||
code: "ROUTE_SEARCH_LIMITED",
|
||||
severity: "caution",
|
||||
message: "Die Snap-Suche wurde begrenzt."
|
||||
},
|
||||
{
|
||||
code: "START_SNAP_TOO_FAR",
|
||||
severity: "caution",
|
||||
message: "Der Startpunkt liegt weit vom Fahrwasser entfernt."
|
||||
},
|
||||
{
|
||||
code: "DESTINATION_SNAP_TOO_FAR",
|
||||
severity: "caution",
|
||||
message: "Der Zielpunkt liegt weit vom Fahrwasser entfernt."
|
||||
}
|
||||
]}
|
||||
onStop={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Hinweise (4)")).toBeVisible();
|
||||
expect(screen.getByText("Eine Brücke ist zu niedrig.")).toBeVisible();
|
||||
expect(screen.getByText("Die Snap-Suche wurde begrenzt.")).toBeVisible();
|
||||
expect(screen.getByText("Der Startpunkt liegt weit vom Fahrwasser entfernt.")).toBeVisible();
|
||||
expect(screen.getByText("Der Zielpunkt liegt weit vom Fahrwasser entfernt.")).toBeVisible();
|
||||
expect(screen.getByText("Routingdetails (1)")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,8 +128,6 @@ test("starts a visible dynamic course assistant only after route planning", asyn
|
||||
distanceNm: 0.7,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: ["E2E"],
|
||||
routingMode: "fairway"
|
||||
})
|
||||
|
||||
@@ -35,6 +35,33 @@ describe("GPX export", () => {
|
||||
geometry: { type: "LineString", coordinates: [[7, 53], [181, 52]] }
|
||||
})).toThrow(/ungültige Koordinaten/);
|
||||
});
|
||||
|
||||
it("does not export retired route warnings", () => {
|
||||
const gpx = createRouteGpx({
|
||||
...routeFixture,
|
||||
warnings: [
|
||||
{
|
||||
code: "FAIRWAY_ROUTE",
|
||||
severity: "info",
|
||||
message: "Alter Graph-Hinweis"
|
||||
},
|
||||
{
|
||||
code: "NO_KNOWN_DEPTH",
|
||||
severity: "caution",
|
||||
message: "Alter Tiefen-Hinweis"
|
||||
},
|
||||
{
|
||||
code: "ROUTE_SEARCH_LIMITED",
|
||||
severity: "caution",
|
||||
message: "Aktueller Such-Hinweis"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(gpx).not.toContain("Alter Graph-Hinweis");
|
||||
expect(gpx).not.toContain("Alter Tiefen-Hinweis");
|
||||
expect(gpx).toContain("Aktueller Such-Hinweis");
|
||||
});
|
||||
});
|
||||
|
||||
const routeFixture: RouteResult = {
|
||||
@@ -47,8 +74,6 @@ const routeFixture: RouteResult = {
|
||||
distanceNm: 153.69,
|
||||
eta: null,
|
||||
warnings: [{ code: "NOT_OFFICIAL", severity: "caution", message: "Nicht amtlich & vor Ort prüfen" }],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: ["OpenStreetMap <curated>"],
|
||||
routingMode: "fairway"
|
||||
};
|
||||
|
||||
@@ -530,8 +530,6 @@ describe("MapView marine feature information", () => {
|
||||
distanceNm: 120,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: [],
|
||||
routingMode: "fairway"
|
||||
};
|
||||
|
||||
@@ -22,9 +22,9 @@ describe("NavigationToolRail", () => {
|
||||
anchor: "alarm",
|
||||
conditions: "active",
|
||||
upcoming: "caution",
|
||||
route: "stale"
|
||||
route: "caution"
|
||||
}}
|
||||
badges={{ upcoming: 123 }}
|
||||
badges={{ upcoming: 123, route: 4 }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,10 @@ describe("NavigationToolRail", () => {
|
||||
expect(upcoming).toHaveAttribute("data-status", "caution");
|
||||
expect(within(upcoming).getByText("99+")).toBeVisible();
|
||||
|
||||
const route = screen.getByRole("button", { name: /Route, Daten veraltet, öffnen/ });
|
||||
const route = screen.getByRole("button", {
|
||||
name: /Route, Warnung, 4 Hinweise, öffnen/
|
||||
});
|
||||
expect(within(route).getByText("4")).toBeVisible();
|
||||
fireEvent.click(route);
|
||||
expect(onSelect).toHaveBeenCalledWith("route");
|
||||
});
|
||||
|
||||
@@ -68,6 +68,71 @@ describe("offline voyage storage", () => {
|
||||
expect(listOfflineVoyages(localStorage)).toEqual([]);
|
||||
});
|
||||
|
||||
it("removes retired warnings and depth fields from legacy offline routes", () => {
|
||||
const record = createOfflineVoyageRecord(
|
||||
{ route: routeFixture },
|
||||
{ id: "legacy-voyage", savedAt: "2026-07-19T12:00:00.000Z" }
|
||||
);
|
||||
const legacyRecord = {
|
||||
...record,
|
||||
route: {
|
||||
...record.route,
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
warnings: [
|
||||
{
|
||||
code: "FAIRWAY_ROUTE",
|
||||
severity: "info",
|
||||
message: "Alter Graph-Hinweis"
|
||||
},
|
||||
{
|
||||
code: "FAIRWAY_DATA_NOT_OFFICIAL",
|
||||
severity: "caution",
|
||||
message: "Alter Daten-Hinweis"
|
||||
},
|
||||
{
|
||||
code: "DEPTH_UNKNOWN",
|
||||
severity: "caution",
|
||||
message: "Alter Tiefenpunkt-Hinweis"
|
||||
},
|
||||
{
|
||||
code: "DEPTH_PARTIAL",
|
||||
severity: "caution",
|
||||
message: "Alter Teil-Tiefenhinweis"
|
||||
},
|
||||
{
|
||||
code: "NO_KNOWN_DEPTH",
|
||||
severity: "caution",
|
||||
message: "Alter Unbekannt-Hinweis"
|
||||
},
|
||||
{
|
||||
code: "DEPTH_TOO_SHALLOW",
|
||||
severity: "critical",
|
||||
message: "Alter Flachwasser-Hinweis"
|
||||
},
|
||||
{
|
||||
code: "ROUTE_SEARCH_LIMITED",
|
||||
severity: "caution",
|
||||
message: "Suche begrenzt"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
localStorage.setItem(OFFLINE_VOYAGES_STORAGE_KEY, JSON.stringify([legacyRecord]));
|
||||
|
||||
const restored = loadOfflineVoyage("legacy-voyage", localStorage);
|
||||
|
||||
expect(restored?.route.warnings).toEqual([
|
||||
{
|
||||
code: "ROUTE_SEARCH_LIMITED",
|
||||
severity: "caution",
|
||||
message: "Suche begrenzt"
|
||||
}
|
||||
]);
|
||||
expect(restored?.route).not.toHaveProperty("minKnownDepthM");
|
||||
expect(restored?.route).not.toHaveProperty("unknownDepthRatio");
|
||||
});
|
||||
|
||||
it("rejects unsafe route coordinates before writing", () => {
|
||||
expect(() => createOfflineVoyageRecord({
|
||||
route: {
|
||||
@@ -91,8 +156,6 @@ const routeFixture: RouteResult = {
|
||||
durationMinutes: 120,
|
||||
eta: "2026-07-20T08:00:00.000Z",
|
||||
warnings: [{ code: "NOT_OFFICIAL", severity: "caution", message: "Nicht amtlich" }],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: ["OSM"],
|
||||
routingMode: "fairway",
|
||||
routeSnaps: {
|
||||
@@ -115,8 +178,6 @@ const routeFixture: RouteResult = {
|
||||
distanceNm: 160,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: ["OSM"]
|
||||
}]
|
||||
};
|
||||
|
||||
@@ -22,8 +22,6 @@ const route: RouteResult = {
|
||||
distanceNm: 60,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: ["test"]
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { act, cleanup, fireEvent, render, screen, within } 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";
|
||||
@@ -416,6 +416,7 @@ describe("RoutePlanner", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText("Routenergebnis")).toBeVisible();
|
||||
expect(screen.getByText("Hinweise (2)")).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();
|
||||
@@ -426,6 +427,89 @@ describe("RoutePlanner", () => {
|
||||
expect(screen.getByRole("button", { name: "Zur Route" })).toBeVisible();
|
||||
});
|
||||
|
||||
it("filters retired fairway and depth warnings from route results", () => {
|
||||
const result = {
|
||||
...routeOption("primary", "Hauptroute", 24.48),
|
||||
warnings: [
|
||||
{
|
||||
code: "FAIRWAY_ROUTE",
|
||||
severity: "info" as const,
|
||||
message: "Entfernter Graph-Hinweis."
|
||||
},
|
||||
{
|
||||
code: "FAIRWAY_DATA_NOT_OFFICIAL",
|
||||
severity: "caution" as const,
|
||||
message: "Entfernter Daten-Hinweis."
|
||||
},
|
||||
{
|
||||
code: "ROUTE_SEARCH_LIMITED",
|
||||
severity: "caution" as const,
|
||||
message: "Die Snap-Suche wurde begrenzt."
|
||||
},
|
||||
{
|
||||
code: "DEPTH_PARTIAL",
|
||||
severity: "caution" as const,
|
||||
message: "Entfernter Teil-Tiefenhinweis."
|
||||
},
|
||||
{
|
||||
code: "NO_KNOWN_DEPTH",
|
||||
severity: "caution" as const,
|
||||
message: "Entfernter Unbekannt-Hinweis."
|
||||
},
|
||||
{
|
||||
code: "DEPTH_UNKNOWN",
|
||||
severity: "caution" as const,
|
||||
message: "Entfernter Tiefenpunkt-Hinweis."
|
||||
},
|
||||
{
|
||||
code: "DEPTH_TOO_SHALLOW",
|
||||
severity: "critical" as const,
|
||||
message: "Entfernter Flachwasser-Hinweis."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
render(
|
||||
<RoutePlanner
|
||||
startPoint={{ lat: 53.4647, lon: 7.474 }}
|
||||
gpsPosition={null}
|
||||
destination={{ lat: 53.5147, lon: 8.1203 }}
|
||||
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()}
|
||||
embedded
|
||||
/>
|
||||
);
|
||||
|
||||
const warnings = screen.getByRole("region", { name: "Routing-Hinweise" });
|
||||
expect(within(warnings).getByText("Hinweise (1)")).toBeVisible();
|
||||
expect(within(warnings).getByText("Die Snap-Suche wurde begrenzt.")).toBeVisible();
|
||||
expect(screen.queryByText("Entfernter Graph-Hinweis.")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Entfernter Daten-Hinweis.")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Entfernter Teil-Tiefenhinweis.")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Entfernter Unbekannt-Hinweis.")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Entfernter Tiefenpunkt-Hinweis.")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Entfernter Flachwasser-Hinweis.")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Routingdetails/)).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Hauptroute").closest(".route-result")).toHaveAttribute(
|
||||
"data-severity",
|
||||
"caution"
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the route weather report after a route was calculated", () => {
|
||||
render(
|
||||
<RoutePlanner
|
||||
@@ -437,8 +521,6 @@ describe("RoutePlanner", () => {
|
||||
distanceNm: 4.2,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: [],
|
||||
routingMode: "fairway"
|
||||
}}
|
||||
@@ -608,8 +690,6 @@ function routeOption(id: string, name: string, distanceNm: number) {
|
||||
distanceNm,
|
||||
eta: "2026-07-13T14:00:00.000Z",
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: [],
|
||||
routingMode: "fairway" as const
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { RouteWarning } from "@watermaps/shared";
|
||||
import {
|
||||
isOpenRouteWarning,
|
||||
isRetainedRouteWarning
|
||||
} from "../src/lib/route-warnings";
|
||||
|
||||
describe("route warning filters", () => {
|
||||
it("filters every retired fairway and depth warning code centrally", () => {
|
||||
const retiredCodes = [
|
||||
"FAIRWAY_ROUTE",
|
||||
"FAIRWAY_DATA_NOT_OFFICIAL",
|
||||
"DEPTH_UNKNOWN",
|
||||
"DEPTH_PARTIAL",
|
||||
"NO_KNOWN_DEPTH",
|
||||
"DEPTH_TOO_SHALLOW"
|
||||
];
|
||||
|
||||
for (const code of retiredCodes) {
|
||||
const warning: RouteWarning = {
|
||||
code,
|
||||
severity: "critical",
|
||||
message: `Alter Hinweis ${code}`
|
||||
};
|
||||
expect(isRetainedRouteWarning(warning)).toBe(false);
|
||||
expect(isOpenRouteWarning(warning)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("retains current warnings and only counts non-informational ones as open", () => {
|
||||
expect(
|
||||
isRetainedRouteWarning({
|
||||
code: "ROUTE_SEARCH_LIMITED",
|
||||
severity: "caution",
|
||||
message: "Suche begrenzt"
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOpenRouteWarning({
|
||||
code: "ROUTE_SEARCH_LIMITED",
|
||||
severity: "caution",
|
||||
message: "Suche begrenzt"
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOpenRouteWarning({
|
||||
code: "ROUTING_SOURCE",
|
||||
severity: "info",
|
||||
message: "Routingquelle"
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -148,8 +148,6 @@ const routeFixture: RouteResult = {
|
||||
distanceNm: 25,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: [],
|
||||
routingMode: "fairway"
|
||||
};
|
||||
|
||||
@@ -126,8 +126,6 @@ const routeFixture: RouteResult = {
|
||||
distanceNm: 0.4,
|
||||
eta: null,
|
||||
warnings: [],
|
||||
minKnownDepthM: null,
|
||||
unknownDepthRatio: 1,
|
||||
dataSources: ["Test"],
|
||||
routingMode: "fairway"
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user