feat: add lane-level turn controls

This commit is contained in:
2026-08-14 09:22:38 +08:00
parent df81a22ce8
commit 850e9eb344
5 changed files with 46 additions and 10 deletions

View File

@@ -15,6 +15,7 @@ const compileButton = document.querySelector("#compile");
const diagnostics = document.querySelector("#diagnostics");
const directionSwitch = document.querySelector("#direction-switch");
const addConnectionButton = document.querySelector("#add-connection");
const movementSummary = document.querySelector("#movement-summary");
let state;
let selected = null;
let staged = [];
@@ -38,6 +39,7 @@ function draw() {
layerLine(feature, active ? "#006e91" : "#f5f6ee", active ? 2.4 : 1.2, true);
}
for (const feature of layers.connectors?.features || []) {
if (!effectiveConnectorEnabled(feature.properties)) continue;
const active = feature.properties.from_lane_id.startsWith(`lane:${selected?.id}:`);
layerLine(feature, active ? "#d1226f" : "#ad3a76", active ? 3 : 1.8, true);
}
@@ -69,10 +71,18 @@ function select(road) {
document.querySelector("#road-name").textContent = `${roadLabel(road)}${road.direction === "forward" ? "沿 OSM 方向行驶" : "逆 OSM 方向行驶"}`;
widthInput.value = road.widthMeters; lanesInput.value = road.laneCount; leftInput.checked = road.sidewalkLeft; rightInput.checked = road.sidewalkRight;
evidence.textContent = JSON.stringify({ OSM道路: road.osmWayIds, 名称: road.tags.name || "未标注", 参数来源: road.provenance, 已应用修改: road.appliedOverrideIds, 原始标签: road.tags }, null, 2);
renderMovementSummary(road);
renderDirectionSwitch(road);
renderConnections(road); draw();
}
function renderMovementSummary(road) {
const connectors = (state.layers.connectors?.features || []).filter((feature) => feature.properties.from_lane_id.startsWith(`lane:${road.id}:`) && effectiveConnectorEnabled(feature.properties));
const byTurn = connectors.reduce((result, feature) => { result[feature.properties.turn] = (result[feature.properties.turn] || 0) + 1; return result; }, {});
const labels = { left: "左转", through: "直行", right: "右转", uturn: "掉头" };
movementSummary.textContent = connectors.length ? `当前方向已生成 ${connectors.length} 条路径:${Object.entries(byTurn).map(([turn, count]) => `${labels[turn] || turn} ${count}`).join("")}` : "当前方向没有已生成的转向路径";
}
function renderDirectionSwitch(road) {
directionSwitch.innerHTML = "";
const alternatives = state.compiled.model.roads.filter((item) => item.osmWayIds.join(",") === road.osmWayIds.join(","));
@@ -100,11 +110,24 @@ function renderConnections(road) {
if (!target || seen.has(target.id)) continue;
seen.add(target.id);
const input = document.createElement("input"); const label = document.createElement("label");
input.type = "checkbox"; input.checked = connection.enabled; input.onchange = () => stageConnection(connection, input.checked);
input.type = "checkbox"; input.checked = effectiveConnectionEnabled(connection); input.onchange = () => { stageConnection(connection, input.checked); renderConnections(road); draw(); };
label.append(input, ` ${turnName(road, target)}${roadLabel(target)}`); box.append(label);
renderLaneConnectionControls(box, connection);
}
}
function renderLaneConnectionControls(box, connection) {
const connectors = state.layers.connectors?.features.filter((feature) => feature.properties.connection_id === connection.id) || [];
const rows = connectors.map((feature) => ({ from_lane_id: feature.properties.from_lane_id, to_lane_id: feature.properties.to_lane_id, enabled: true }));
const sourceRoad = connection.fromEndpointId.slice("endpoint:".length).replace(/:end$/, "");
const targetRoad = connection.toEndpointId.slice("endpoint:".length).replace(/:start$/, "");
for (const override of state.overrides.overrides.filter((item) => item.kind === "lane-connection" && item.fromLaneId.startsWith(`lane:${sourceRoad}:`) && item.toLaneId.startsWith(`lane:${targetRoad}:`))) {
if (!rows.some((row) => row.from_lane_id === override.fromLaneId && row.to_lane_id === override.toLaneId)) rows.push({ from_lane_id: override.fromLaneId, to_lane_id: override.toLaneId, enabled: override.enabled });
}
for (const row of rows) appendLaneConnectionControl(box, row);
}
function appendLaneConnectionControl(box, row) { const fromIndex = row.from_lane_id.split(":").at(-1); const toIndex = row.to_lane_id.split(":").at(-1); const input = document.createElement("input"); const label = document.createElement("label"); input.type = "checkbox"; input.checked = effectiveLaneEnabled(row); input.onchange = () => { stageLaneConnection(row, input.checked); if (selected) { renderConnections(selected); renderMovementSummary(selected); draw(); } }; label.append(input, `${fromIndex} 车道 → 第 ${toIndex} 车道`); box.append(label); }
function renderAdditionalConnections(road, endpoint) {
const box = document.querySelector("#connections");
const existing = new Set(state.compiled.model.connections.filter((connection) => connection.fromEndpointId === endpoint.id).map((connection) => connection.toEndpointId));
@@ -131,6 +154,10 @@ function turnName(from, to) {
function heading(a, b) { return Math.atan2(b[0] - a[0], b[1] - a[1]) * 180 / Math.PI; }
function endpointDistanceMeters(a, b) { const dx = (a.coordinate[0] - b.coordinate[0]) * 111320 * Math.cos(a.coordinate[1] * Math.PI / 180); const dy = (a.coordinate[1] - b.coordinate[1]) * 111320; return Math.hypot(dx, dy); }
function stageConnection(connection, enabled) { const id = `连接:${connection.id}`; staged = staged.filter((item) => item.id !== id); staged.push({ id, kind: "junction-connection", fromEndpointId: connection.fromEndpointId, toEndpointId: connection.toEndpointId, enabled }); message("有未保存修改"); }
function stageLaneConnection(connector, enabled) { const id = `车道连接:${connector.from_lane_id}->${connector.to_lane_id}`; staged = staged.filter((item) => item.id !== id); staged.push({ id, kind: "lane-connection", fromLaneId: connector.from_lane_id, toLaneId: connector.to_lane_id, enabled }); message("有未保存修改"); }
function effectiveLaneEnabled(connector) { const id = `车道连接:${connector.from_lane_id}->${connector.to_lane_id}`; const override = [...staged, ...state.overrides.overrides].find((item) => item.id === id); return override ? override.enabled : connector.enabled !== false; }
function effectiveConnectionEnabled(connection) { const id = `连接:${connection.id}`; const override = [...staged, ...state.overrides.overrides].find((item) => item.id === id); return override ? override.enabled : connection.enabled; }
function effectiveConnectorEnabled(connector) { const connection = state.compiled.model.connections.find((item) => item.id === connector.connection_id); return effectiveLaneEnabled(connector) && (!connection || effectiveConnectionEnabled(connection)); }
function pointToSegmentDistance(point, a, b) { const dx = b[0] - a[0]; const dy = b[1] - a[1]; const lengthSquared = dx * dx + dy * dy; const t = lengthSquared ? Math.max(0, Math.min(1, ((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / lengthSquared)) : 0; return Math.hypot(point[0] - (a[0] + t * dx), point[1] - (a[1] + t * dy)); }
function pickRoad(point) { let best = null; let distance = Infinity; for (const road of state.compiled.model.roads) for (let index = 1; index < road.centerline.length; index += 1) { const candidate = pointToSegmentDistance(point, coord(road.centerline[index - 1]), coord(road.centerline[index])); if (candidate < distance) { distance = candidate; best = road; } } return distance <= 18 * devicePixelRatio ? best : null; }
canvas.onclick = (event) => { const rect = canvas.getBoundingClientRect(); select(pickRoad([(event.clientX - rect.left) * devicePixelRatio, (event.clientY - rect.top) * devicePixelRatio])); };