feat: osm2streets上游箭头修复

This commit is contained in:
2026-07-28 09:08:58 +08:00
parent 694f18826a
commit 96c9baf540
6 changed files with 279 additions and 5 deletions

View File

@@ -14,12 +14,16 @@ const qgisApp = config.qgisApp;
const qgisMacOS = path.join(qgisApp, "Contents", "MacOS");
const qgisPython = path.join(qgisMacOS, "python3.12");
const ogr2ogr = path.join(qgisMacOS, "ogr2ogr");
const normalizeLaneArrowsScript = path.join(repoRoot, "scripts", "normalize-lane-arrows.py");
const inputPath = path.resolve(config.input);
const outDir = path.resolve(config.outDir);
const gpkgPath = path.resolve(config.gpkg);
const projectPath = path.resolve(config.project);
const previewPath = path.resolve(config.preview);
const arrowScale = Number(config.arrowScale);
const arrowMergeTriangles = config.arrowMergeTriangles !== false;
const arrowOutlineSimplifyMeters = Number(config.arrowOutlineSimplifyMeters ?? 0.05);
const intersectionCornerSourceMaxDimensionMeters = Number(config.intersectionCornerSourceMaxDimensionMeters ?? 2.6);
const clipPad = Number(config.clipPad);
const canvasPad = Number(config.canvasPad);
const previewPad = Number(config.previewPad);
@@ -28,6 +32,12 @@ const layerPrefix = config.layerPrefix || "osm2streets";
if (!Number.isFinite(arrowScale) || arrowScale <= 0) {
throw new Error(`Invalid arrowScale: ${config.arrowScale}`);
}
if (!Number.isFinite(arrowOutlineSimplifyMeters) || arrowOutlineSimplifyMeters < 0) {
throw new Error(`Invalid arrowOutlineSimplifyMeters: ${config.arrowOutlineSimplifyMeters}`);
}
if (!Number.isFinite(intersectionCornerSourceMaxDimensionMeters) || intersectionCornerSourceMaxDimensionMeters <= 0) {
throw new Error(`Invalid intersectionCornerSourceMaxDimensionMeters: ${config.intersectionCornerSourceMaxDimensionMeters}`);
}
for (const [key, value] of [["clipPad", clipPad], ["canvasPad", canvasPad], ["previewPad", previewPad]]) {
if (!Number.isFinite(value) || value < 0) {
throw new Error(`Invalid ${key}: ${config[key]}`);
@@ -41,6 +51,9 @@ for (const exe of [ogr2ogr, qgisPython]) {
throw new Error(`QGIS executable not found: ${exe}`);
}
}
if (!fs.existsSync(normalizeLaneArrowsScript)) {
throw new Error(`Lane-arrow normalizer not found: ${normalizeLaneArrowsScript}`);
}
fs.mkdirSync(outDir, { recursive: true });
fs.mkdirSync(path.dirname(gpkgPath), { recursive: true });
@@ -60,7 +73,12 @@ writeGeoJson(outDir, "lane_markings.geojson", network.toLaneMarkingsGeojson());
writeGeoJson(outDir, "intersection_markings.geojson", network.toIntersectionMarkingsGeojson());
fs.writeFileSync(path.join(outDir, "network.json"), network.toJson());
const split = splitLayers(outDir, arrowScale, osm);
const split = splitLayers(
outDir,
arrowScale,
intersectionCornerSourceMaxDimensionMeters,
osm,
);
writeJson(path.join(outDir, "road_surface.geojson"), split.roadSurface);
writeJson(path.join(outDir, "intersection_surface.geojson"), split.intersectionSurface);
writeJson(path.join(outDir, "sidewalks.geojson"), split.sidewalks);
@@ -68,6 +86,10 @@ writeJson(path.join(outDir, "lane_separators.geojson"), split.laneSeparators);
writeJson(path.join(outDir, "center_lines.geojson"), split.centerLines);
writeJson(path.join(outDir, "vehicle_stop_lines.geojson"), split.vehicleStopLines);
writeJson(path.join(outDir, "lane_arrows_webscale.geojson"), split.laneArrows);
if (arrowMergeTriangles) {
normalizeLaneArrows(path.join(outDir, "lane_arrows_webscale.geojson"), arrowOutlineSimplifyMeters);
split.laneArrows = JSON.parse(fs.readFileSync(path.join(outDir, "lane_arrows_webscale.geojson"), "utf8"));
}
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));
@@ -161,6 +183,9 @@ function loadConfig(file, cliArgs) {
project: "project",
preview: "preview",
arrowScale: "arrowScale",
arrowMergeTriangles: "arrowMergeTriangles",
arrowOutlineSimplifyMeters: "arrowOutlineSimplifyMeters",
intersectionCornerSourceMaxDimensionMeters: "intersectionCornerSourceMaxDimensionMeters",
clipPad: "clipPad",
pad: "clipPad",
canvasPad: "canvasPad",
@@ -428,7 +453,7 @@ function sceneStyle() {
};
}
function splitLayers(dir, arrowScaleValue, osm) {
function splitLayers(dir, arrowScaleValue, maxCornerDimensionMeters, 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"));
const markings = JSON.parse(fs.readFileSync(path.join(dir, "lane_markings.geojson"), "utf8"));
@@ -445,7 +470,7 @@ function splitLayers(dir, arrowScaleValue, osm) {
centerLines: emptyCollection(),
vehicleStopLines: crosswalkData.stopLines,
laneArrows: emptyCollection(),
sidewalkCorners: intersections,
sidewalkCorners: filteredSidewalkCorners(intersections, maxCornerDimensionMeters),
crosswalks: crosswalkData.stripes,
};
const serviceDrivingPolygons = [];
@@ -455,7 +480,6 @@ function splitLayers(dir, arrowScaleValue, osm) {
out.intersectionSurface.features.push(feature);
}
}
for (const feature of lanePolygons.features || []) {
const type = feature.properties?.type;
if (type === "Sidewalk" || type === "Footway") {
@@ -479,7 +503,9 @@ function splitLayers(dir, arrowScaleValue, osm) {
if (type === "vehicle stop line" && !isInAnyPolygon(feature, crosswalkData.stopLineExclusionZones, "intersects")) {
out.vehicleStopLines.features.push(feature);
}
if (type === "lane arrow" && !conflictsWithCrosswalk && !isInAnyPolygon(feature, serviceDrivingPolygons)) out.laneArrows.features.push(scaleFeature(feature, arrowScaleValue));
if (type === "lane arrow" && !conflictsWithCrosswalk && !isInAnyPolygon(feature, serviceDrivingPolygons)) {
out.laneArrows.features.push(scaleFeature(feature, arrowScaleValue));
}
}
return out;
@@ -490,6 +516,30 @@ function hasAnyWayId(value, ids) {
return values.some((id) => ids.has(Number(id)));
}
function filteredSidewalkCorners(intersections, maxDimensionMeters) {
const out = emptyCollection();
for (const feature of intersections.features || []) {
if (feature.properties?.type !== "sidewalk corner") continue;
const dimension = maxFeatureDimensionMeters(feature);
if (dimension === null || dimension > maxDimensionMeters) continue;
out.features.push(feature);
}
return out;
}
function maxFeatureDimensionMeters(feature) {
const points = [];
collectCoords(feature.geometry?.coordinates, points);
if (points.length === 0) return null;
const lat = points.reduce((sum, point) => sum + point[1], 0) / points.length;
const meters = metersForLat(lat);
const xs = points.map((point) => point[0]);
const ys = points.map((point) => point[1]);
const width = (Math.max(...xs) - Math.min(...xs)) * meters.lon;
const height = (Math.max(...ys) - Math.min(...ys)) * meters.lat;
return Math.max(width, height);
}
function polygonRings(geometry) {
if (!geometry?.coordinates) return [];
if (geometry.type === "Polygon") return [geometry.coordinates];
@@ -846,6 +896,26 @@ function scaleCoords(obj, cx, cy, scale) {
return obj;
}
function normalizeLaneArrows(geojsonPath, outlineSimplifyMeters) {
execFileSync(qgisPython, [
normalizeLaneArrowsScript,
"--input", geojsonPath,
"--outline-simplify-meters", String(outlineSimplifyMeters),
], {
stdio: "inherit",
env: {
...process.env,
...qgisEnv(),
QT_QPA_PLATFORM: "offscreen",
PYTHONHOME: path.join(qgisApp, "Contents", "Frameworks"),
PYTHONPATH: [
path.join(qgisApp, "Contents", "Resources", "python"),
path.join(qgisApp, "Contents", "Resources", "python", "plugins"),
].join(path.delimiter),
},
});
}
function importLayer(gpkg, source, layerName, update, env) {
const args = ["-f", "GPKG"];
if (update) args.push("-update", "-overwrite");