351 lines
9.5 KiB
TypeScript
351 lines
9.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { buildRoute } from "@watermaps/shared";
|
||
import { createCache } from "../src/services/cache.js";
|
||
import {
|
||
FairwayService,
|
||
fairwayRowsToGraph,
|
||
localFairwaysToGraph,
|
||
mergeConnectedFairwayGraphs,
|
||
overpassToGraph
|
||
} from "../src/services/fairways.js";
|
||
|
||
describe("fairway graph extraction", () => {
|
||
it("builds a routable graph from the persistent local Geofabrik format", () => {
|
||
const graph = localFairwaysToGraph(
|
||
[
|
||
{
|
||
id: "local-1",
|
||
bbox: [7.15, 53.62, 7.17, 53.71],
|
||
tags: { route: "ferry", name: "Norddeich–Norderney" },
|
||
coordinates: [
|
||
[53.6234, 7.1559],
|
||
[53.66, 7.16],
|
||
[53.7023, 7.1658]
|
||
]
|
||
}
|
||
],
|
||
[7.0, 53.47, 7.32, 53.85],
|
||
"germany-test.osm.pbf"
|
||
);
|
||
const route = buildRoute(
|
||
{
|
||
start: { lat: 53.6234, lon: 7.1559 },
|
||
destination: { lat: 53.7023, lon: 7.1658 },
|
||
vesselProfile: { draughtM: 1.4, safetyReserveM: 0.5, cruiseSpeedKn: 6 }
|
||
},
|
||
graph ?? undefined
|
||
);
|
||
|
||
expect(route?.routingMode).toBe("fairway");
|
||
expect(route?.dataSources).toContain("local-geofabrik-germany-test.osm.pbf");
|
||
});
|
||
|
||
it("allows country-wide requests against the persistent local index", async () => {
|
||
const temporaryDirectory = await mkdtemp(join(tmpdir(), "watermaps-local-span-"));
|
||
const localDataPath = join(temporaryDirectory, "germany-netherlands-fairways.json");
|
||
const request = {
|
||
start: { lat: 52, lon: 4 },
|
||
destination: { lat: 52, lon: 12 },
|
||
vesselProfile: { draughtM: 1, safetyReserveM: 0.3 }
|
||
};
|
||
|
||
try {
|
||
await writeFile(
|
||
localDataPath,
|
||
JSON.stringify({
|
||
version: 1,
|
||
source: "germany+netherlands",
|
||
ways: [
|
||
{
|
||
id: "country-wide-test",
|
||
bbox: [4, 52, 12, 52],
|
||
tags: { waterway: "canal", name: "Country-wide test fairway" },
|
||
coordinates: [
|
||
[52, 4],
|
||
[52, 12]
|
||
]
|
||
}
|
||
]
|
||
})
|
||
);
|
||
|
||
const service = new FairwayService({
|
||
cache: createCache(),
|
||
fetcher: fetch,
|
||
liveEnabled: false,
|
||
localDataPath
|
||
});
|
||
const graphs = await service.getGraphsForRoute(request);
|
||
const route = buildRoute(request, graphs[0]);
|
||
|
||
expect(graphs).toHaveLength(1);
|
||
expect(route?.routingMode).toBe("fairway");
|
||
expect(route?.dataSources).toContain(
|
||
"local-geofabrik-germany+netherlands"
|
||
);
|
||
await service.close();
|
||
} finally {
|
||
await rm(temporaryDirectory, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
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();
|
||
});
|
||
});
|