Hinweise optimiert
Test and publish container images / test (push) Successful in 2m27s
Test and publish container images / publish (push) Failing after 4s

This commit is contained in:
BuTzZ
2026-07-27 13:04:32 +02:00
parent 9813a1fafb
commit b98ec8bc6f
34 changed files with 500 additions and 399 deletions
+2 -84
View File
@@ -1,7 +1,6 @@
import { haversineDistanceNm, sumRouteDistanceNm } from "./geo.js";
import type {
Coordinate,
DepthSample,
RouteOption,
RouteRequest,
RouteResult,
@@ -22,7 +21,6 @@ export type FairwayEdge = {
from: string;
to: string;
coordinates: Coordinate[];
minDepthM: number | null;
maxAirDraftM?: number | null;
maxBeamM?: number | null;
maxDraughtM?: number | null;
@@ -126,7 +124,6 @@ export const EMDEN_BORKUM_DEMO_GRAPH: FairwayGraph = {
name: "Emden Außenhafen Ausfahrt",
from: "emden-aussenhafen",
to: "emden-harbour-leading",
minDepthM: 8.5,
coordinates: [
{ lat: 53.344167, lon: 7.186111 },
{ lat: 53.339, lon: 7.191 },
@@ -138,7 +135,6 @@ export const EMDEN_BORKUM_DEMO_GRAPH: FairwayGraph = {
name: "Emder Leitlinie zum Emsfahrwasser",
from: "emden-harbour-leading",
to: "ems-fairway-inner",
minDepthM: 8.5,
coordinates: [
{ lat: 53.333897, lon: 7.201469 },
{ lat: 53.332236, lon: 7.132959 },
@@ -150,7 +146,6 @@ export const EMDEN_BORKUM_DEMO_GRAPH: FairwayGraph = {
name: "Inneres Emsfahrwasser",
from: "ems-fairway-inner",
to: "knock-reach",
minDepthM: 8.5,
coordinates: [
{ lat: 53.33532, lon: 7.132674 },
{ lat: 53.333, lon: 7.075 },
@@ -162,7 +157,6 @@ export const EMDEN_BORKUM_DEMO_GRAPH: FairwayGraph = {
name: "Außenems über Paapsand",
from: "knock-reach",
to: "paapsand-reach",
minDepthM: 7.5,
coordinates: [
{ lat: 53.327597, lon: 7.015002 },
{ lat: 53.323764, lon: 6.985 },
@@ -175,7 +169,6 @@ export const EMDEN_BORKUM_DEMO_GRAPH: FairwayGraph = {
name: "Westerems Südansteuerung",
from: "paapsand-reach",
to: "westerems-south",
minDepthM: 7,
coordinates: [
{ lat: 53.339, lon: 6.93 },
{ lat: 53.348, lon: 6.9 },
@@ -188,7 +181,6 @@ export const EMDEN_BORKUM_DEMO_GRAPH: FairwayGraph = {
name: "Westerems Fahrwasser",
from: "westerems-south",
to: "eemshaven-approach",
minDepthM: 7,
coordinates: [
{ lat: 53.376, lon: 6.865 },
{ lat: 53.398, lon: 6.85 },
@@ -201,7 +193,6 @@ export const EMDEN_BORKUM_DEMO_GRAPH: FairwayGraph = {
name: "Borkum Südansteuerung",
from: "eemshaven-approach",
to: "borkum-south-approach",
minDepthM: 6,
coordinates: [
{ lat: 53.442996, lon: 6.833146 },
{ lat: 53.463, lon: 6.807 },
@@ -214,7 +205,6 @@ export const EMDEN_BORKUM_DEMO_GRAPH: FairwayGraph = {
name: "Borkum Reede Ansteuerung",
from: "borkum-south-approach",
to: "borkum-reede",
minDepthM: 5,
coordinates: [
{ lat: 53.505, lon: 6.765 },
{ lat: 53.525, lon: 6.758 },
@@ -382,19 +372,7 @@ function buildFairwayRouteCandidate(
const departureTimestamp = requestedDepartureTimestamp(request.departureTime);
const durationMinutes = Math.round((distanceNm / speedKn) * 60);
const eta = new Date(departureTimestamp + durationMinutes * 60 * 1000).toISOString();
const depthSamples = request.depthSamples ?? edgeDepthSamples(usedEdges);
const depthAssessment = assessFairwayDepthSamples(depthSamples, request.vesselProfile);
const warnings: RouteWarning[] = [
{
code: "FAIRWAY_ROUTE",
severity: "info",
message: `Route über bekannten Fahrwasser-Graphen: ${routableGraph.name}.`
},
{
code: "FAIRWAY_DATA_NOT_OFFICIAL",
severity: "caution",
message: "Fahrwasser-Graph ist eine MVP-Planungshilfe und keine amtliche Navigationsgrundlage."
},
...(searchTruncated
? [
{
@@ -406,8 +384,7 @@ function buildFairwayRouteCandidate(
}
]
: []),
...routeSnapWarnings(routeSnaps),
...depthAssessment.warnings
...routeSnapWarnings(routeSnaps)
];
const route: RouteResult = {
@@ -420,16 +397,9 @@ function buildFairwayRouteCandidate(
departureTime: new Date(departureTimestamp).toISOString(),
durationMinutes,
warnings,
minKnownDepthM: depthAssessment.minKnownDepthM,
unknownDepthRatio: depthAssessment.unknownDepthRatio,
dataSources: [
`fairway-graph:${routableGraph.id}`,
...uniqueSources(usedEdges),
request.depthSamples?.length
? "submitted-depth-samples"
: usedEdges.some((edge) => edge.minDepthM === null)
? "fairway-depth-unknown"
: "fairway-depth-estimates"
...uniqueSources(usedEdges)
],
routingMode: "fairway",
routeSnaps
@@ -620,14 +590,10 @@ export function mergeFairwayGraphs(id: string, name: string, graphs: FairwayGrap
}
function filterRestrictedEdges(graph: FairwayGraph, request: RouteRequest): FairwayGraph {
const requiredDepthM = request.vesselProfile.draughtM + request.vesselProfile.safetyReserveM;
return {
...graph,
maxSnapDistanceNm: Math.min(graph.maxSnapDistanceNm, MAX_ROUTE_SNAP_DISTANCE_NM),
edges: graph.edges.filter((edge) => {
if (edge.minDepthM !== null && edge.minDepthM < requiredDepthM) {
return false;
}
if (edge.maxDraughtM != null && request.vesselProfile.draughtM > edge.maxDraughtM) {
return false;
}
@@ -1099,13 +1065,6 @@ function edgeLengthNm(edge: FairwayEdge) {
return sumRouteDistanceNm(edge.coordinates);
}
function edgeDepthSamples(edges: FairwayEdge[]): DepthSample[] {
return edges.map((edge) => ({
coordinate: edge.coordinates[Math.floor(edge.coordinates.length / 2)]!,
depthM: edge.minDepthM
}));
}
function uniqueSources(edges: FairwayEdge[]) {
const sources = new Set<string>();
for (const edge of edges) {
@@ -1114,47 +1073,6 @@ function uniqueSources(edges: FairwayEdge[]) {
return [...sources];
}
function assessFairwayDepthSamples(
samples: DepthSample[],
profile: VesselProfile
): {
minKnownDepthM: number | null;
unknownDepthRatio: number;
warnings: RouteWarning[];
} {
const known = samples.filter((sample) => typeof sample.depthM === "number");
const unknownDepthRatio = samples.length > 0 ? round((samples.length - known.length) / samples.length, 2) : 1;
const minKnownDepthM = known.length > 0 ? Math.min(...known.map((sample) => sample.depthM!)) : null;
const requiredDepthM = round(profile.draughtM + profile.safetyReserveM, 2);
const warnings: RouteWarning[] = [];
if (unknownDepthRatio > 0) {
warnings.push({
code: "DEPTH_PARTIAL",
severity: unknownDepthRatio > 0.5 ? "caution" : "info",
message: `${Math.round(unknownDepthRatio * 100)}% der Route haben keine Tiefenprobe.`
});
}
if (minKnownDepthM !== null && minKnownDepthM < requiredDepthM) {
warnings.push({
code: "DEPTH_TOO_SHALLOW",
severity: "critical",
message: `Minimale bekannte Tiefe ${round(minKnownDepthM, 1)} m unterschreitet erforderliche Tiefe ${requiredDepthM} m.`
});
}
if (known.length === 0) {
warnings.push({
code: "NO_KNOWN_DEPTH",
severity: "caution",
message: "Alle geprüften Tiefenpunkte sind unbekannt."
});
}
return { minKnownDepthM, unknownDepthRatio, warnings };
}
function appendCoordinate(points: Coordinate[], coordinate: Coordinate) {
const previous = points.at(-1);
if (previous && haversineDistanceNm(previous, coordinate) < 0.000001) {
-4
View File
@@ -274,7 +274,6 @@ export const EMDEN_HAMM_GRAPH: FairwayGraph = {
{ lat: 52.622597, lon: 7.3071821 },
{ lat: 52.6170897, lon: 7.3077526 }
],
minDepthM: null,
maxDraughtM: 2.5,
source: "openstreetmap-geofabrik-curated-seed"
},
@@ -454,7 +453,6 @@ export const EMDEN_HAMM_GRAPH: FairwayGraph = {
{ lat: 51.6572451, lon: 7.362679 },
{ lat: 51.6480728, lon: 7.3527065 }
],
minDepthM: null,
maxDraughtM: 2.5,
source: "openstreetmap-nominatim-curated-seed"
},
@@ -511,7 +509,6 @@ export const EMDEN_HAMM_GRAPH: FairwayGraph = {
{ lat: 51.6792115, lon: 7.9600807 },
{ lat: 51.677629, lon: 7.9640468 }
],
minDepthM: null,
maxDraughtM: 2.5,
source: "openstreetmap-nominatim-curated-seed"
}
@@ -537,7 +534,6 @@ export const EMDEN_EAST_EMS_GRAPH: FairwayGraph = {
name: "Unterems östlich von Emden",
from: "emden-east-start",
to: "emden-east-destination",
minDepthM: null,
source: "openstreetmap-geofabrik-curated-seed",
coordinates: [
{ lat: 53.3422, lon: 7.1871 },
+3 -74
View File
@@ -6,11 +6,8 @@ import {
} from "./fairway-routing.js";
import { EMDEN_EAST_EMS_GRAPH, EMDEN_HAMM_GRAPH } from "./inland-seed.js";
import type {
DepthSample,
RouteRequest,
RouteResult,
RouteWarning,
VesselProfile
RouteResult
} from "./types.js";
const DEFAULT_CRUISE_SPEED_KN = 6;
@@ -41,66 +38,6 @@ export function buildRoute(
);
}
export function requiredDepthM(profile: VesselProfile): number {
return round(profile.draughtM + profile.safetyReserveM, 2);
}
export function assessDepthSamples(
samples: DepthSample[] | undefined,
profile: VesselProfile
): {
minKnownDepthM: number | null;
unknownDepthRatio: number;
warnings: RouteWarning[];
} {
if (!samples || samples.length === 0) {
return {
minKnownDepthM: null,
unknownDepthRatio: 1,
warnings: [
{
code: "DEPTH_UNKNOWN",
severity: "caution",
message: "Keine belastbaren Tiefendaten entlang der Route vorhanden."
}
]
};
}
const known = samples.filter((sample) => typeof sample.depthM === "number");
const unknownDepthRatio = round((samples.length - known.length) / samples.length, 2);
const minKnownDepthM =
known.length > 0 ? Math.min(...known.map((sample) => sample.depthM!)) : null;
const warnings: RouteWarning[] = [];
const required = requiredDepthM(profile);
if (unknownDepthRatio > 0) {
warnings.push({
code: "DEPTH_PARTIAL",
severity: unknownDepthRatio > 0.5 ? "caution" : "info",
message: `${Math.round(unknownDepthRatio * 100)}% der Route haben keine Tiefenprobe.`
});
}
if (minKnownDepthM !== null && minKnownDepthM < required) {
warnings.push({
code: "DEPTH_TOO_SHALLOW",
severity: "critical",
message: `Minimale bekannte Tiefe ${round(minKnownDepthM, 1)} m unterschreitet erforderliche Tiefe ${required} m.`
});
}
if (known.length === 0) {
warnings.push({
code: "NO_KNOWN_DEPTH",
severity: "caution",
message: "Alle geprüften Tiefenpunkte sind unbekannt."
});
}
return { minKnownDepthM, unknownDepthRatio, warnings };
}
export function buildManualRoute(request: RouteRequest): RouteResult {
const points = [
request.start,
@@ -116,8 +53,6 @@ export function buildManualRoute(request: RouteRequest): RouteResult {
const departureTimestamp = Number.isFinite(parsedDeparture) ? parsedDeparture : Date.now();
const durationMinutes = Math.round((distanceNm / speedKn) * 60);
const eta = new Date(departureTimestamp + durationMinutes * 60 * 1000).toISOString();
const depthAssessment = assessDepthSamples(request.depthSamples, request.vesselProfile);
return {
geometry: {
type: "LineString",
@@ -132,15 +67,9 @@ export function buildManualRoute(request: RouteRequest): RouteResult {
code: "MANUAL_ROUTE",
severity: "info",
message: "MVP-Route nutzt direkte Wegpunktsegmente ohne automatische Fahrwasserlogik."
},
...depthAssessment.warnings
],
minKnownDepthM: depthAssessment.minKnownDepthM,
unknownDepthRatio: depthAssessment.unknownDepthRatio,
dataSources: [
"user-waypoints",
request.depthSamples?.length ? "submitted-depth-samples" : "depth-unavailable"
}
],
dataSources: ["user-waypoints"],
routingMode: "manual"
};
}
-8
View File
@@ -70,8 +70,6 @@ type RouteDetails = {
departureTime?: string;
durationMinutes?: number;
warnings: RouteWarning[];
minKnownDepthM: number | null;
unknownDepthRatio: number;
dataSources: string[];
routingMode?: "manual" | "fairway";
routeSnaps?: RouteSnaps;
@@ -128,18 +126,12 @@ export type TideSummary = {
updatedAt: string;
};
export type DepthSample = {
coordinate: Coordinate;
depthM: number | null;
};
export type RouteRequest = {
start: Coordinate;
destination: Coordinate;
waypoints?: Coordinate[];
departureTime?: string;
vesselProfile: VesselProfile;
depthSamples?: DepthSample[];
};
export type AppConfig = {
@@ -268,8 +268,6 @@ describe("route guidance", () => {
distanceNm: 1,
eta: null,
warnings: [],
minKnownDepthM: null,
unknownDepthRatio: 1,
dataSources: []
};
+25 -24
View File
@@ -6,7 +6,6 @@ import {
buildRoute,
FairwayRoutingSearchLimitError,
haversineDistanceNm,
requiredDepthM,
type FairwayEdge,
type FairwayGraph
} from "../src/index.js";
@@ -14,6 +13,14 @@ import {
const EMDEN_AUSSENHAFEN = { lat: 53.344167, lon: 7.186111 };
const BORKUM_REEDE = { lat: 53.563776, lon: 6.750562 };
const HAMM_INNENSTADT_MARINA = { lat: 51.6814536, lon: 7.8042615 };
const REMOVED_AUTOMATIC_ROUTE_WARNING_CODES = new Set([
"FAIRWAY_ROUTE",
"FAIRWAY_DATA_NOT_OFFICIAL",
"DEPTH_UNKNOWN",
"DEPTH_PARTIAL",
"NO_KNOWN_DEPTH",
"DEPTH_TOO_SHALLOW"
]);
const ALTERNATIVE_GRAPH: FairwayGraph = {
id: "alternative-test",
@@ -92,7 +99,6 @@ function edge(
from,
to,
coordinates,
minDepthM: 4,
source: "synthetic-test",
...restrictions
};
@@ -119,32 +125,17 @@ function singleEdgeGraph(restrictions: Partial<FairwayEdge>): FairwayGraph {
};
}
describe("route assessment", () => {
it("marks routes with no depth data as unknown", () => {
describe("route building", () => {
it("builds manual routes without synthesizing a depth status", () => {
const result = buildManualRoute({
start: { lat: 54.18, lon: 12.08 },
destination: { lat: 54.32, lon: 12.22 },
vesselProfile: { draughtM: 1.4, safetyReserveM: 0.5 }
});
expect(result.unknownDepthRatio).toBe(1);
expect(result.warnings.some((warning) => warning.code === "DEPTH_UNKNOWN")).toBe(true);
});
it("flags known shallow samples as critical", () => {
const result = buildManualRoute({
start: { lat: 54.18, lon: 12.08 },
destination: { lat: 54.32, lon: 12.22 },
vesselProfile: { draughtM: 1.4, safetyReserveM: 0.5 },
depthSamples: [
{ coordinate: { lat: 54.2, lon: 12.1 }, depthM: 1.6 },
{ coordinate: { lat: 54.25, lon: 12.16 }, depthM: 2.4 }
]
});
expect(requiredDepthM({ draughtM: 1.4, safetyReserveM: 0.5 })).toBe(1.9);
expect(result.minKnownDepthM).toBe(1.6);
expect(result.warnings.some((warning) => warning.severity === "critical")).toBe(true);
expect(result.warnings.map((warning) => warning.code)).toEqual(["MANUAL_ROUTE"]);
expect(result).not.toHaveProperty("minKnownDepthM");
expect(result).not.toHaveProperty("unknownDepthRatio");
});
it("routes Emden Außenhafen to Borkum Reede along the Ems fairway graph", () => {
@@ -175,8 +166,14 @@ describe("route assessment", () => {
expect(result.routingMode).toBe("fairway");
expect(result.dataSources).toContain("fairway-graph:ems-borkum-seed");
expect(result.warnings.some((warning) => warning.code === "FAIRWAY_ROUTE")).toBe(true);
expect(result.warnings.some((warning) => warning.code === "MANUAL_ROUTE")).toBe(false);
expect(
result.warnings.some((warning) =>
REMOVED_AUTOMATIC_ROUTE_WARNING_CODES.has(warning.code)
)
).toBe(false);
expect(result).not.toHaveProperty("minKnownDepthM");
expect(result).not.toHaveProperty("unknownDepthRatio");
expect(coordinates.length).toBeGreaterThan(20);
expect(result.distanceNm).toBeGreaterThan(directDistanceNm * 1.2);
expect(largestSegmentNm).toBeLessThan(6);
@@ -606,7 +603,11 @@ describe("route assessment", () => {
expect(result?.dataSources).toContain("fairway-graph:emden-hamm-inland-seed");
expect(result?.dataSources).toContain("openstreetmap-geofabrik-curated-seed");
expect(result?.dataSources).toContain("openstreetmap-nominatim-curated-seed");
expect(result?.warnings.some((warning) => warning.code === "FAIRWAY_DATA_NOT_OFFICIAL")).toBe(true);
expect(
result?.warnings.some((warning) =>
REMOVED_AUTOMATIC_ROUTE_WARNING_CODES.has(warning.code)
)
).toBe(false);
});
it("uses the requested departure time as the basis for duration and ETA", () => {