- 高德 GeoJSON 参考流程: `scripts/lib/gaode-junction-reference.js` 与 `scripts/inspect-junction-reference.js` 将 GCJ-02 参考转换为 WGS84, 按 node id/最近距离关联 OSM, 支持普通路口面和 `complex-cluster` 两种匹配。 - 复合路口模板 `complex-junction-v1`: `scripts/lib/complex-junction.js` 用参考 几何校准 core 半径, 生成路口面、进口路面、斑马线、停止线、角部圆角与安全岛; 拓扑/信号/连接全部沿用 OSM/native。 - 车道中心线控制要素避让: `compileLaneCenterlines` 现接收模板已产出的斑马线/停止线, 新增 `trimLaneOutsideControls` 按到路口中心的半径定向裁剪; 标线源几何同步裁剪, 不再 越过斑马线继续画到核心区。拓扑几何不变, connector 集合前后一致。 - 复合路口人行道转角: `buildComplexJunctionGeometry` 沿已定义的路缘生成 2m 宽转角带, 复用圆角曲线, 通过 `islands` 通道并入 `sidewalk_surface`; 自交或坐标非有限时报 `complex-junction-sidewalk-corner-fallback` 并跳过。 - 新增诊断: `complex-junction-configured-radius-ignored`、 `lane-centerline-fully-inside-control`、`complex-junction-sidewalk-corner-fallback`。 - 死码清理: 移除未被调用的 `clusterApproachRing`。 - spec 更新: `.trellis/spec/pipeline/cli-and-stages.md` 复合路口小节补充控制要素 避让顺序、人行道转角契约、Validation 矩阵三行; 索引新增导航。 - 任务产物 `08-19-gaode-junction-reference`: 8 条验收标准全部实测记录, Scope Drift / Verification Log / Known Gaps 三节沉淀本次工作。 Regression: test:native-road / test:road-workbench / test:preflight / test:native-preview-traffic / test:package-contract / test:traffic-signals / test:gaode-junction-reference 全绿; road:check ok=true, errors=[]。
107 lines
8.9 KiB
JavaScript
107 lines
8.9 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const http = require("http");
|
|
const path = require("path");
|
|
const { execFileSync } = require("child_process");
|
|
const { readAreaConfig } = require("./lib/area-config");
|
|
const { loadOverrides, validateOverrides, writeJsonAtomic } = require("./lib/native-road");
|
|
const { generate, validateDocument, runtime } = require("./lib/native-traffic-signals");
|
|
const { compileArea, parseArgs } = require("./compile-native-roads");
|
|
const { convertGeoJson } = require("./lib/gaode-junction-reference");
|
|
|
|
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 junctionReference = args.junctionReference ? readJunctionReference(path.resolve(args.junctionReference)) : null;
|
|
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, junctionReference));
|
|
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, junctionReference) {
|
|
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.startsWith("/vendor/")) return sendVendorFile(response, url.pathname);
|
|
if (request.method === "GET" && url.pathname === "/api/state") return sendJson(response, 200, state(area, junctionReference));
|
|
if (request.method === "POST" && url.pathname === "/api/traffic-signals") return readBody(request).then((body) => {
|
|
const document = validateDocument(body, fs.readFileSync(area.input, "utf8"));
|
|
writeJsonAtomic(area.outputs.nativeTrafficSignals, document);
|
|
sendJson(response, 200, { ok: true, trafficSignals: document, runtime: runtime(document) });
|
|
}).catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
|
|
if (request.method === "POST" && url.pathname === "/api/traffic-signals/generate") return Promise.resolve().then(() => {
|
|
const compiled = readCompiled(area);
|
|
const generated = generate(fs.readFileSync(area.input, "utf8"), readLayer(path.join(area.outputs.nativeRoadDir, "layers", "vehicle_stop_lines.geojson")), readLayer(path.join(area.outputs.nativeRoadDir, "layers", "intersection_surface.geojson")));
|
|
const current = validateDocument(readJson(area.outputs.nativeTrafficSignals), fs.readFileSync(area.input, "utf8"));
|
|
const present = new Set(current.assemblies.features.map((feature) => feature.properties.signal_uid));
|
|
current.assemblies.features.push(...generated.assemblies.features.filter((feature) => !present.has(feature.properties.signal_uid)));
|
|
writeJsonAtomic(area.outputs.nativeTrafficSignals, current);
|
|
sendJson(response, 200, { ok: true, trafficSignals: current, runtime: runtime(current), generated: generated.assemblies.features.length, compiled: Boolean(compiled) });
|
|
}).catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
|
|
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(() => {
|
|
compileFresh(configPath);
|
|
sendJson(response, 200, state(area, junctionReference));
|
|
}).catch((error) => sendJson(response, 500, { ok: false, error: error.message }));
|
|
sendJson(response, 404, { error: "Not found" });
|
|
}
|
|
|
|
function state(area, junctionReference = null) {
|
|
const nativeDir = area.outputs.nativeRoadDir;
|
|
const osm2streetsRoadSurface = path.join(area.outputs.geojsonDir, "road_surface.geojson");
|
|
const trafficSignals = fs.existsSync(area.outputs.nativeTrafficSignals)
|
|
? validateDocument(readJson(area.outputs.nativeTrafficSignals), fs.readFileSync(area.input, "utf8"))
|
|
: { schema: "native-traffic-signals/v1", provenance: "empty", assemblies: { type: "FeatureCollection", features: [] } };
|
|
const trafficRuntime = runtime(trafficSignals);
|
|
return { areaId: area.id, compiled: readCompiled(area), overrides: loadOverrides(area.outputs.nativeRoadOverrides), trafficSignals, trafficRuntime, comparison: readJson(path.join(nativeDir, "comparison.json")), junctionReference, layers: { nativeRoadSurface: readLayer(path.join(nativeDir, "layers", "road_surface.geojson")), edgeLines: readLayer(path.join(nativeDir, "layers", "edge_lines.geojson")), nativeSidewalkSurface: readLayer(path.join(nativeDir, "layers", "sidewalk_surface.geojson")), nativeIntersectionSurface: readLayer(path.join(nativeDir, "layers", "intersection_surface.geojson")), laneCenterlines: readLayer(path.join(nativeDir, "layers", "lane_centerlines.geojson")), laneSeparators: readLayer(path.join(nativeDir, "layers", "lane_separators.geojson")), centerLines: readLayer(path.join(nativeDir, "layers", "center_lines.geojson")), directionArrows: readLayer(path.join(nativeDir, "layers", "direction_arrows.geojson")), turnArrows: readLayer(path.join(nativeDir, "layers", "turn_arrows.geojson")), crosswalks: readLayer(path.join(nativeDir, "layers", "crosswalks.geojson")), vehicleStopLines: readLayer(path.join(nativeDir, "layers", "vehicle_stop_lines.geojson")), connectors: readLayer(path.join(nativeDir, "layers", "connectors.geojson")), osm2streetsRoadSurface: fs.existsSync(osm2streetsRoadSurface) ? readLayer(osm2streetsRoadSurface) : null } };
|
|
}
|
|
function compileFresh(configPath) {
|
|
try {
|
|
return execFileSync(process.execPath, [path.join(repoRoot, "scripts", "compile-native-roads.js"), "--config", configPath], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
} catch (error) {
|
|
const detail = String(error.stderr || error.stdout || error.message || "native compilation failed").trim();
|
|
throw new Error(`Native road compilation failed: ${detail}`);
|
|
}
|
|
}
|
|
function readJunctionReference(file) {
|
|
if (!fs.existsSync(file)) throw new Error(`Junction reference not found: ${file}`);
|
|
const converted = convertGeoJson(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
return { source: file, coordinateSystem: "GCJ-02", converted };
|
|
}
|
|
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 sendVendorFile(response, pathname) {
|
|
const match = /^\/vendor\/(ol|rbush|quickselect)\/(.+)$/.exec(pathname);
|
|
if (!match) return sendJson(response, 404, { error: "Not found" });
|
|
const root = path.join(repoRoot, "node_modules", match[1]);
|
|
const file = path.resolve(root, match[2]);
|
|
if (!file.startsWith(`${root}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) return sendJson(response, 404, { error: "Not found" });
|
|
return sendFile(response, file, file.endsWith(".css") ? "text/css; charset=utf-8" : "text/javascript; charset=utf-8");
|
|
}
|
|
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();
|