feat: improve native road diagnostics and review
This commit is contained in:
@@ -57,7 +57,7 @@ function compileRoadModel(xml, overrides) {
|
||||
for (const [nodeId, items] of byNode) {
|
||||
if (items.length === 1 && distanceToExtentEdgeMeters(items[0].coordinate, extent) > 25) {
|
||||
const endpoint = items[0];
|
||||
diagnostics.push({ ...diagnostic("warning", endpoint.roadId, [nodeId], "unconnected-interior-road-end", "道路在区域内部结束,未连接到其他机动车道路。请确认这是实际断头,还是 OSM 节点尚未连接。", endpoint.coordinate), endpointId: endpoint.id });
|
||||
diagnostics.push({ ...diagnostic("warning", endpoint.roadId, [nodeId], "unconnected-interior-road-end", "道路在区域内部结束,未连接到其他机动车道路。请确认这是实际断头,还是 OSM 节点尚未连接。", endpoint.coordinate), endpointId: endpoint.id, manualCandidates: nearbyManualCandidates(endpoints, endpoint) });
|
||||
}
|
||||
}
|
||||
return { schema: "native-road-model/v1", roads, endpoints, connections, diagnostics };
|
||||
@@ -177,13 +177,17 @@ function sameOsmWay(endpoints, firstRoadId, secondRoadId) {
|
||||
function connectionEndpointsCompatible(model, fromId, toId) {
|
||||
const from = model.endpoints.find((endpoint) => endpoint.id === fromId);
|
||||
const to = model.endpoints.find((endpoint) => endpoint.id === toId);
|
||||
if (!from || !to || from.roadId === to.roadId || from.side !== "end" || to.side !== "start") return false;
|
||||
if (!from || !to || from.roadId === to.roadId || sameOsmWay(model.endpoints, 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 nearbyManualCandidates(endpoints, from) {
|
||||
return endpoints.filter((to) => to.side === "start" && to.roadId !== from.roadId && !sameOsmWay(endpoints, from.roadId, to.roadId)).map((to) => ({ to, distanceMeters: distanceMeters(from.coordinate, to.coordinate) })).filter((item) => item.distanceMeters <= 35).sort((a, b) => a.distanceMeters - b.distanceMeters).slice(0, 3).map(({ to, distanceMeters: meters }) => ({ toEndpointId: to.id, roadId: to.roadId, distanceMeters: Math.round(meters * 10) / 10 }));
|
||||
}
|
||||
|
||||
function compileGeometry(model, overrides = { overrides: [] }) {
|
||||
const diagnostics = [...model.diagnostics];
|
||||
const features = [];
|
||||
@@ -354,36 +358,53 @@ function compileJunctionSurfaces(model, lanes, connectors, diagnostics) {
|
||||
const wayIds = new Set(endpoints.map((endpoint) => endpoint.roadId.split(":")[1]));
|
||||
if (wayIds.size < 3 || wayIds.size > 4) continue;
|
||||
const node = endpoints[0].coordinate;
|
||||
const roads = endpoints.map((endpoint) => model.roads.find((road) => road.id === endpoint.roadId));
|
||||
const cutbackMeters = Math.max(...roads.map((road) => road.widthMeters)) * 1.4;
|
||||
const boundary = junctionBoundary(model, endpoints, node, cutbackMeters);
|
||||
const approaches = junctionApproaches(model, endpoints);
|
||||
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) {
|
||||
diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-surface-deferred", "路口缺少足够的截面或转向路径,暂不生成路口面。", node));
|
||||
continue;
|
||||
}
|
||||
const envelope = convexHull([...boundary, ...junctionConnectors.flatMap((feature) => feature.geometry.coordinates)]);
|
||||
const ring = [...envelope, envelope[0]];
|
||||
let ring = [...boundary, boundary[0]];
|
||||
let boundaryMode = "approach-envelope";
|
||||
if (hasSelfIntersection(ring) || !junctionConnectors.every((feature) => feature.geometry.coordinates.every((point) => pointInPolygon(point, ring)))) {
|
||||
const envelope = convexHull([...boundary, ...junctionConnectors.flatMap((feature) => feature.geometry.coordinates)]);
|
||||
ring = [...envelope, envelope[0]];
|
||||
boundaryMode = "connector-convex-fallback";
|
||||
}
|
||||
if (hasSelfIntersection(ring)) {
|
||||
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: [...new Set(roads.map((road) => road.id))].join(","), cutback_m: cutbackMeters, connector_count: junctionConnectors.length, rule: "junction-cutback-envelope/v1" }, 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, 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;
|
||||
}
|
||||
|
||||
function junctionBoundary(model, endpoints, node, cutbackMeters) {
|
||||
const points = [];
|
||||
function junctionApproaches(model, endpoints) {
|
||||
const groups = new Map();
|
||||
for (const endpoint of endpoints) {
|
||||
const road = model.roads.find((item) => item.id === endpoint.roadId);
|
||||
if (!road) continue;
|
||||
const line = endpoint.side === "end" ? [...road.centerline].reverse() : road.centerline;
|
||||
const cutback = pointAlongLine(line, cutbackMeters);
|
||||
const key = road.osmWayIds.join(",");
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push({ endpoint, road });
|
||||
}
|
||||
return [...groups.values()].map((directions) => {
|
||||
const { endpoint, road } = directions[0];
|
||||
return { line: endpoint.side === "end" ? [...road.centerline].reverse() : road.centerline, roadIds: directions.map((item) => item.road.id), widthMeters: directions.reduce((sum, item) => sum + item.road.widthMeters, 0) };
|
||||
});
|
||||
}
|
||||
|
||||
function junctionBoundary(approaches, node, cutbackMeters) {
|
||||
const points = [];
|
||||
for (const approach of approaches) {
|
||||
const cutback = pointAlongLine(approach.line, cutbackMeters);
|
||||
if (!cutback) continue;
|
||||
const heading = headingAtEndpoint(line);
|
||||
const half = road.widthMeters / 2;
|
||||
const heading = headingAtEndpoint(approach.line);
|
||||
const half = approach.widthMeters / 2;
|
||||
points.push(offsetCoordinate(cutback, heading + 90, half));
|
||||
points.push(offsetCoordinate(cutback, heading - 90, half));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user