127 lines
15 KiB
JavaScript
127 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { compileRoadModel, compileGeometry, validateOverrides } = require("./lib/native-road");
|
|
const { compileArea } = require("./compile-native-roads");
|
|
const { checkArea } = require("./check-native-roads");
|
|
|
|
const osm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.001" lat="30"/><node id="3" lon="114.001" lat="30.001"/><way id="10"><nd ref="1"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="lanes" v="2"/><tag k="sidewalk" v="both"/></way><way id="11"><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/><tag k="oneway" v="yes"/></way></osm>`;
|
|
const empty = { schema: "native-road-overrides/v1", overrides: [] };
|
|
const initial = compileRoadModel(osm, empty);
|
|
assert.equal(initial.roads.length, 3);
|
|
const target = initial.roads.find((road) => road.id === "road:way/10:forward");
|
|
const overrides = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "road-width", kind: "road", roadId: target.id, widthMeters: 9, laneCount: 2, sidewalkLeft: false }] }, initial);
|
|
const model = compileRoadModel(osm, overrides);
|
|
const edited = model.roads.find((road) => road.id === target.id);
|
|
assert.equal(edited.widthMeters, 9);
|
|
assert.equal(edited.provenance.widthMeters, "override:road-width");
|
|
assert.equal(edited.sidewalkLeft, false);
|
|
const geometry = compileGeometry(model);
|
|
assert.equal(geometry.roadSurface.features.length, 2);
|
|
assert.ok(geometry.roadSurface.features.every((feature) => feature.geometry.coordinates[0].length >= 5));
|
|
assert.equal(geometry.sidewalkSurface.features.length, 2);
|
|
assert.ok(geometry.sidewalkSurface.features.every((feature) => feature.geometry.coordinates[0].length >= 5));
|
|
assert.equal(geometry.laneCenterlines.features.length, model.roads.reduce((sum, road) => sum + road.laneCount, 0));
|
|
assert.ok(geometry.laneSeparators.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-lane-separator/v1"));
|
|
assert.ok(geometry.directionArrows.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-direction-arrow/v1" && feature.properties.placement_interval_meters === 32));
|
|
assert.ok(geometry.turnArrows.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-turn-arrow/v1"));
|
|
assert.ok(geometry.connectors.features.length > 0);
|
|
assert.ok(geometry.connectors.features.every((feature) => feature.geometry.coordinates.length === 13));
|
|
assert.ok(geometry.connectors.features.every((feature) => feature.properties.node_id));
|
|
assert.ok(geometry.movements.length >= geometry.connectors.features.length);
|
|
assert.ok(geometry.movements.every((movement) => movement.id.startsWith("movement:") && movement.connectorId.startsWith("connector:")));
|
|
assert.ok(geometry.movements.every((movement) => ["connector", "continuous", "deferred-too-long"].includes(movement.geometryStatus)));
|
|
assert.ok(geometry.intersectionSurface.features.every((feature) => feature.properties.rule === "junction-shared-cutback/v3"));
|
|
assert.ok(geometry.intersectionSurface.features.every((feature) => ["approach-envelope", "connector-convex-fallback"].includes(feature.properties.boundary_mode)));
|
|
assert.ok(geometry.intersectionSurface.features.every((feature) => feature.properties.approach_area_m2 > 0 && feature.properties.surface_area_m2 > 0 && feature.properties.expansion_ratio >= 1));
|
|
for (const feature of geometry.intersectionSurface.features.filter((item) => item.properties.boundary_mode === "connector-convex-fallback")) assert.ok(geometry.diagnostics.some((item) => item.subjectId === feature.properties.native_id && item.rule === "junction-connector-envelope-fallback"));
|
|
const controlOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.00080" lat="30"><tag k="highway" v="crossing"/><tag k="crossing:markings" v="zebra"/></node><node id="3" lon="114.001" lat="30"/><node id="4" lon="114.002" lat="30"><tag k="highway" v="crossing"/><tag k="crossing:markings" v="unmarked"/></node><node id="5" lon="114.0035" lat="30"><tag k="highway" v="crossing"/></node><node id="6" lon="114.004" lat="30"/><node id="7" lon="114.001" lat="30.001"/><way id="60"><nd ref="1"/><nd ref="2"/><nd ref="3"/><nd ref="4"/><tag k="highway" v="residential"/></way><way id="61"><nd ref="5"/><nd ref="6"/><tag k="highway" v="footway"/></way><way id="62"><nd ref="3"/><nd ref="7"/><tag k="highway" v="residential"/></way></osm>`;
|
|
const controlGeometry = compileGeometry(compileRoadModel(controlOsm, empty));
|
|
assert.equal(controlGeometry.crosswalks.features.length, 6);
|
|
assert.equal(controlGeometry.vehicleStopLines.features.length, 1);
|
|
assert.ok(controlGeometry.crosswalks.features.every((feature) => feature.properties.crossing_node_id === "2" && feature.properties.provenance === "native-road-crosswalk/v1"));
|
|
assert.ok(controlGeometry.vehicleStopLines.features.every((feature) => feature.properties.crossing_node_id === "2" && feature.properties.provenance === "native-road-stop-line/v1"));
|
|
assert.ok(controlGeometry.diagnostics.some((item) => item.rule === "crossing-no-native-lane" && item.sourceIds.includes("5")));
|
|
const arrowControlOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.00095" lat="30"><tag k="highway" v="crossing"/><tag k="crossing:markings" v="zebra"/></node><node id="3" lon="114.001" lat="30"/><way id="63"><nd ref="1"/><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/><tag k="oneway" v="yes"/><tag k="lanes" v="1"/><tag k="turn:lanes" v="through"/></way></osm>`;
|
|
const arrowControlGeometry = compileGeometry(compileRoadModel(arrowControlOsm, empty));
|
|
assert.ok(arrowControlGeometry.turnArrows.features.length > 0);
|
|
assert.ok(arrowControlGeometry.turnArrows.features.every((feature) => feature.properties.placement_distance_meters > 6));
|
|
const crossOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.001" lat="30"/><node id="3" lon="114.002" lat="30"/><node id="4" lon="114.001" lat="30.001"/><node id="5" lon="114.001" lat="29.999"/><way id="40"><nd ref="1"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="41"><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="42"><nd ref="5"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="43"><nd ref="2"/><nd ref="4"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way></osm>`;
|
|
const crossCenter = [114.001, 30];
|
|
const crossGeometry = compileGeometry(compileRoadModel(crossOsm, empty));
|
|
assert.equal(crossGeometry.intersectionSurface.features.length, 1);
|
|
assert.equal(crossGeometry.turnArrows.features.length, 0);
|
|
assert.ok(crossGeometry.directionArrows.features.length > 0);
|
|
assert.ok(crossGeometry.directionArrows.features.every((feature) => feature.properties.maneuver === "through" && feature.properties.provenance === "native-road-direction-arrow/v1"));
|
|
assert.ok(crossGeometry.roadSurface.features.some((feature) => Math.min(...feature.geometry.coordinates[0].map((point) => Math.hypot((point[0] - crossCenter[0]) * 96400, (point[1] - crossCenter[1]) * 111320))) < 4));
|
|
const exteriorRings = (geometry) => geometry.type === "Polygon" ? [geometry.coordinates[0]] : geometry.coordinates.map((polygon) => polygon[0]);
|
|
assert.ok(crossGeometry.sidewalkSurface.features.every((feature) => Math.min(...exteriorRings(feature.geometry).flat().map((point) => Math.hypot((point[0] - crossCenter[0]) * 96400, (point[1] - crossCenter[1]) * 111320))) > 5));
|
|
assert.equal(crossGeometry.sidewalkSurface.features.filter((feature) => feature.properties.kind === "corner").length, 4);
|
|
const sharedInteriorNodeOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.001" lat="30"/><node id="3" lon="114.002" lat="30"/><node id="4" lon="114.001" lat="30.001"/><way id="50"><nd ref="1"/><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="51"><nd ref="4"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way></osm>`;
|
|
const sharedInteriorModel = compileRoadModel(sharedInteriorNodeOsm, empty);
|
|
assert.equal(sharedInteriorModel.roads.length, 6);
|
|
assert.ok(sharedInteriorModel.roads.some((road) => road.id === "road:way/50:segment/1:forward"));
|
|
assert.ok(sharedInteriorModel.roads.some((road) => road.id === "road:way/50:segment/2:forward"));
|
|
const sharedInteriorGeometry = compileGeometry(sharedInteriorModel);
|
|
assert.equal(sharedInteriorGeometry.intersectionSurface.features.length, 1);
|
|
assert.equal(sharedInteriorGeometry.intersectionSurface.features[0].properties.osm_node_id, "2");
|
|
assert.equal(sharedInteriorGeometry.intersectionSurface.features[0].properties.kind, "t");
|
|
assert.ok(sharedInteriorGeometry.connectors.features.length >= 4);
|
|
assert.ok(sharedInteriorGeometry.sidewalkSurface.features.some((feature) => feature.properties.kind === "corner" && /segment:way\/50\/1:.*->segment:way\/50\/2:/.test(feature.properties.native_id)));
|
|
const connection = initial.connections[0];
|
|
assert.ok(initial.connections.every((item) => item.fromEndpointId.endsWith(":end") && item.toEndpointId.endsWith(":start")));
|
|
assert.equal(initial.connections.length, new Set(initial.connections.map((item) => `${item.fromEndpointId}->${item.toEndpointId}`)).size);
|
|
const connectionOverrides = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "disconnect", kind: "junction-connection", fromEndpointId: connection.fromEndpointId, toEndpointId: connection.toEndpointId, enabled: false }] }, initial);
|
|
assert.equal(validateOverrides(connectionOverrides).overrides.length, 1);
|
|
assert.equal(compileRoadModel(osm, connectionOverrides).connections.find((item) => item.id === connection.id).enabled, false);
|
|
assert.ok(compileGeometry(compileRoadModel(osm, connectionOverrides)).connectors.features.length < geometry.connectors.features.length);
|
|
const disconnectedOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.001" lat="30"/><node id="3" lon="114.00105" lat="30"/><node id="4" lon="114.002" lat="30"/><way id="20"><nd ref="1"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="oneway" v="yes"/></way><way id="21"><nd ref="3"/><nd ref="4"/><tag k="highway" v="residential"/><tag k="oneway" v="yes"/></way></osm>`;
|
|
const disconnected = compileRoadModel(disconnectedOsm, empty);
|
|
const from = disconnected.endpoints.find((endpoint) => endpoint.roadId === "road:way/20:forward" && endpoint.side === "end");
|
|
const to = disconnected.endpoints.find((endpoint) => endpoint.roadId === "road:way/21:forward" && endpoint.side === "start");
|
|
const manualOverrides = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "manual", kind: "junction-connection", fromEndpointId: from.id, toEndpointId: to.id, enabled: true }] }, disconnected);
|
|
assert.ok(compileRoadModel(disconnectedOsm, manualOverrides).connections.some((item) => item.fromEndpointId === from.id && item.toEndpointId === to.id));
|
|
assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "same-way", kind: "junction-connection", fromEndpointId: initial.endpoints.find((endpoint) => endpoint.roadId === "road:way/10:forward" && endpoint.side === "end").id, toEndpointId: initial.endpoints.find((endpoint) => endpoint.roadId === "road:way/10:backward" && endpoint.side === "start").id, enabled: true }] }, initial), /manual junction connection/);
|
|
const turnOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.001" lat="30"/><node id="3" lon="114.001" lat="30.001"/><way id="30"><nd ref="1"/><nd ref="2"/><tag k="highway" v="primary"/><tag k="oneway" v="yes"/><tag k="lanes" v="3"/><tag k="turn:lanes" v="left|through|right"/></way><way id="31"><nd ref="2"/><nd ref="3"/><tag k="highway" v="primary"/><tag k="oneway" v="yes"/><tag k="lanes" v="3"/></way></osm>`;
|
|
const turnModel = compileRoadModel(turnOsm, empty);
|
|
const turnGeometry = compileGeometry(turnModel);
|
|
assert.equal(turnGeometry.sidewalkSurface.features.length, 0);
|
|
assert.ok(turnGeometry.directionArrows.features.length > 0);
|
|
assert.ok(turnGeometry.turnArrows.features.length > 0);
|
|
assert.ok(turnGeometry.turnArrows.features.every((feature) => feature.properties.provenance === "native-road-turn-arrow/v1"));
|
|
const sidewalkOverride = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "add-sidewalk", kind: "road", roadId: "road:way/30:forward", sidewalkLeft: true }] }, turnModel);
|
|
assert.ok(compileGeometry(compileRoadModel(turnOsm, sidewalkOverride)).sidewalkSurface.features.some((feature) => feature.properties.native_id === "sidewalk:way/30:left"));
|
|
assert.equal(turnGeometry.connectors.features.length, 1);
|
|
assert.match(turnGeometry.connectors.features[0].properties.from_lane_id, /road:way\/30:forward:1$/);
|
|
assert.match(turnGeometry.connectors.features[0].properties.to_lane_id, /road:way\/31:forward:1$/);
|
|
const laneOverrides = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "block-left", kind: "lane-connection", fromLaneId: turnGeometry.connectors.features[0].properties.from_lane_id, toLaneId: turnGeometry.connectors.features[0].properties.to_lane_id, enabled: false }] }, turnModel);
|
|
assert.equal(compileGeometry(turnModel, laneOverrides).connectors.features.length, 0);
|
|
assert.equal(compileGeometry(turnModel, laneOverrides).movements.length, 0);
|
|
assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "bad", kind: "road", roadId: "missing", widthMeters: 4 }] }, initial), /Unknown road/);
|
|
const freshArea = fs.mkdtempSync(path.join(os.tmpdir(), "native-road-fresh-area-"));
|
|
try {
|
|
const input = path.join(freshArea, "input.osm");
|
|
const outputRoot = path.join(freshArea, "outputs");
|
|
const config = path.join(freshArea, "area.json");
|
|
fs.writeFileSync(input, osm);
|
|
fs.writeFileSync(config, JSON.stringify({ id: "fresh", input, outputRoot }));
|
|
const compiledArea = compileArea(config);
|
|
assert.equal(compiledArea.result.areaId, "fresh");
|
|
assert.ok(fs.existsSync(path.join(outputRoot, "fresh", "native-road", "compiled.json")));
|
|
assert.equal(compiledArea.comparison.schema, "native-road-comparison/v2");
|
|
assert.equal(compiledArea.comparison.nativeRoadCount, compiledArea.result.model.roads.length);
|
|
assert.equal(compiledArea.comparison.nativePublishedMovementCount, compiledArea.result.movements.filter((movement) => movement.geometryPublished).length);
|
|
assert.equal(compiledArea.comparison.nativeCrosswalkFeatures, 0);
|
|
assert.equal(compiledArea.comparison.nativeVehicleStopLineFeatures, 0);
|
|
assert.equal(compiledArea.comparison.nativeApproachEnvelopeJunctions + compiledArea.comparison.nativeFallbackJunctions, compiledArea.comparison.nativeJunctionSurfaceFeatures);
|
|
assert.ok(compiledArea.comparison.nativeMaxJunctionExpansionRatio >= 0);
|
|
assert.equal(checkArea(config).ok, true);
|
|
} finally {
|
|
fs.rmSync(freshArea, { recursive: true, force: true });
|
|
}
|
|
console.log("native road tests passed");
|