import { describe, expect, it } from "vitest"; import { buildRoute } from "@watermaps/shared"; import { fairwayRowsToGraph, mergeConnectedFairwayGraphs, overpassToGraph } from "../src/services/fairways.js"; describe("fairway graph extraction", () => { it("builds a routable graph from PostGIS fairway rows", () => { const graph = fairwayRowsToGraph( [ { id: "1", source: "osm", source_id: "way-1", name: "Harbour Reach", min_depth_m: "4.2", geometry: { type: "LineString", coordinates: [ [10, 54], [10.04, 54.02] ] } }, { id: "2", source: "osm", source_id: "way-2", name: "Outer Reach", min_depth_m: 4.2, geometry: { type: "LineString", coordinates: [ [10.04, 54.02], [10.1, 54.04] ] } } ], [9.9, 53.9, 10.2, 54.1] ); expect(graph).not.toBeNull(); const route = buildRoute( { start: { lat: 54, lon: 10 }, destination: { lat: 54.04, lon: 10.1 }, vesselProfile: { draughtM: 1.2, safetyReserveM: 0.4 } }, graph ?? undefined ); expect(route?.routingMode).toBe("fairway"); expect(route?.dataSources).toContain("fairway-graph:postgis-9.900-53.900-10.200-54.100"); expect(route?.dataSources).toContain("postgis-osm"); }); it("routes the iPhone Emden coordinates through intermediate fairway vertices", () => { const start = { lat: 53.3306, lon: 7.1752 }; const destination = { lat: 53.6741, lon: 7.1474 }; const graph = fairwayRowsToGraph( [ { id: "emden-main-reach", source: "osm", source_id: "way-main", name: "Ems Fahrwasser", min_depth_m: null, geometry: { type: "LineString", coordinates: [ [7.1751368, 53.3331995], [7.16, 53.42], [7.1474, 53.55], [7.18, 53.61] ] } }, { id: "busetief-branch", source: "osm", source_id: "way-branch", name: "Busetief", min_depth_m: null, geometry: { type: "LineString", coordinates: [ [7.1474, 53.55], [7.1414995, 53.6668156] ] } } ], [6.9974, 53.1806, 7.3252, 53.8241] ); expect(graph).not.toBeNull(); const route = buildRoute( { start, destination, vesselProfile: { draughtM: 1.4, safetyReserveM: 0.5, cruiseSpeedKn: 12 } }, graph ?? undefined ); expect(route).not.toBeNull(); expect(route?.routingMode).toBe("fairway"); expect(route?.dataSources).toContain("postgis-osm"); expect(route?.geometry.coordinates[0]).toEqual([start.lon, start.lat]); expect(route?.geometry.coordinates.at(-1)).toEqual([destination.lon, destination.lat]); expect(route?.geometry.coordinates.some(([lon, lat]) => lon === 7.1474 && lat === 53.55)).toBe(true); }); it("builds a graph from OSM/OpenSeaMap Overpass ways", () => { const graph = overpassToGraph( { elements: [ { type: "way", id: 123, tags: { "seamark:type": "navigation_line", "seamark:navigation_line:minimum_depth": "3.5" }, geometry: [ { lat: 54, lon: 10 }, { lat: 54.02, lon: 10.05 } ] } ] }, [9.9, 53.9, 10.1, 54.1] ); 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", () => { const graph = overpassToGraph( { elements: [ { type: "way", id: 201, tags: { waterway: "canal", boat: "yes", name: "Datteln-Hamm-Kanal" }, geometry: [ { lat: 51.65, lon: 7.35 }, { lat: 51.66, lon: 7.4 } ] }, { type: "way", id: 202, tags: { waterway: "canal", boat: "no", name: "Gesperrter Kanal" }, geometry: [ { lat: 51.66, lon: 7.4 }, { lat: 51.67, lon: 7.45 } ] } ] }, [7.3, 51.6, 7.5, 51.7] ); expect(graph).not.toBeNull(); expect(graph?.edges).toHaveLength(1); expect(graph?.edges[0]?.name).toBe("Datteln-Hamm-Kanal"); }); it("topologically joins graph fragments from PostGIS and live data", () => { const first = fairwayRowsToGraph( [ { id: "north", source: "osm", source_id: "north", name: "Dortmund-Ems-Kanal", min_depth_m: null, geometry: { type: "LineString", coordinates: [ [7.3, 52.1], [7.35, 52] ] } } ], [7.2, 51.8, 7.6, 52.2] ); const second = overpassToGraph( { elements: [ { type: "way", id: 301, tags: { waterway: "canal", boat: "yes", name: "Datteln-Hamm-Kanal" }, geometry: [ { lat: 52, lon: 7.35 }, { lat: 51.9, lon: 7.5 } ] } ] }, [7.2, 51.8, 7.6, 52.2] ); const graph = mergeConnectedFairwayGraphs([first!, second!]); const route = buildRoute( { start: { lat: 52.1, lon: 7.3 }, destination: { lat: 51.9, lon: 7.5 }, vesselProfile: { draughtM: 1.2, safetyReserveM: 0.3 } }, graph ?? undefined ); expect(route).not.toBeNull(); expect(route?.dataSources).toContain("postgis-osm"); expect(route?.dataSources).toContain("osm-overpass-waterway-canal"); }); it("carries OSM vessel restrictions into the routing graph", () => { const graph = fairwayRowsToGraph( [ { id: "restricted", 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", coordinates: [ [7, 52], [7.04, 52] ] } } ], [6.9, 51.9, 7.1, 52.1] ); expect(graph?.edges[0]).toMatchObject({ maxAirDraftM: 2.4, maxBeamM: 3.2, maxDraughtM: 1.8, oneway: true }); expect( buildRoute( { start: { lat: 52, lon: 7 }, destination: { lat: 52, lon: 7.04 }, vesselProfile: { draughtM: 1.4, safetyReserveM: 0.3, airDraftM: 2.5, beamM: 3 } }, graph ?? undefined ) ).toBeNull(); }); });