feat: compile native road lanes and junctions
This commit is contained in:
@@ -14,6 +14,7 @@ const saveButton = document.querySelector("#save");
|
||||
const compileButton = document.querySelector("#compile");
|
||||
const diagnostics = document.querySelector("#diagnostics");
|
||||
const directionSwitch = document.querySelector("#direction-switch");
|
||||
const addConnectionButton = document.querySelector("#add-connection");
|
||||
let state;
|
||||
let selected = null;
|
||||
let staged = [];
|
||||
@@ -24,6 +25,7 @@ function coord(point) { const b = state.bounds; return [(point[0] - b.minX) / (b
|
||||
function setBounds() { const points = state.compiled.model.roads.flatMap((road) => road.centerline); const xs = points.map((point) => point[0]); const ys = points.map((point) => point[1]); const pad = Math.max((Math.max(...xs) - Math.min(...xs)) * 0.06, 0.0001); state.bounds = { minX: Math.min(...xs) - pad, maxX: Math.max(...xs) + pad, minY: Math.min(...ys) - pad, maxY: Math.max(...ys) + pad }; }
|
||||
function resize() { canvas.width = canvas.clientWidth * devicePixelRatio; canvas.height = canvas.clientHeight * devicePixelRatio; draw(); }
|
||||
function polygon(feature, fill) { const ring = feature.geometry?.coordinates?.[0]; if (!ring) return; context.beginPath(); ring.forEach((point, index) => { const point2d = coord(point); index ? context.lineTo(...point2d) : context.moveTo(...point2d); }); context.fillStyle = fill; context.fill(); }
|
||||
function layerLine(feature, stroke, width, dashed) { const points = feature.geometry?.coordinates; if (!points?.length) return; context.save(); context.setLineDash(dashed ? [5 * devicePixelRatio, 4 * devicePixelRatio] : []); context.beginPath(); points.forEach((point, index) => { const point2d = coord(point); index ? context.lineTo(...point2d) : context.moveTo(...point2d); }); context.strokeStyle = stroke; context.lineWidth = width * devicePixelRatio; context.stroke(); context.restore(); }
|
||||
function draw() {
|
||||
if (!state) return;
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -31,6 +33,14 @@ function draw() {
|
||||
for (const feature of layers.osm2streetsRoadSurface?.features || []) polygon(feature, "#9ba8ae55");
|
||||
for (const feature of layers.nativeRoadSurface?.features || []) polygon(feature, "#28695666");
|
||||
for (const feature of layers.nativeIntersectionSurface?.features || []) polygon(feature, "#0e786066");
|
||||
for (const feature of layers.laneCenterlines?.features || []) {
|
||||
const active = feature.properties.road_id === selected?.id;
|
||||
layerLine(feature, active ? "#006e91" : "#f5f6ee", active ? 2.4 : 1.2, true);
|
||||
}
|
||||
for (const feature of layers.connectors?.features || []) {
|
||||
const active = feature.properties.from_lane_id.startsWith(`lane:${selected?.id}:`);
|
||||
layerLine(feature, active ? "#d1226f" : "#ad3a76", active ? 3 : 1.8, true);
|
||||
}
|
||||
for (const road of state.compiled.model.roads) {
|
||||
context.beginPath(); road.centerline.forEach((point, index) => { const point2d = coord(point); index ? context.lineTo(...point2d) : context.moveTo(...point2d); });
|
||||
context.strokeStyle = road.id === selected?.id ? "#006e91" : "#263630";
|
||||
@@ -78,6 +88,8 @@ function renderDirectionSwitch(road) {
|
||||
function renderConnections(road) {
|
||||
const box = document.querySelector("#connections"); box.innerHTML = "";
|
||||
const endpoint = state.compiled.model.endpoints.find((item) => item.roadId === road.id && item.side === "end");
|
||||
addConnectionButton.hidden = !endpoint;
|
||||
addConnectionButton.onclick = () => renderAdditionalConnections(road, endpoint);
|
||||
const rows = state.compiled.model.connections.filter((connection) => connection.fromEndpointId === endpoint?.id);
|
||||
if (!rows.length) { box.textContent = "当前行驶方向到达道路终点后,没有可编辑的驶出道路。"; return; }
|
||||
const intro = document.createElement("p"); intro.textContent = "到达终点路口后,允许驶入:"; box.append(intro);
|
||||
@@ -93,6 +105,21 @@ function renderConnections(road) {
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
const candidates = state.compiled.model.endpoints.filter((item) => item.side === "start" && item.roadId !== road.id && !existing.has(item.id) && endpointDistanceMeters(endpoint, item) <= 35);
|
||||
box.innerHTML = "";
|
||||
if (!candidates.length) { box.textContent = "附近 35 米内没有可新增的驶出道路。请检查 OSM 路口节点,或选择另一条道路。"; return; }
|
||||
const intro = document.createElement("p"); intro.textContent = "选择要新增允许驶入的道路:"; box.append(intro);
|
||||
for (const targetEndpoint of candidates) {
|
||||
const target = state.compiled.model.roads.find((item) => item.id === targetEndpoint.roadId);
|
||||
const button = document.createElement("button"); button.type = "button"; button.textContent = `${turnName(road, target)}:${roadLabel(target)}(相距 ${endpointDistanceMeters(endpoint, targetEndpoint).toFixed(1)} 米)`;
|
||||
button.onclick = () => { stageConnection({ id: `manual:${endpoint.id}:${targetEndpoint.id}`, fromEndpointId: endpoint.id, toEndpointId: targetEndpoint.id }, true); renderConnections(road); };
|
||||
box.append(button);
|
||||
}
|
||||
}
|
||||
|
||||
function turnName(from, to) {
|
||||
const a = heading(from.centerline.at(-2), from.centerline.at(-1));
|
||||
const b = heading(to.centerline[0], to.centerline[1]);
|
||||
@@ -102,6 +129,7 @@ function turnName(from, to) {
|
||||
return delta > 0 ? "右转" : "左转";
|
||||
}
|
||||
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 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; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>道路编译工作台</title><link rel="stylesheet" href="/app.css"></head>
|
||||
<body><header><strong>道路编译工作台</strong><span id="area"></span><span id="status"></span><button id="save">保存修改</button><button id="compile">保存并重新生成</button></header>
|
||||
<main><aside class="issues"><h1>待检查问题</h1><p class="muted">点击问题可定位到道路或路口。</p><ul id="diagnostics"></ul></aside><section class="map"><canvas id="map"></canvas><div class="legend"><i class="reference"></i> osm2streets 参考面 <i class="native"></i> 自研道路面 <i class="line"></i> 道路中心线 <i class="junction"></i> 已识别路口 <i class="warning"></i> 待检查点</div></section><aside class="inspector"><h1>当前道路设置</h1><p id="hint">在地图中点击道路,查看和调整参数。</p><form id="road-form" hidden><label>道路</label><output id="road-name"></output><div id="direction-switch"></div><label>本方向道路宽度(米)<input id="width" type="number" min="1" step="0.1"></label><label>本方向车道数<input id="lanes" type="number" min="1" step="1"></label><label><input id="left" type="checkbox"> 左侧有人行道</label><label><input id="right" type="checkbox"> 右侧有人行道</label><button type="submit">暂存本道路修改</button></form><hr><h2>路口连接</h2><div id="connections">请选择一条道路。</div><details><summary>技术详情与来源</summary><pre id="evidence">无</pre></details></aside></main><script src="/app.js"></script></body></html>
|
||||
<main><aside class="issues"><h1>待检查问题</h1><p class="muted">点击问题可定位到道路或路口。</p><ul id="diagnostics"></ul></aside><section class="map"><canvas id="map"></canvas><div class="legend"><i class="reference"></i> osm2streets 参考面 <i class="native"></i> 自研道路面 <i class="lane"></i> 车道中心线 <i class="connector"></i> 转向路径 <i class="junction"></i> 已识别路口 <i class="warning"></i> 待检查点</div></section><aside class="inspector"><h1>当前道路设置</h1><p id="hint">在地图中点击道路,查看和调整参数。</p><form id="road-form" hidden><label>道路</label><output id="road-name"></output><div id="direction-switch"></div><label>本方向道路宽度(米)<input id="width" type="number" min="1" step="0.1"></label><label>本方向车道数<input id="lanes" type="number" min="1" step="1"></label><label><input id="left" type="checkbox"> 左侧有人行道</label><label><input id="right" type="checkbox"> 右侧有人行道</label><button type="submit">暂存本道路修改</button></form><hr><h2>路口连接</h2><div id="connections">请选择一条道路。</div><button id="add-connection" type="button" hidden>新增驶入道路</button><details><summary>技术详情与来源</summary><pre id="evidence">无</pre></details></aside></main><script src="/app.js"></script></body></html>
|
||||
|
||||
Reference in New Issue
Block a user