feat: add canonical road movements

This commit is contained in:
2026-08-14 10:15:47 +08:00
parent 1876472bf8
commit a556c0fc97
6 changed files with 98 additions and 22 deletions

View File

@@ -203,10 +203,10 @@ 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 connectors = compileConnectors(model, lanes, diagnostics, overrides);
const junctionFeatures = compileJunctionSurfaces(model, lanes, connectors, diagnostics);
validateConnectorContainment(connectors, junctionFeatures, diagnostics);
return { roadSurface: { type: "FeatureCollection", features }, intersectionSurface: { type: "FeatureCollection", features: junctionFeatures }, laneCenterlines: { type: "FeatureCollection", features: lanes.features }, connectors: { type: "FeatureCollection", features: connectors }, 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 };
}
function validateConnectorContainment(connectors, junctionFeatures, diagnostics) {
@@ -265,6 +265,7 @@ function compileLaneCenterlines(model, diagnostics) {
function compileConnectors(model, lanes, diagnostics, overrides) {
const features = [];
const movements = [];
for (const connection of model.connections.filter((item) => item.enabled)) {
const fromRoad = model.roads.find((road) => road.id === endpointRoadId(model, connection.fromEndpointId));
const toRoad = model.roads.find((road) => road.id === endpointRoadId(model, connection.toEndpointId));
@@ -277,17 +278,22 @@ function compileConnectors(model, lanes, diagnostics, overrides) {
const defaultFromLane = fromLanes[index]; const defaultToLane = toLanes[defaultTargetIndex];
const override = laneOverride(overrides, defaultFromLane.id, defaultToLane.id);
if ((!laneAllowsTurn(fromRoad, index, turn) && override?.enabled !== true) || override?.enabled === false) continue;
const targetIndex = defaultTargetIndex;
const from = defaultFromLane.coordinates.at(-1); const to = defaultToLane.coordinates[0];
const control = connectorControlPoint(model, connection, from, to);
const coordinates = quadraticCurve(from, control, to, 12);
const length = lineLengthMeters(coordinates);
if (length < 0.4) continue;
if (length > 80) { diagnostics.push(diagnostic("warning", connection.id, [connection.nodeId], "connector-too-long", "转向路径超过 80 米,已跳过;请检查路口拓扑或人工连接。", from)); continue; }
features.push({ type: "Feature", properties: { native_id: `connector:${connection.id}:${defaultFromLane.id}->${defaultToLane.id}`, connection_id: connection.id, node_id: connection.nodeId, from_lane_id: defaultFromLane.id, to_lane_id: defaultToLane.id, turn, provenance: override ? `override:${override.id}` : connection.provenance }, geometry: { type: "LineString", coordinates } });
const id = `movement:${connection.id}:${defaultFromLane.id}->${defaultToLane.id}`;
const provenance = override ? `override:${override.id}` : connection.provenance;
const connectorId = `connector:${id}`;
const geometryStatus = length < .4 ? "continuous" : length > 80 ? "deferred-too-long" : "connector";
const movement = { id, connectorId, connectionId: connection.id, nodeId: connection.nodeId, fromRoadId: fromRoad.id, toRoadId: defaultToLane.roadId, fromLaneId: defaultFromLane.id, toLaneId: defaultToLane.id, turn, provenance, appliedOverrideIds: override ? [override.id] : [], geometryPublished: geometryStatus === "connector", geometryStatus };
if (length < .4) { movements.push(movement); continue; }
if (length > 80) { diagnostics.push(diagnostic("warning", connection.id, [connection.nodeId], "connector-too-long", "转向路径超过 80 米,未发布几何;请检查路口拓扑或人工连接。", from)); movements.push(movement); continue; }
features.push({ type: "Feature", properties: { native_id: connectorId, movement_id: id, connection_id: connection.id, node_id: connection.nodeId, from_lane_id: defaultFromLane.id, to_lane_id: defaultToLane.id, turn, provenance }, geometry: { type: "LineString", coordinates } });
movements.push(movement);
}
}
return features;
return { features, movements };
}
function laneOverride(overrides, fromLaneId, toLaneId) { return overrides.overrides.find((item) => item.kind === "lane-connection" && item.fromLaneId === fromLaneId && item.toLaneId === toLaneId); }
@@ -347,7 +353,7 @@ function offsetLine(line, offsetMeters) {
function lineLengthMeters(line) { return line.slice(1).reduce((sum, point, index) => { const previous = line[index]; const dx = (point[0] - previous[0]) * 111320 * Math.cos(point[1] * Math.PI / 180); const dy = (point[1] - previous[1]) * 111320; return sum + Math.hypot(dx, dy); }, 0); }
function compileJunctionSurfaces(model, lanes, connectors, diagnostics) {
function compileJunctionSurfaces(model, lanes, connectors, movements, diagnostics) {
const byNode = new Map();
for (const endpoint of model.endpoints) {
if (!byNode.has(endpoint.nodeId)) byNode.set(endpoint.nodeId, []);
@@ -362,7 +368,8 @@ function compileJunctionSurfaces(model, lanes, connectors, diagnostics) {
const cutbackMeters = Math.max(...approaches.map((approach) => approach.widthMeters)) * 1.4;
const boundary = junctionBoundary(approaches, node, cutbackMeters);
const junctionConnectors = connectors.filter((feature) => feature.properties.node_id === nodeId);
if (boundary.length < 3 || !junctionConnectors.length) {
const junctionMovements = movements.filter((movement) => movement.nodeId === nodeId);
if (boundary.length < 3 || !junctionMovements.length) {
diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-surface-deferred", "路口缺少足够的截面或转向路径,暂不生成路口面。", node));
continue;
}
@@ -377,7 +384,7 @@ function compileJunctionSurfaces(model, lanes, connectors, diagnostics) {
diagnostics.push(diagnostic("error", `junction:node/${nodeId}`, [nodeId], "invalid-junction-surface", "路口截面边界发生自相交,未发布路口面。请检查道路方向或路口拓扑。", node));
continue;
}
result.push({ type: "Feature", properties: { native_id: `junction:node/${nodeId}`, osm_node_id: nodeId, kind: wayIds.size === 3 ? "t" : "cross", source_road_ids: approaches.flatMap((approach) => approach.roadIds).join(","), cutback_m: cutbackMeters, connector_count: junctionConnectors.length, boundary_mode: boundaryMode, rule: "junction-approach-envelope/v2" }, geometry: { type: "Polygon", coordinates: [ring] } });
result.push({ type: "Feature", properties: { native_id: `junction:node/${nodeId}`, osm_node_id: nodeId, kind: wayIds.size === 3 ? "t" : "cross", source_road_ids: approaches.flatMap((approach) => approach.roadIds).join(","), cutback_m: cutbackMeters, movement_count: junctionMovements.length, connector_count: junctionConnectors.length, boundary_mode: boundaryMode, rule: "junction-approach-envelope/v2" }, geometry: { type: "Polygon", coordinates: [ring] } });
diagnostics.push(diagnostic("info", `junction:node/${nodeId}`, [nodeId], "ordinary-junction-surface", "已按道路截面与转向路径生成普通路口面。", node));
}
return result;