From e1f3fc10ca2380dd7d2a0eac7471e31520cee9f5 Mon Sep 17 00:00:00 2001 From: que01 Date: Fri, 14 Aug 2026 10:53:20 +0800 Subject: [PATCH] fix: persist and render native sidewalks --- scripts/compile-native-roads.js | 4 +++- scripts/lib/native-road.js | 38 ++++++++++++++++++++++++++++++++- scripts/road-workbench.js | 2 +- scripts/test-native-road.js | 5 +++++ scripts/test-road-workbench.js | 6 ++++++ scripts/workbench/app.js | 14 +++++++----- scripts/workbench/index.html | 2 +- 7 files changed, 62 insertions(+), 9 deletions(-) diff --git a/scripts/compile-native-roads.js b/scripts/compile-native-roads.js index 0b1b6c4..038bac9 100644 --- a/scripts/compile-native-roads.js +++ b/scripts/compile-native-roads.js @@ -34,13 +34,14 @@ function compileArea(configPath) { model: { roads: model.roads, endpoints: model.endpoints, connections: model.connections }, movements: compiled.movements, diagnostics: compiled.diagnostics, - layers: { roadSurface: "layers/road_surface.geojson", intersectionSurface: "layers/intersection_surface.geojson", laneCenterlines: "layers/lane_centerlines.geojson", connectors: "layers/connectors.geojson" }, + layers: { roadSurface: "layers/road_surface.geojson", sidewalkSurface: "layers/sidewalk_surface.geojson", intersectionSurface: "layers/intersection_surface.geojson", laneCenterlines: "layers/lane_centerlines.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", "connectors.geojson"), compiled.connectors); @@ -71,6 +72,7 @@ function compareOsm2Streets(area, model, compiled) { schema: "native-road-comparison/v2", nativeRoadCount: model.roads.length, nativeRoadSurfaceFeatures: compiled.roadSurface.features.length, + nativeSidewalkSurfaceFeatures: compiled.sidewalkSurface.features.length, nativeJunctionSurfaceFeatures: compiled.intersectionSurface.features.length, nativeLaneCenterlineFeatures: compiled.laneCenterlines.features.length, nativeConnectorFeatures: compiled.connectors.features.length, diff --git a/scripts/lib/native-road.js b/scripts/lib/native-road.js index e8bbcf3..e8b847d 100644 --- a/scripts/lib/native-road.js +++ b/scripts/lib/native-road.js @@ -6,6 +6,7 @@ const path = require("path"); const OVERRIDE_SCHEMA = "native-road-overrides/v1"; const MOTOR_HIGHWAYS = new Set(["motorway", "trunk", "primary", "secondary", "tertiary", "unclassified", "residential", "living_street", "service"]); const DEFAULT_WIDTHS = { motorway: 12, trunk: 10, primary: 10, secondary: 8, tertiary: 7, unclassified: 6, residential: 6, living_street: 5, service: 4 }; +const DEFAULT_SIDEWALK_WIDTH_METERS = 2; function parseOsmRoads(xml) { const nodes = new Map(); @@ -203,10 +204,37 @@ function compileGeometry(model, overrides = { overrides: [] }) { features.push({ type: "Feature", properties: { native_id: `surface:way/${wayKey}`, directional_road_ids: directions.map((item) => item.id).join(","), osm_way_ids: wayKey, width_m: totalWidth, lane_count: directions.reduce((sum, item) => sum + item.laneCount, 0), provenance: JSON.stringify(directions.map((item) => item.provenance)), override_ids: directions.flatMap((item) => item.appliedOverrideIds).join(",") }, geometry: { type: "Polygon", coordinates: [ring] } }); } const lanes = compileLaneCenterlines(model, diagnostics); + const sidewalks = compileSidewalkSurfaces(model, diagnostics); const connectorResult = compileConnectors(model, lanes, diagnostics, overrides); const junctionFeatures = compileJunctionSurfaces(model, lanes, connectorResult.features, connectorResult.movements, diagnostics); validateConnectorContainment(connectorResult.features, junctionFeatures, diagnostics); - return { roadSurface: { type: "FeatureCollection", features }, intersectionSurface: { type: "FeatureCollection", features: junctionFeatures }, laneCenterlines: { type: "FeatureCollection", features: lanes.features }, connectors: { type: "FeatureCollection", features: connectorResult.features }, movements: connectorResult.movements, diagnostics }; + return { roadSurface: { type: "FeatureCollection", features }, sidewalkSurface: { type: "FeatureCollection", features: sidewalks }, intersectionSurface: { type: "FeatureCollection", features: junctionFeatures }, laneCenterlines: { type: "FeatureCollection", features: lanes.features }, connectors: { type: "FeatureCollection", features: connectorResult.features }, movements: connectorResult.movements, diagnostics }; +} + +function compileSidewalkSurfaces(model, diagnostics) { + const features = []; + const byWay = new Map(); + for (const road of model.roads) { + const key = road.osmWayIds.join(","); + if (!byWay.has(key)) byWay.set(key, []); + byWay.get(key).push(road); + } + for (const [wayKey, directions] of byWay) { + const forward = directions.find((road) => road.direction === "forward") || directions[0]; + const backward = directions.find((road) => road.id !== forward.id); + const totalWidth = directions.reduce((sum, road) => sum + road.widthMeters, 0); + const sides = [ + ["left", forward.sidewalkLeft || Boolean(backward?.sidewalkRight)], + ["right", forward.sidewalkRight || Boolean(backward?.sidewalkLeft)], + ]; + for (const [side, enabled] of sides) { + if (!enabled) continue; + const ring = sidewalkRing(forward.centerline, totalWidth / 2, totalWidth / 2 + DEFAULT_SIDEWALK_WIDTH_METERS, side === "left" ? 1 : -1); + if (!ring) { diagnostics.push(diagnostic("warning", forward.id, forward.osmWayIds, "invalid-sidewalk-surface", "无法为该道路生成连续人行道面。", forward.centerline[0])); continue; } + features.push({ type: "Feature", properties: { native_id: `sidewalk:way/${wayKey}:${side}`, osm_way_ids: wayKey, side, width_m: DEFAULT_SIDEWALK_WIDTH_METERS, directional_road_ids: directions.map((road) => road.id).join(","), provenance: "native-road-sidewalk/v1", override_ids: directions.flatMap((road) => road.appliedOverrideIds).join(",") }, geometry: { type: "Polygon", coordinates: [ring] } }); + } + } + return features; } function validateConnectorContainment(connectors, junctionFeatures, diagnostics) { @@ -483,6 +511,14 @@ function roadRing(line, width) { return ring.every((point) => point.every(Number.isFinite)) ? ring : null; } +function sidewalkRing(line, innerOffset, outerOffset, side) { + const inner = offsetLine(line, innerOffset * side); + const outer = offsetLine(line, outerOffset * side); + if (!inner || !outer) return null; + const ring = [...inner, ...outer.reverse(), inner[0]]; + return ring.every((point) => point.every(Number.isFinite)) ? ring : null; +} + function project(point, origin) { const scale = 111320; return [(point[0] - origin[0]) * scale * Math.cos(origin[1] * Math.PI / 180), (point[1] - origin[1]) * scale]; } function unproject(point, origin) { const scale = 111320; return [point[0] / (scale * Math.cos(origin[1] * Math.PI / 180)) + origin[0], point[1] / scale + origin[1]]; } function diagnostic(severity, subjectId, sourceIds, rule, message, coordinate) { return { id: `diagnostic:${rule}:${subjectId}`, severity, subjectId, sourceIds, rule, message, geometry: coordinate ? { type: "Point", coordinates: coordinate } : null }; } diff --git a/scripts/road-workbench.js b/scripts/road-workbench.js index 88c6b73..fc42faa 100644 --- a/scripts/road-workbench.js +++ b/scripts/road-workbench.js @@ -48,7 +48,7 @@ function handle(request, response, area, configPath) { 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 } }; + 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")), 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")), 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")); } diff --git a/scripts/test-native-road.js b/scripts/test-native-road.js index 0b8b48d..43f3f88 100644 --- a/scripts/test-native-road.js +++ b/scripts/test-native-road.js @@ -23,6 +23,8 @@ 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.connectors.features.length > 0); assert.ok(geometry.connectors.features.every((feature) => feature.geometry.coordinates.length === 13)); @@ -49,6 +51,9 @@ assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", over const turnOsm = ``; const turnModel = compileRoadModel(turnOsm, empty); const turnGeometry = compileGeometry(turnModel); +assert.equal(turnGeometry.sidewalkSurface.features.length, 0); +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$/); diff --git a/scripts/test-road-workbench.js b/scripts/test-road-workbench.js index 76101aa..b3e595f 100644 --- a/scripts/test-road-workbench.js +++ b/scripts/test-road-workbench.js @@ -7,4 +7,10 @@ const path = require("path"); const html = fs.readFileSync(path.join(__dirname, "workbench", "index.html"), "utf8"); assert.match(html, /id="width" type="number" min="1" step="0\.01"/); +assert.match(html, /data-layer="sidewalks" type="checkbox" checked/); +const app = fs.readFileSync(path.join(__dirname, "workbench", "app.js"), "utf8"); +assert.match(app, /async function saveStagedChanges\(\)/); +assert.match(app, /if \(!await saveStagedChanges\(\)\) return;/); +assert.match(app, /function stageRoadOverride\(road, changes\)/); +assert.match(app, /sidewalkLeft: rightInput\.checked, sidewalkRight: leftInput\.checked/); console.log("road workbench tests passed"); diff --git a/scripts/workbench/app.js b/scripts/workbench/app.js index 151ce66..88b502c 100644 --- a/scripts/workbench/app.js +++ b/scripts/workbench/app.js @@ -49,6 +49,7 @@ const source = () => new VectorSource(); const layers = { reference: new VectorLayer({ source: source(), style: new Style({ fill: new Fill({ color: "rgba(123, 140, 148, .28)" }), stroke: new Stroke({ color: "#8999a0", width: 1 }) }) }), native: new VectorLayer({ source: source(), style: (feature) => feature.get("native_id")?.startsWith("junction:") ? new Style({ fill: new Fill({ color: "rgba(12, 116, 91, .38)" }), stroke: new Stroke({ color: "#0e785f", width: 1.5 }) }) : new Style({ fill: new Fill({ color: "rgba(40, 105, 86, .35)" }), stroke: new Stroke({ color: "#296956", width: 1 }) }) }), + sidewalks: new VectorLayer({ source: source(), style: new Style({ fill: new Fill({ color: "rgba(218, 191, 137, .6)" }), stroke: new Stroke({ color: "#9b7c40", width: 1 }) }) }), osm: new VectorLayer({ source: source(), style: (feature) => new Style({ stroke: new Stroke({ color: feature.get("road_id") === selectedRoad?.id ? "#006e91" : "#263630", width: feature.get("road_id") === selectedRoad?.id ? 5 : 2 }) }) }), lanes: new VectorLayer({ source: source(), style: laneStyle }), osmDirection: new VectorLayer({ source: source(), style: (feature) => new Style({ image: new RegularShape({ points: 3, radius: 9, rotation: feature.get("rotation"), fill: new Fill({ color: "#006e91" }), stroke: new Stroke({ color: "#fff", width: 1.5 }) }) }), zIndex: 11 }), @@ -57,7 +58,7 @@ const layers = { selectedRoad: new VectorLayer({ source: source(), style: new Style({ stroke: new Stroke({ color: "#00a5cf", width: 8 }) }), zIndex: 10 }), selectedMovement: new VectorLayer({ source: source(), style: new Style({ stroke: new Stroke({ color: "#f0b323", width: 6 }) }), zIndex: 12 }), }; -const map = new Map({ target: "map", layers: [layers.reference, layers.native, layers.osm, layers.lanes, layers.connectors, layers.diagnostics, layers.selectedRoad, layers.osmDirection, layers.selectedMovement], view: new View({ center: [0, 0], zoom: 2 }) }); +const map = new Map({ target: "map", layers: [layers.reference, layers.native, layers.sidewalks, layers.osm, layers.lanes, layers.connectors, layers.diagnostics, layers.selectedRoad, layers.osmDirection, layers.selectedMovement], view: new View({ center: [0, 0], zoom: 2 }) }); const select = new Select({ condition: click, layers: (layer) => manualFromEndpoint ? layer === layers.osm : [layers.osm, layers.lanes, layers.connectors, layers.native, layers.diagnostics].includes(layer), hitTolerance: 8, style: new Style({ stroke: new Stroke({ color: "#005e89", width: 5 }), fill: new Fill({ color: "rgba(0, 94, 137, .18)" }) }) }); map.addInteraction(select); select.on("select", ({ selected }) => { const feature = selected[0]; if (!feature) return; if (manualFromEndpoint) return chooseManualTarget(roadForFeature(feature)); const movement = state.compiled.movements?.find((item) => item.id === feature.get("movement_id")) || null; selectRoad(roadForFeature(feature), undefined, movement); }); @@ -91,6 +92,7 @@ function rawRoadFeatures() { return state.compiled.model.roads.map((road) => new function updateSources() { layers.reference.getSource().clear(); layers.reference.getSource().addFeatures(readFeatures(state.layers.osm2streetsRoadSurface)); layers.native.getSource().clear(); layers.native.getSource().addFeatures([...readFeatures(state.layers.nativeRoadSurface), ...readFeatures(state.layers.nativeIntersectionSurface)]); + layers.sidewalks.getSource().clear(); layers.sidewalks.getSource().addFeatures(readFeatures(state.layers.nativeSidewalkSurface)); layers.osm.getSource().clear(); layers.osm.getSource().addFeatures(rawRoadFeatures()); layers.lanes.getSource().clear(); layers.lanes.getSource().addFeatures(readFeatures(state.layers.laneCenterlines)); layers.connectors.getSource().clear(); layers.connectors.getSource().addFeatures(readFeatures(state.layers.connectors)); @@ -136,10 +138,12 @@ addConnectionButton.onclick = () => { const endpoint = endpointFor(selectedRoad, function focusDiagnostic(item) { const feature = layers.diagnostics.getSource().getFeatures().find((candidate) => candidate.get("id") === item.id); if (feature) map.getView().fit(feature.getGeometry().getExtent(), { padding: [80, 80, 80, 360], maxZoom: 18, duration: 250 }); selectRoad(state.compiled.model.roads.find((road) => road.id === item.subjectId), `已定位:${item.message}`); } function diagnosticLabel(item) { const road = state.compiled.model.roads.find((candidate) => candidate.id === item.subjectId); if (item.rule !== "unconnected-interior-road-end" || !road) return item.message; const candidateCount = item.manualCandidates?.length || 0; return `${roadLabel(road)}(${osmDirectionLabel(road)},节点 ${item.sourceIds[0]}):内部端点未连接${candidateCount ? `,附近有 ${candidateCount} 个可手工连接候选` : ""}`; } function renderDiagnostics() { const all = state.compiled.diagnostics.filter((diagnostic) => diagnostic.rule !== "ordinary-junction-surface"); const counts = { all: all.length, candidates: all.filter((item) => item.manualCandidates?.length).length, other: all.filter((item) => !item.manualCandidates?.length).length }; for (const button of diagnosticFilters.querySelectorAll("button")) { const filter = button.dataset.diagnosticFilter; button.classList.toggle("active", filter === diagnosticFilter); button.textContent = `${filter === "all" ? "全部" : filter === "candidates" ? "可连接" : "其他"}(${counts[filter]})`; } const visible = all.filter((item) => diagnosticFilter === "all" || diagnosticFilter === "candidates" ? Boolean(item.manualCandidates?.length) : !item.manualCandidates?.length).sort((a, b) => (b.manualCandidates?.length || 0) - (a.manualCandidates?.length || 0)); diagnostics.innerHTML = ""; for (const item of visible) { const button = document.createElement("button"); button.textContent = diagnosticLabel(item); button.onclick = () => focusDiagnostic(item); diagnostics.append(button); } } -function renderSummary() { const comparison = state.comparison; const rows = [["方向道路", comparison.nativeRoadCount], ["路口面", comparison.nativeJunctionSurfaceFeatures], ["行驶动作", comparison.nativeMovementCount], ["已绘制路径", comparison.nativePublishedMovementCount], ["内部断头", comparison.unconnectedInteriorRoadEnds], ["可手工复核", comparison.unconnectedEndsWithManualCandidates], ["osm2streets 参考", comparison.osm2streetsAvailable ? comparison.osm2streetsRoadSurfaceFeatures : "无"]]; summary.innerHTML = ""; for (const [label, value] of rows) { const term = document.createElement("dt"); const detail = document.createElement("dd"); term.textContent = label; detail.textContent = value; summary.append(term, detail); } } -form.onsubmit = (event) => { event.preventDefault(); const id = `道路:${selectedRoad.id}`; staged = staged.filter((item) => item.id !== id); staged.push({ id, kind: "road", roadId: selectedRoad.id, widthMeters: Number(widthInput.value), laneCount: Number(lanesInput.value), sidewalkLeft: leftInput.checked, sidewalkRight: rightInput.checked }); updateDirtyState(); message("有未保存修改"); }; -saveButton.onclick = async () => { const existing = state.overrides.overrides.filter((item) => !staged.some((change) => change.id === item.id)); const response = await fetch("/api/overrides", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ schema: "native-road-overrides/v1", overrides: [...existing, ...staged] }) }); const result = await response.json(); if (!result.ok) return message(result.error); state.overrides = result.overrides; staged = []; updateDirtyState(); message("已保存,点击“保存并重新生成”写入几何"); }; -compileButton.onclick = async () => { message("正在重新生成..."); const response = await fetch("/api/compile", { method: "POST" }); state = await response.json(); staged = []; updateDirtyState(); updateSources(); renderDiagnostics(); renderSummary(); selectRoad(selectedRoad ? state.compiled.model.roads.find((road) => road.id === selectedRoad.id) : null); message("已重新生成"); }; +function renderSummary() { const comparison = state.comparison; const rows = [["方向道路", comparison.nativeRoadCount], ["人行道面", comparison.nativeSidewalkSurfaceFeatures], ["路口面", comparison.nativeJunctionSurfaceFeatures], ["行驶动作", comparison.nativeMovementCount], ["已绘制路径", comparison.nativePublishedMovementCount], ["内部断头", comparison.unconnectedInteriorRoadEnds], ["可手工复核", comparison.unconnectedEndsWithManualCandidates], ["osm2streets 参考", comparison.osm2streetsAvailable ? comparison.osm2streetsRoadSurfaceFeatures : "无"]]; summary.innerHTML = ""; for (const [label, value] of rows) { const term = document.createElement("dt"); const detail = document.createElement("dd"); term.textContent = label; detail.textContent = value; summary.append(term, detail); } } +function stageRoadOverride(road, changes) { const id = `道路:${road.id}`; const existing = staged.find((item) => item.id === id) || state.overrides.overrides.find((item) => item.id === id); staged = staged.filter((item) => item.id !== id); staged.push({ ...existing, id, kind: "road", roadId: road.id, ...changes }); } +form.onsubmit = (event) => { event.preventDefault(); const roadChanges = { widthMeters: Number(widthInput.value), laneCount: Number(lanesInput.value), sidewalkLeft: leftInput.checked, sidewalkRight: rightInput.checked }; stageRoadOverride(selectedRoad, roadChanges); const opposite = state.compiled.model.roads.find((road) => road.id !== selectedRoad.id && road.osmWayIds.join(",") === selectedRoad.osmWayIds.join(",")); if (opposite) stageRoadOverride(opposite, { sidewalkLeft: rightInput.checked, sidewalkRight: leftInput.checked }); updateDirtyState(); message(opposite ? "有未保存修改:双向道路的人行道已按实际侧边同步" : "有未保存修改"); }; +async function saveStagedChanges() { if (!staged.length) return true; const existing = state.overrides.overrides.filter((item) => !staged.some((change) => change.id === item.id)); const response = await fetch("/api/overrides", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ schema: "native-road-overrides/v1", overrides: [...existing, ...staged] }) }); const result = await response.json(); if (!result.ok) { message(result.error); return false; } state.overrides = result.overrides; staged = []; updateDirtyState(); return true; } +saveButton.onclick = async () => { if (await saveStagedChanges()) message("已保存,点击“保存并重新生成”写入几何"); }; +compileButton.onclick = async () => { if (!await saveStagedChanges()) return; message("正在保存修改并重新生成..."); const response = await fetch("/api/compile", { method: "POST" }); state = await response.json(); staged = []; updateDirtyState(); updateSources(); renderDiagnostics(); renderSummary(); selectRoad(selectedRoad ? state.compiled.model.roads.find((road) => road.id === selectedRoad.id) : null); message("已保存并重新生成"); }; for (const input of document.querySelectorAll("[data-layer]")) input.onchange = () => { layers[input.dataset.layer].setVisible(input.checked); if (input.dataset.layer === "osm") layers.osmDirection.setVisible(input.checked); }; for (const button of diagnosticFilters.querySelectorAll("button")) button.onclick = () => { diagnosticFilter = button.dataset.diagnosticFilter; renderDiagnostics(); }; fetch("/api/state").then((response) => response.json()).then((value) => { state = value; updateDirtyState(); updateSources(); renderDiagnostics(); renderSummary(); areaLabel.textContent = state.areaId; message(`已加载 ${state.compiled.model.roads.length} 条方向道路`); }).catch((error) => message(error.message)); diff --git a/scripts/workbench/index.html b/scripts/workbench/index.html index 97d0739..c4670d0 100644 --- a/scripts/workbench/index.html +++ b/scripts/workbench/index.html @@ -1,4 +1,4 @@ 道路编译工作台
道路编译工作台
-
+