fix: persist and render native sidewalks

This commit is contained in:
2026-08-14 10:53:20 +08:00
parent f43a122ff7
commit e1f3fc10ca
7 changed files with 62 additions and 9 deletions

View File

@@ -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 }; }