成果暂存

This commit is contained in:
2026-07-24 16:03:15 +08:00
parent 662f8d908b
commit fb2a36ac7e
32 changed files with 198341 additions and 12 deletions

View File

@@ -69,6 +69,11 @@ writeJson(path.join(outDir, "vehicle_stop_lines.geojson"), split.vehicleStopLine
writeJson(path.join(outDir, "lane_arrows_webscale.geojson"), split.laneArrows);
writeJson(path.join(outDir, "sidewalk_corners.geojson"), split.sidewalkCorners);
writeJson(path.join(outDir, "crosswalks.geojson"), split.crosswalks);
writeJson(path.join(outDir, "osm2streets_scene.geojson"), mergedScene(split));
fs.writeFileSync(
path.join(outDir, "osm2streets_scene_style.json"),
JSON.stringify(sceneStyle(), null, 2),
);
if (fs.existsSync(gpkgPath)) {
fs.unlinkSync(gpkgPath);
@@ -206,6 +211,23 @@ function qgisEnv() {
}
function getOsmBounds(xmlText) {
// OSM exports may contain distant relation members (for example subway
// nodes) that are not part of the requested map extent. Prefer the explicit
// bounds element when present and only fall back to node extents for files
// that do not provide one.
const boundsMatch = xmlText.match(/<bounds\b([^>]*)\/?\s*>/);
if (boundsMatch) {
const attrs = parseAttrs(boundsMatch[1]);
const values = {
minLon: Number(attrs.minlon),
minLat: Number(attrs.minlat),
maxLon: Number(attrs.maxlon),
maxLat: Number(attrs.maxlat),
};
if (Object.values(values).every(Number.isFinite)) {
return values;
}
}
let minLon = Infinity;
let minLat = Infinity;
let maxLon = -Infinity;
@@ -357,6 +379,53 @@ function emptyCollection() {
return { type: "FeatureCollection", features: [] };
}
function mergedScene(split) {
const layers = [
["road_surface", 10, split.roadSurface],
["intersection_surface", 20, split.intersectionSurface],
["sidewalks", 30, split.sidewalks],
["sidewalk_corners", 40, split.sidewalkCorners],
["lane_separators", 50, split.laneSeparators],
["center_lines", 60, split.centerLines],
["crosswalks", 70, split.crosswalks],
["vehicle_stop_lines", 80, split.vehicleStopLines],
["lane_arrows_webscale", 90, split.laneArrows],
];
return {
type: "FeatureCollection",
features: layers.flatMap(([renderLayer, zIndex, collection]) => (
(collection.features || []).map((feature) => ({
...feature,
properties: {
...(feature.properties || {}),
render_layer: renderLayer,
z_index: zIndex,
},
}))
)),
};
}
function sceneStyle() {
return {
version: 1,
geometry: "polygon",
sortProperty: "z_index",
layerProperty: "render_layer",
layers: [
{ id: "road_surface", zIndex: 10, fill: "#2b2b28", outline: "#1e1e1c", outlineWidth: 0.04 },
{ id: "intersection_surface", zIndex: 20, fill: "#2b2b28", outline: "#1e1e1c", outlineWidth: 0.04 },
{ id: "sidewalks", zIndex: 30, fill: "#bebeb6", outline: "#9c9c94", outlineWidth: 0.025 },
{ id: "sidewalk_corners", zIndex: 40, fill: "#bebeb6", outline: "#9c9c94", outlineWidth: 0.025 },
{ id: "lane_separators", zIndex: 50, fill: "#eeeee6", outline: null, outlineWidth: 0 },
{ id: "center_lines", zIndex: 60, fill: "#f5be2a", outline: null, outlineWidth: 0 },
{ id: "crosswalks", zIndex: 70, fill: "#fffff6", outline: null, outlineWidth: 0 },
{ id: "vehicle_stop_lines", zIndex: 80, fill: "#fffff6", outline: null, outlineWidth: 0 },
{ id: "lane_arrows_webscale", zIndex: 90, fill: "#fffff6", outline: "#2b2b28", outlineWidth: 0.015 },
],
};
}
function splitLayers(dir, arrowScaleValue, osm) {
const plain = JSON.parse(fs.readFileSync(path.join(dir, "plain.geojson"), "utf8"));
const lanePolygons = JSON.parse(fs.readFileSync(path.join(dir, "lane_polygons.geojson"), "utf8"));
@@ -410,6 +479,7 @@ function splitLayers(dir, arrowScaleValue, osm) {
}
if (type === "lane arrow" && !conflictsWithCrosswalk && !isInAnyPolygon(feature, serviceDrivingPolygons)) out.laneArrows.features.push(scaleFeature(feature, arrowScaleValue));
}
return out;
}
@@ -497,11 +567,7 @@ function pointInRing([x, y], ring) {
}
function buildCrosswalks(osm) {
const crossingNodes = [...osm.nodes.values()].filter((node) => {
if (node.tags.highway !== "crossing") return false;
const markings = node.tags["crossing:markings"];
return !(markings && ["no", "none", "unmarked"].includes(markings));
});
const crossingNodes = markedCrossingNodes(osm);
const clusterCenters = crossingClusters(crossingNodes);
const stripes = emptyCollection();
const stopLines = emptyCollection();
@@ -527,7 +593,15 @@ function buildCrosswalks(osm) {
return { stripes, stopLines, zones, stopLineExclusionZones };
}
function crossingClusters(nodes) {
function markedCrossingNodes(osm) {
return [...osm.nodes.values()].filter((node) => {
if (node.tags.highway !== "crossing") return false;
const markings = node.tags["crossing:markings"];
return !(markings && ["no", "none", "unmarked"].includes(markings));
});
}
function crossingNodeClusters(nodes) {
const clusters = [];
for (const node of nodes) {
let cluster = clusters.find((candidate) => {
@@ -540,6 +614,11 @@ function crossingClusters(nodes) {
}
cluster.push(node);
}
return clusters;
}
function crossingClusters(nodes) {
const clusters = crossingNodeClusters(nodes);
const centers = new Map();
for (const cluster of clusters) {
if (cluster.length < 2) continue;
@@ -636,10 +715,13 @@ function syntheticStopLine(node, way, roadUnit, acrossUnit, stripeLength, crossw
const offset = stripeLength / 2 + 1.2;
const candidateA = addMeters([node.lon, node.lat], roadUnit, offset, meters);
const candidateB = addMeters([node.lon, node.lat], roadUnit, -offset, meters);
const stopCenter = pointDistanceMeters(candidateA, intersectionCenter, meters) >= pointDistanceMeters(candidateB, intersectionCenter, meters)
? candidateA
: candidateB;
const coords = rectangleMeters(stopCenter, acrossUnit, roadUnit, crosswalkWidth + 0.8, 0.45, meters);
const stopSide = pointDistanceMeters(candidateA, intersectionCenter, meters) >= pointDistanceMeters(candidateB, intersectionCenter, meters)
? 1
: -1;
const stopCenterOnRoad = stopSide === 1 ? candidateA : candidateB;
const placement = stopLineLanePlacement(way, stripeLength, stopSide);
const stopCenter = addMeters(stopCenterOnRoad, acrossUnit, placement.acrossOffset, meters);
const coords = rectangleMeters(stopCenter, acrossUnit, roadUnit, placement.length, 0.45, meters);
return {
type: "Feature",
properties: {
@@ -647,13 +729,39 @@ function syntheticStopLine(node, way, roadUnit, acrossUnit, stripeLength, crossw
source: "crosswalk",
crossing_node_id: node.id,
highway: way?.tags.highway || null,
stop_side: stopSide === 1 ? "with_way_outside" : "against_way_outside",
},
geometry: { type: "Polygon", coordinates: [coords] },
};
}
function pointDistanceMeters([lon, lat], point, meters) {
return Math.hypot((lon - point.lon) * meters.lon, (lat - point.lat) * meters.lat);
function pointDistanceMeters(pointA, point, meters) {
const [lon, lat] = Array.isArray(pointA) ? pointA : [pointA.lon, pointA.lat];
const px = Array.isArray(point) ? point[0] : point.lon;
const py = Array.isArray(point) ? point[1] : point.lat;
return Math.hypot((lon - px) * meters.lon, (lat - py) * meters.lat);
}
function stopLineLanePlacement(way, roadWidth, stopSide) {
if (isOneway(way)) {
return { length: Math.max(2.8, roadWidth - 1.4), acrossOffset: 0 };
}
const halfWidth = roadWidth / 2;
const innerGap = 0.15;
const outerGap = 0.8;
const laneWidth = Math.max(2.4, halfWidth - innerGap - outerGap);
// China uses right-hand traffic. If the stop line is on the +roadUnit side,
// the approaching traffic direction is -roadUnit, whose right side is +acrossUnit.
const laneSide = stopSide === 1 ? 1 : -1;
return {
length: laneWidth,
acrossOffset: laneSide * (innerGap + laneWidth / 2),
};
}
function isOneway(way) {
const value = String(way?.tags.oneway || "").toLowerCase();
return ["yes", "true", "1"].includes(value);
}
function crosswalkLengthMeters(way) {