Files
watermaps/packages/shared/tests/marine-poi-clustering.test.ts
T
2026-07-24 11:29:24 +02:00

149 lines
5.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
canonicalizeMarinePois,
normalizedMarineFacilityName,
type MarinePoiCandidate
} from "../src/marine-poi-clustering.js";
const lock = (
id: string,
name: string | null,
lon: number,
properties: Record<string, unknown> = {},
overrides: Partial<MarinePoiCandidate> = {}
): MarinePoiCandidate => ({
id,
layer: "locks",
source: "osm",
sourceId: id,
name,
coordinate: { lon, lat: 51.695 },
properties,
...overrides
});
describe("marine POI canonicalization", () => {
it("normalizes facility type words but retains facility ordinals", () => {
expect(normalizedMarineFacilityName("Schleuse Werries")).toBe("werries");
expect(normalizedMarineFacilityName("Schleusengebiet Werries")).toBe("werries");
expect(normalizedMarineFacilityName("Werries Lock")).toBe("werries");
expect(normalizedMarineFacilityName("W.S.V. Sixhaven")).toBe("sixhaven");
expect(normalizedMarineFacilityName("Große Kammer Schleuse Ahsen")).toBe("ahsen");
expect(normalizedMarineFacilityName("Schleuse Ahsen Kammer 2")).toBe("ahsen");
expect(normalizedMarineFacilityName("Schleuse 55")).toBe("55");
expect(normalizedMarineFacilityName("Außentor")).toBeNull();
});
it("merges matching OSM and EuRIS locks and combines their contacts", () => {
const result = canonicalizeMarinePois([
lock("w-osm", "Schleuse Werries", 7.867, {
website: "https://osm.example/werries",
phone: "+49 2381 9019290"
}),
lock(
"w-euris",
"Werries",
7.86708,
{ "ref:EU:RIS": "DEHMM00301LOCKS00404", phone: "+49 2381 9019-290", vhf: "22" },
{ source: "euris", sourceId: "DEHMM00301LOCKS00404" }
)
]);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
entityId: "marine-poi:locks:euris:DEHMM00301LOCKS00404",
canonicalSource: "euris",
memberCount: 2,
properties: {
phone: "+49 2381 9019-290",
website: "https://osm.example/werries",
vhf: "22",
source: "EuRIS + OpenStreetMap"
}
});
});
it("does not merge neighbouring facilities with different specific names", () => {
const result = canonicalizeMarinePois([
lock("a", "Wilhelminasluis", 4.724),
lock("b", "Hondsbossche Sluis", 4.72418)
]);
expect(result).toHaveLength(2);
});
it("merges duplicate facilities with the exact same name despite differing source websites", () => {
const result = canonicalizeMarinePois([
lock("relation", "Kesselschleuse Emden", 7.2, { website: "https://city.example/lock" }),
lock("way", "Kesselschleuse Emden", 7.20008, { website: "https://operator.example/lock" })
]);
expect(result).toHaveLength(1);
expect(result[0]?.properties.websites).toEqual([
"https://city.example/lock",
"https://operator.example/lock"
]);
});
it("does not merge equal names carrying conflicting official IDs", () => {
const result = canonicalizeMarinePois([
lock("a", "Schleuse 55", 7.1, { "ref:EU:RIS": "DE-55" }),
lock("b", "Schleuse 55", 7.1001, { "ref:EU:RIS": "DE-56" })
]);
expect(result).toHaveLength(2);
});
it("assigns an unnamed gate to exactly the nearest anchor without bridging facilities", () => {
const result = canonicalizeMarinePois([
lock("west", "Schleuse West", 7),
lock("east", "Schleuse Ost", 7.0018),
lock("gate", "Außentor", 7.0002, { waterway: "lock_gate" })
]);
expect(result).toHaveLength(2);
expect(result.find((poi) => poi.name === "Schleuse West")?.memberIds).toContain("gate");
expect(result.find((poi) => poi.name === "Schleuse Ost")?.memberIds).not.toContain("gate");
});
it("groups a named harbour anchor with its dock component", () => {
const result = canonicalizeMarinePois([
{
id: "marina",
layer: "harbours",
source: "osm",
sourceId: "w1",
name: "Stadthafen",
coordinate: { lon: 7.7, lat: 52 },
properties: { leisure: "marina" }
},
{
id: "dock",
layer: "harbours",
source: "osm",
sourceId: "w2",
name: "Stadthafen",
coordinate: { lon: 7.7004, lat: 52 },
properties: { waterway: "dock" }
}
]);
expect(result).toHaveLength(1);
expect(result[0]?.memberCount).toBe(2);
});
it("is independent of input order and adding a gate keeps the anchor ID stable", () => {
const anchor = lock("anchor", "Kesselschleuse Emden", 7.2, { lock: "yes" });
const gate = lock("gate", null, 7.2001, { waterway: "lock_gate" });
const forward = canonicalizeMarinePois([anchor, gate]);
const reverse = canonicalizeMarinePois([gate, anchor]);
const anchorOnly = canonicalizeMarinePois([anchor]);
expect(forward).toEqual(reverse);
expect(forward[0]?.entityId).toBe(anchorOnly[0]?.entityId);
});
it("keeps same-named facilities apart beyond the layer radius", () => {
const result = canonicalizeMarinePois([
lock("one", "Keersluis", 5),
lock("two", "Keersluis", 5.02)
]);
expect(result).toHaveLength(2);
});
});