#!/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 = ``; 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.equal(compileGeometry(model, empty, { edgeLines: false }).edgeLines.features.length, 0); 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")); const separator = geometry.laneSeparators.features[0]; const separatorOverride = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "separator-style", kind: "lane-separator-style", roadId: separator.properties.road_id, leftLaneIndex: separator.properties.left_lane_index, rightLaneIndex: separator.properties.right_lane_index, color: "yellow", pattern: "solid" }] }, model); const styledSeparators = compileGeometry(model, separatorOverride).laneSeparators.features.filter((feature) => feature.properties.road_id === separator.properties.road_id && feature.properties.left_lane_index === separator.properties.left_lane_index); assert.ok(styledSeparators.length > 0 && styledSeparators.every((feature) => feature.properties.effective_style === "yellow-solid")); assert.ok(geometry.centerLines.features.length > 0); assert.ok(geometry.centerLines.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-center-line/v1" && feature.properties.dash_length_m === 2 && feature.properties.dash_gap_m === 2)); for (const feature of geometry.centerLines.features) { const ring = feature.geometry.coordinates[0]; const lengths = [distance(ring[0], ring[1]), distance(ring[1], ring[2])].sort((a, b) => a - b); assert.ok(Math.abs(lengths[0] - .25) < .01 && Math.abs(lengths[1] - 2) < .01); assert.ok(feature.properties.segment_id && feature.properties.directional_road_ids && feature.properties.osm_way_ids && feature.properties.placement_rule); } const centerLineOverride = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "center-white-solid", kind: "center-line-style", segmentId: target.segmentId, color: "white", pattern: "solid" }] }, model); const styledCenterLines = compileGeometry(model, centerLineOverride).centerLines.features.filter((feature) => feature.properties.segment_id === target.segmentId); assert.ok(styledCenterLines.length > 0); assert.ok(styledCenterLines.every((feature) => feature.properties.color === "white" && feature.properties.pattern === "solid" && feature.properties.effective_style === "white-solid" && feature.properties.dash_gap_m === 0)); const doubleCenterOverride = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "center-double-yellow", kind: "center-line-style", segmentId: target.segmentId, color: "yellow", pattern: "solid", double: true }] }, model); const doubleCenterLines = compileGeometry(model, doubleCenterOverride).centerLines.features.filter((feature) => feature.properties.segment_id === target.segmentId); assert.equal(doubleCenterLines.length, styledCenterLines.length * 2); assert.ok(doubleCenterLines.every((feature) => feature.properties.double === true && feature.properties.effective_style === "double-yellow-solid")); assert.equal(new Set(doubleCenterLines.map((feature) => feature.properties.dash_index)).size, styledCenterLines.length); for (const dashIndex of new Set(doubleCenterLines.map((feature) => feature.properties.dash_index))) { const pair = doubleCenterLines.filter((feature) => feature.properties.dash_index === dashIndex); const centers = pair.map((feature) => feature.geometry.coordinates[0].slice(0, 4).reduce((sum, point) => [sum[0] + point[0] / 4, sum[1] + point[1] / 4], [0, 0])); assert.ok(Math.hypot((centers[0][0] - centers[1][0]) * 96400, (centers[0][1] - centers[1][1]) * 111320) > .25); } assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "bad-double-center", kind: "center-line-style", segmentId: target.segmentId, color: "white", pattern: "solid", double: true }] }, model), /Invalid center line style override/); assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "bad-center", kind: "center-line-style", segmentId: target.segmentId, color: "blue", pattern: "solid" }] }, model), /Invalid center line style override/); const edgeLine = geometry.edgeLines.features[0]; assert.ok(edgeLine && edgeLine.properties.effective_style === "white-solid"); const twoWayEdgeLines = geometry.edgeLines.features.filter((feature) => feature.properties.road_id.includes("way/10")); assert.ok(twoWayEdgeLines.length > 0 && twoWayEdgeLines.every((feature) => feature.properties.side === "right")); const edgeOverride = validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "edge-yellow-dashed", kind: "edge-line-style", roadId: edgeLine.properties.road_id, side: edgeLine.properties.side, color: "yellow", pattern: "dashed" }] }, model); const styledEdgeLines = compileGeometry(model, edgeOverride).edgeLines.features.filter((feature) => feature.properties.road_id === edgeLine.properties.road_id && feature.properties.side === edgeLine.properties.side); assert.ok(styledEdgeLines.length > 1 && styledEdgeLines.every((feature) => feature.properties.effective_style === "yellow-dashed")); assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "bad-edge", kind: "edge-line-style", roadId: edgeLine.properties.road_id, side: "middle", color: "yellow", pattern: "solid" }] }, model), /Invalid edge line style override/); assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "missing-edge-road", kind: "edge-line-style", roadId: "road:way/missing:forward", side: "left", color: "yellow", pattern: "solid" }] }, model), /Invalid edge line style override/); 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", "rounded-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 = ``; 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.crosswalks.features.every((feature) => feature.properties.junction_inset_m > 0)); assert.equal(controlGeometry.vehicleStopLines.features[0].properties.junction_inset_m, controlGeometry.crosswalks.features[0].properties.junction_inset_m); assert.ok(controlGeometry.diagnostics.some((item) => item.rule === "crossing-no-native-lane" && item.sourceIds.includes("5"))); assert.ok(controlGeometry.centerLines.features.length > 0); assert.ok(controlGeometry.centerLines.features.every((dash) => ![...controlGeometry.crosswalks.features, ...controlGeometry.vehicleStopLines.features].some((control) => ringsOverlap(dash.geometry.coordinates[0], control.geometry.coordinates[0])))); const solidControlLines = compileGeometry(compileRoadModel(controlOsm, empty), { schema: "native-road-overrides/v1", overrides: [{ id: "solid-control", kind: "center-line-style", segmentId: controlGeometry.centerLines.features[0].properties.segment_id, color: "yellow", pattern: "solid" }] }).centerLines.features; assert.ok(solidControlLines.every((dash) => ![...controlGeometry.crosswalks.features, ...controlGeometry.vehicleStopLines.features].some((control) => ringsOverlap(dash.geometry.coordinates[0], control.geometry.coordinates[0])))); const arrowControlOsm = ``; 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)); assert.equal(arrowControlGeometry.centerLines.features.length, 0); const serviceCenterLineOsm = ``; assert.equal(compileGeometry(compileRoadModel(serviceCenterLineOsm, empty)).centerLines.features.length, 0); const crossOsm = ``; const crossCenter = [114.001, 30]; const crossGeometry = compileGeometry(compileRoadModel(crossOsm, empty)); assert.equal(crossGeometry.intersectionSurface.features.length, 1); assert.equal(crossGeometry.intersectionSurface.features[0].properties.boundary_mode, "rounded-approach-envelope"); assert.ok(crossGeometry.intersectionSurface.features[0].geometry.coordinates[0].length > 9); const crossBoundary = crossGeometry.intersectionSurface.features[0].geometry.coordinates[0]; const crossRadius = (point) => Math.hypot((point[0] - crossCenter[0]) * 96400, (point[1] - crossCenter[1]) * 111320); // The sampled tangent arc must cut inward from its old straight chord; an // outward-bowed control point leaks asphalt into the pedestrian corner. const firstCurveEnd = crossBoundary[8]; assert.ok(crossRadius(crossBoundary[4]) < crossRadius([(crossBoundary[0][0] + firstCurveEnd[0]) / 2, (crossBoundary[0][1] + firstCurveEnd[1]) / 2])); 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")); // Approach asphalt ends at the shared cutback; the rounded junction surface // exclusively owns the central road area so its boundary remains visible. assert.ok(crossGeometry.roadSurface.features.every((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)); const crossSidewalkCorners = crossGeometry.sidewalkSurface.features.filter((feature) => feature.properties.kind === "corner"); assert.equal(crossSidewalkCorners.length, 4); // A rounded sidewalk corner must sample both the curb and outer boundaries. // The legacy wedge had five closing-ring points; two curved edges need more. assert.ok(crossSidewalkCorners.every((feature) => feature.geometry.coordinates[0].length > 9)); assert.ok(crossSidewalkCorners.every((feature) => { const ring = feature.geometry.coordinates[0]; const outerStart = ring[1]; const outerCurvePoint = ring[2]; const outerEnd = ring[(ring.length - 1) / 2]; const twiceArea = (outerEnd[0] - outerStart[0]) * (outerCurvePoint[1] - outerStart[1]) - (outerEnd[1] - outerStart[1]) * (outerCurvePoint[0] - outerStart[0]); return Math.abs(twiceArea) > 1e-12; })); assert.ok(crossSidewalkCorners.every((feature) => { const ring = feature.geometry.coordinates[0]; const curbStart = ring[10]; const curbCurvePoint = ring[11]; const curbEnd = ring[0]; const twiceArea = (curbEnd[0] - curbStart[0]) * (curbCurvePoint[1] - curbStart[1]) - (curbEnd[1] - curbStart[1]) * (curbCurvePoint[0] - curbStart[0]); return Math.abs(twiceArea) > 1e-12; })); const sharedInteriorNodeOsm = ``; 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); const throughConnector = sharedInteriorGeometry.connectors.features.find((feature) => feature.properties.turn === "through"); assert.ok(throughConnector, "T junction emits a through connector"); const throughCoordinates = throughConnector.geometry.coordinates; const throughStart = throughCoordinates[0]; const throughEnd = throughCoordinates.at(-1); for (const point of throughCoordinates.slice(1, -1)) { const area = Math.abs((throughEnd[0] - throughStart[0]) * (point[1] - throughStart[1]) - (throughEnd[1] - throughStart[1]) * (point[0] - throughStart[0])); assert.ok(area < 1e-12, "through connector stays on its lane-to-lane chord instead of bending through the junction node"); } assert.ok(sharedInteriorGeometry.sidewalkSurface.features.some((feature) => feature.properties.kind === "continuation" && /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 = ``; 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 = ``; 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"))); const centerLineLayer = JSON.parse(fs.readFileSync(path.join(outputRoot, "fresh", "native-road", "layers", "center_lines.geojson"), "utf8")); const edgeLineLayer = JSON.parse(fs.readFileSync(path.join(outputRoot, "fresh", "native-road", "layers", "edge_lines.geojson"), "utf8")); assert.equal(centerLineLayer.type, "FeatureCollection"); assert.equal(centerLineLayer.features.length, compiledArea.comparison.nativeCenterLineFeatures); assert.equal(edgeLineLayer.features.length, 0); 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 }); } function distance(a, b) { return Math.hypot((b[0] - a[0]) * 111320 * Math.cos(a[1] * Math.PI / 180), (b[1] - a[1]) * 111320); } function ringsOverlap(first, second) { const bounds = (ring) => [Math.min(...ring.map((point) => point[0])), Math.min(...ring.map((point) => point[1])), Math.max(...ring.map((point) => point[0])), Math.max(...ring.map((point) => point[1]))]; const a = bounds(first); const b = bounds(second); return !(a[0] > b[2] || a[2] < b[0] || a[1] > b[3] || a[3] < b[1]); } console.log("native road tests passed");