From 3d5765549708e00d0d7f33b52da84f523afe6f83 Mon Sep 17 00:00:00 2001 From: que01 Date: Fri, 14 Aug 2026 15:31:48 +0800 Subject: [PATCH] feat: inspect native junctions in workbench --- scripts/test-road-workbench.js | 3 +++ scripts/workbench/app.js | 17 ++++++++++++++--- scripts/workbench/index.html | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/scripts/test-road-workbench.js b/scripts/test-road-workbench.js index f9fc580..ee55409 100644 --- a/scripts/test-road-workbench.js +++ b/scripts/test-road-workbench.js @@ -9,6 +9,7 @@ const html = fs.readFileSync(path.join(__dirname, "workbench", "index.html"), "u assert.match(html, /id="width" type="number" min="1" step="0\.01"/); assert.match(html, /data-layer="sidewalks" type="checkbox" checked> 路缘与步行带/); assert.match(html, /id="scene-preview" type="checkbox"/); +assert.match(html, /id="selected-junction" hidden/); const app = fs.readFileSync(path.join(__dirname, "workbench", "app.js"), "utf8"); assert.match(app, /async function saveStagedChanges\(\)/); assert.match(app, /if \(!await saveStagedChanges\(\)\) return;/); @@ -16,7 +17,9 @@ assert.match(app, /function stageRoadOverride\(road, changes\)/); assert.match(app, /road\.segmentId === selectedRoad\.segmentId/); assert.match(app, /sidewalkLeft: rightInput\.checked, sidewalkRight: leftInput\.checked/); assert.match(app, /function nativeSurfaceStyle\(feature\)/); +assert.match(app, /reference: new VectorLayer\(\{ source: source\(\), visible: false/); assert.match(app, /scene mode must render fills only/); assert.match(app, /scenePreviewToggle\.onchange/); assert.match(app, /layers\.sidewalks\.setVisible\(document\.querySelector\('\[data-layer="sidewalks"\]'\)\.checked\)/); +assert.match(app, /function selectJunction\(feature\)/); console.log("road workbench tests passed"); diff --git a/scripts/workbench/app.js b/scripts/workbench/app.js index 6cbac39..a627f63 100644 --- a/scripts/workbench/app.js +++ b/scripts/workbench/app.js @@ -39,17 +39,20 @@ const saveButton = document.querySelector("#save"); const compileButton = document.querySelector("#compile"); const dirtyState = document.querySelector("#dirty-state"); const scenePreviewToggle = document.querySelector("#scene-preview"); +const selectedJunctionPanel = document.querySelector("#selected-junction"); +const junctionDetail = document.querySelector("#junction-detail"); let state; let selectedRoad = null; let selectedMovement = null; +let selectedJunction = null; let staged = []; let manualFromEndpoint = null; let diagnosticFilter = "all"; let scenePreview = false; 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 }) }) }), + reference: new VectorLayer({ source: source(), visible: false, 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: nativeSurfaceStyle }), sidewalks: new VectorLayer({ source: source(), style: sidewalkSurfaceStyle }), 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 }) }) }), @@ -63,7 +66,7 @@ const layers = { 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); }); +select.on("select", ({ selected }) => { const feature = selected[0]; if (!feature) return; if (manualFromEndpoint) return chooseManualTarget(roadForFeature(feature)); const junction = junctionForFeature(feature); if (junction) return selectJunction(junction); const movement = state.compiled.movements?.find((item) => item.id === feature.get("movement_id")) || null; selectRoad(roadForFeature(feature), undefined, movement); }); map.on("pointermove", (event) => { map.getTargetElement().style.cursor = map.hasFeatureAtPixel(event.pixel, { hitTolerance: 8 }) ? "pointer" : ""; }); function message(text) { status.textContent = text; } @@ -100,6 +103,7 @@ function roadForFeature(feature) { const roadId = properties.road_id || properties.subjectId || roadIdFromLane(properties.from_lane_id) || properties.directional_road_ids?.split(",")[0]; return state.compiled.model.roads.find((road) => road.id === roadId) || null; } +function junctionForFeature(feature) { const id = feature.get("native_id"); return id?.startsWith("junction:") ? layers.native.getSource().getFeatures().find((item) => item.get("native_id") === id) : null; } function endpointFor(road, side) { return state.compiled.model.endpoints.find((endpoint) => endpoint.roadId === road?.id && endpoint.side === side) || null; } function endpointsCompatible(from, to) { if (!from || !to || from.roadId === to.roadId || from.side !== "end" || to.side !== "start") return false; if (from.nodeId === to.nodeId) return true; const dx = (from.coordinate[0] - to.coordinate[0]) * 111320 * Math.cos(from.coordinate[1] * Math.PI / 180); const dy = (from.coordinate[1] - to.coordinate[1]) * 111320; return Math.hypot(dx, dy) <= 35; } function readFeatures(collection) { return geojson.readFeatures(collection || { type: "FeatureCollection", features: [] }, { dataProjection: "EPSG:4326", featureProjection: "EPSG:3857" }); } @@ -120,7 +124,7 @@ function effectiveConnectionEnabled(connection) { const id = `连接:${connectio function effectiveConnectorEnabled(connector) { const connection = state?.compiled.model.connections.find((item) => item.id === connector.connection_id); return effectiveLaneEnabled(connector) && (!connection || effectiveConnectionEnabled(connection)); } function selectRoad(road, note, movement = null) { - selectedRoad = road; selectedMovement = movement; layers.osm.changed(); layers.lanes.changed(); layers.connectors.changed(); refreshOsmDirection(); refreshSelectedMovement(); + selectedRoad = road; selectedMovement = movement; selectedJunction = null; selectedJunctionPanel.hidden = true; layers.osm.changed(); layers.lanes.changed(); layers.connectors.changed(); refreshOsmDirection(); refreshSelectedMovement(); layers.selectedRoad.getSource().clear(); if (road) layers.selectedRoad.getSource().addFeature(new Feature({ geometry: new LineString(road.centerline).transform("EPSG:4326", "EPSG:3857") })); form.hidden = !road; hint.hidden = Boolean(road); if (!road) return; roadName.textContent = `${roadLabel(road)}(${osmDirectionLabel(road)})`; @@ -129,6 +133,13 @@ function selectRoad(road, note, movement = null) { laneConvention.textContent = road.laneCount === 1 ? "蓝色箭头在 OSM 原始中心线上,表示当前方向;“沿 OSM 方向”即节点顺序。本方向只有一条车道。" : "蓝色箭头在 OSM 原始中心线上,表示当前方向;“沿 OSM 方向”即节点顺序。车道按行驶方向从左向右编号。"; renderDirectionSwitch(road); renderMovementSummary(road); renderSelectedMovement(); renderConnections(road); message(note || (selectedMovement ? `已选中行驶动作:${turnLabel(selectedMovement.turn)}` : `已选中:${roadLabel(road)}`)); } +function selectJunction(feature) { + selectedJunction = feature; selectedRoad = null; selectedMovement = null; form.hidden = true; hint.hidden = true; selectedJunctionPanel.hidden = false; + const properties = feature.getProperties(); const roadIds = String(properties.source_road_ids || "").split(",").filter(Boolean); + const roads = roadIds.map((id) => state.compiled.model.roads.find((road) => road.id === id)).filter(Boolean); + junctionDetail.textContent = JSON.stringify({ OSM节点: properties.osm_node_id, 类型: properties.kind === "t" ? "T字路口" : "十字路口", 参与方向道路: roads.map((road) => ({ 道路: roadLabel(road), OSM道路: road.osmWayIds, 节点顺序: road.sourceNodeIds })), 构面规则: properties.rule, 边界策略: properties.boundary_mode, 路口退让距离米: properties.cutback_m, 行驶动作数: properties.movement_count, 已绘制连接数: properties.connector_count }, null, 2); + message(`已选中路口:OSM 节点 ${properties.osm_node_id}`); +} function turnLabel(turn) { return { left: "左转", through: "直行", right: "右转", uturn: "掉头" }[turn] || turn; } function renderSelectedMovement() { selectedMovementPanel.hidden = !selectedMovement; if (!selectedMovement) return; const targetRoad = state.compiled.model.roads.find((road) => road.id === selectedMovement.toRoadId); const geometry = selectedMovement.geometryStatus === "connector" ? "已绘制路径" : selectedMovement.geometryStatus === "continuous" ? "节点连续" : "路径过长未绘制"; movementDetail.textContent = `${turnLabel(selectedMovement.turn)}:${lanePositionLabel(selectedRoad, laneIndex(selectedMovement.fromLaneId))} → ${lanePositionLabel(targetRoad, laneIndex(selectedMovement.toLaneId))}\n目标:${roadLabel(targetRoad)}(${osmDirectionLabel(targetRoad)})\n来源端点:${selectedRoad.sourceNodeIds.at(-1)};目标端点:${targetRoad.sourceNodeIds[0]}\n路口节点:${selectedMovement.nodeId}\n状态:${geometry}\n来源:${selectedMovement.provenance}`; } function renderDirectionSwitch(road) { diff --git a/scripts/workbench/index.html b/scripts/workbench/index.html index d3645db..5b7b537 100644 --- a/scripts/workbench/index.html +++ b/scripts/workbench/index.html @@ -1,4 +1,4 @@ 道路编译工作台
道路编译工作台
-
+