成果暂存
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user