#!/usr/bin/env node "use strict"; const fs = require("fs"); const http = require("http"); const path = require("path"); const { readAreaConfig } = require("./lib/area-config"); const { loadOverrides, validateOverrides, writeJsonAtomic } = require("./lib/native-road"); const { compileArea, parseArgs } = require("./compile-native-roads"); const repoRoot = path.resolve(__dirname, ".."); 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")); if (args.noCompile !== "true") compileArea(configPath); const area = readAreaConfig(configPath, { repoRoot }); const port = Number(args.port || 8787); if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("--port must be an integer in [1024, 65535]."); const server = http.createServer((request, response) => handle(request, response, area, configPath)); server.on("error", (error) => { console.error(`Road Workbench failed to listen: ${error.message}`); process.exitCode = 1; }); server.listen(port, "127.0.0.1", () => console.log(`Road Workbench: http://127.0.0.1:${port}/`)); } function handle(request, response, area, configPath) { const url = new URL(request.url, "http://127.0.0.1"); if (request.method === "GET" && url.pathname === "/") return sendFile(response, path.join(repoRoot, "scripts", "workbench", "index.html"), "text/html; charset=utf-8"); if (request.method === "GET" && url.pathname === "/app.js") return sendFile(response, path.join(repoRoot, "scripts", "workbench", "app.js"), "text/javascript; charset=utf-8"); if (request.method === "GET" && url.pathname === "/app.css") return sendFile(response, path.join(repoRoot, "scripts", "workbench", "app.css"), "text/css; charset=utf-8"); if (request.method === "GET" && url.pathname === "/api/state") return sendJson(response, 200, state(area)); if (request.method === "POST" && url.pathname === "/api/overrides") return readBody(request).then((body) => { const compiled = readCompiled(area); const overrides = validateOverrides(body, { roads: compiled.model.roads, endpoints: compiled.model.endpoints }); writeJsonAtomic(area.outputs.nativeRoadOverrides, overrides); sendJson(response, 200, { ok: true, overrides }); }).catch((error) => sendJson(response, 400, { ok: false, error: error.message })); if (request.method === "POST" && url.pathname === "/api/compile") return Promise.resolve().then(() => { compileArea(configPath); sendJson(response, 200, state(area)); }).catch((error) => sendJson(response, 500, { ok: false, error: error.message })); sendJson(response, 404, { error: "Not found" }); } function state(area) { const nativeDir = area.outputs.nativeRoadDir; const osm2streetsRoadSurface = path.join(area.outputs.geojsonDir, "road_surface.geojson"); return { areaId: area.id, compiled: readCompiled(area), overrides: loadOverrides(area.outputs.nativeRoadOverrides), comparison: readJson(path.join(nativeDir, "comparison.json")), layers: { nativeRoadSurface: readLayer(path.join(nativeDir, "layers", "road_surface.geojson")), nativeIntersectionSurface: readLayer(path.join(nativeDir, "layers", "intersection_surface.geojson")), laneCenterlines: readLayer(path.join(nativeDir, "layers", "lane_centerlines.geojson")), connectors: readLayer(path.join(nativeDir, "layers", "connectors.geojson")), osm2streetsRoadSurface: fs.existsSync(osm2streetsRoadSurface) ? readLayer(osm2streetsRoadSurface) : null } }; } function readCompiled(area) { return readJson(path.join(area.outputs.nativeRoadDir, "compiled.json")); } function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } function readLayer(file) { return fs.existsSync(file) ? readJson(file) : { type: "FeatureCollection", features: [] }; } function readBody(request) { return new Promise((resolve, reject) => { let body = ""; request.setEncoding("utf8"); request.on("data", (part) => { body += part; if (body.length > 1024 * 1024) request.destroy(); }); request.on("end", () => { try { resolve(JSON.parse(body)); } catch (_) { reject(new Error("Request body must be JSON.")); } }); request.on("error", reject); }); } function sendFile(response, file, type) { response.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" }); fs.createReadStream(file).pipe(response); } function sendJson(response, status, value) { response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }); response.end(`${JSON.stringify(value)}\n`); } if (require.main === module) main();