#!/usr/bin/env node "use strict"; const fs = require("fs"); const path = require("path"); const { readAreaConfig } = require("./lib/area-config"); const { compileRoadModel, compileGeometry, loadOverrides, validateOverrides, writeJsonAtomic } = require("./lib/native-road"); const repoRoot = path.resolve(__dirname, ".."); function parseArgs(argv) { const result = {}; for (let index = 0; index < argv.length; index += 1) { if (!argv[index].startsWith("--")) continue; const key = argv[index].slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); result[key] = argv[index + 1] && !argv[index + 1].startsWith("--") ? argv[++index] : "true"; } return result; } function compileArea(configPath) { const area = readAreaConfig(configPath, { repoRoot }); const overrides = loadOverrides(area.outputs.nativeRoadOverrides); const model = compileRoadModel(fs.readFileSync(area.input, "utf8"), overrides); validateOverrides(overrides, model); fs.mkdirSync(area.outputs.pipelineDir, { recursive: true }); const compiled = compileGeometry(model, overrides); const staging = fs.mkdtempSync(path.join(area.outputs.pipelineDir, "native-road-")); try { const result = { schema: "native-road-compiled/v1", areaId: area.id, source: { osm: area.input, overrides: area.outputs.nativeRoadOverrides }, model: { roads: model.roads, endpoints: model.endpoints, connections: model.connections }, movements: compiled.movements, diagnostics: compiled.diagnostics, layers: { roadSurface: "layers/road_surface.geojson", sidewalkSurface: "layers/sidewalk_surface.geojson", intersectionSurface: "layers/intersection_surface.geojson", laneCenterlines: "layers/lane_centerlines.geojson", laneSeparators: "layers/lane_separators.geojson", centerLines: "layers/center_lines.geojson", directionArrows: "layers/direction_arrows.geojson", turnArrows: "layers/turn_arrows.geojson", crosswalks: "layers/crosswalks.geojson", vehicleStopLines: "layers/vehicle_stop_lines.geojson", connectors: "layers/connectors.geojson" }, }; const comparison = compareOsm2Streets(area, result.model, compiled); writeJsonAtomic(path.join(staging, "compiled.json"), result); writeJsonAtomic(path.join(staging, "diagnostics.json"), { schema: "native-road-diagnostics/v1", diagnostics: compiled.diagnostics }); writeJsonAtomic(path.join(staging, "comparison.json"), comparison); writeJsonAtomic(path.join(staging, "layers", "road_surface.geojson"), compiled.roadSurface); writeJsonAtomic(path.join(staging, "layers", "sidewalk_surface.geojson"), compiled.sidewalkSurface); writeJsonAtomic(path.join(staging, "layers", "intersection_surface.geojson"), compiled.intersectionSurface); writeJsonAtomic(path.join(staging, "layers", "lane_centerlines.geojson"), compiled.laneCenterlines); writeJsonAtomic(path.join(staging, "layers", "lane_separators.geojson"), compiled.laneSeparators); writeJsonAtomic(path.join(staging, "layers", "center_lines.geojson"), compiled.centerLines); writeJsonAtomic(path.join(staging, "layers", "direction_arrows.geojson"), compiled.directionArrows); writeJsonAtomic(path.join(staging, "layers", "turn_arrows.geojson"), compiled.turnArrows); writeJsonAtomic(path.join(staging, "layers", "crosswalks.geojson"), compiled.crosswalks); writeJsonAtomic(path.join(staging, "layers", "vehicle_stop_lines.geojson"), compiled.vehicleStopLines); writeJsonAtomic(path.join(staging, "layers", "connectors.geojson"), compiled.connectors); fs.rmSync(area.outputs.nativeRoadDir, { recursive: true, force: true }); fs.renameSync(staging, area.outputs.nativeRoadDir); return { area, result, comparison }; } catch (error) { fs.rmSync(staging, { recursive: true, force: true }); throw error; } } function compareOsm2Streets(area, model, compiled) { const source = path.join(area.outputs.geojsonDir, "road_surface.geojson"); let featureCount = null; if (fs.existsSync(source)) { const collection = JSON.parse(fs.readFileSync(source, "utf8")); featureCount = Array.isArray(collection.features) ? collection.features.length : null; } const diagnosticsBySeverity = {}; const diagnosticsByRule = {}; for (const item of compiled.diagnostics) { diagnosticsBySeverity[item.severity] = (diagnosticsBySeverity[item.severity] || 0) + 1; diagnosticsByRule[item.rule] = (diagnosticsByRule[item.rule] || 0) + 1; } const dangling = compiled.diagnostics.filter((item) => item.rule === "unconnected-interior-road-end"); const junctions = compiled.intersectionSurface.features; const fallbackJunctions = junctions.filter((feature) => feature.properties.boundary_mode === "connector-convex-fallback"); return { schema: "native-road-comparison/v2", nativeRoadCount: model.roads.length, nativeRoadSurfaceFeatures: compiled.roadSurface.features.length, nativeSidewalkSurfaceFeatures: compiled.sidewalkSurface.features.length, nativeJunctionSurfaceFeatures: compiled.intersectionSurface.features.length, nativeApproachEnvelopeJunctions: junctions.length - fallbackJunctions.length, nativeFallbackJunctions: fallbackJunctions.length, nativeMaxJunctionExpansionRatio: junctions.reduce((maximum, feature) => Math.max(maximum, Number(feature.properties.expansion_ratio) || 0), 0), nativeLaneCenterlineFeatures: compiled.laneCenterlines.features.length, nativeLaneSeparatorFeatures: compiled.laneSeparators.features.length, nativeCenterLineFeatures: compiled.centerLines.features.length, nativeDirectionArrowFeatures: compiled.directionArrows.features.length, nativeTurnArrowFeatures: compiled.turnArrows.features.length, nativeCrosswalkFeatures: compiled.crosswalks.features.length, nativeVehicleStopLineFeatures: compiled.vehicleStopLines.features.length, nativeConnectorFeatures: compiled.connectors.features.length, nativeMovementCount: compiled.movements.length, nativePublishedMovementCount: compiled.movements.filter((movement) => movement.geometryPublished).length, nativeConnectionCount: model.connections.length, unconnectedInteriorRoadEnds: dangling.length, unconnectedEndsWithManualCandidates: dangling.filter((item) => item.manualCandidates?.length).length, diagnosticsBySeverity, diagnosticsByRule, osm2streetsRoadSurfaceFeatures: featureCount, osm2streetsAvailable: featureCount !== null, note: "Counts are coverage evidence only; geometry quality requires diagnostic and visual review.", }; } function main() { const args = parseArgs(process.argv.slice(2)); const configPath = path.resolve(args.config || path.join(repoRoot, "config", "areas", "nantaizi-lake-innovation-valley.json")); const { area, result, comparison } = compileArea(configPath); console.log(`NATIVE_ROAD_COMPILE_DONE ${JSON.stringify({ areaId: area.id, roads: result.model.roads.length, endpoints: result.model.endpoints.length, diagnostics: result.diagnostics.length, output: area.outputs.nativeRoadDir, comparison })}`); } if (require.main === module) main(); module.exports = { compileArea, parseArgs };