feat: add reimport stage for hand-edited GeoPackages
QGIS 手工修正后的回导流程此前是 README 里的三段 shell:一个硬编码
area-id 和 ogr2ogr 绝对路径的 for 循环、一段内联 node heredoc、再加一次
npm run build。改为一个 reimport 阶段:
npm run build -- --config config/areas/<area-id>.json \
--stages reimport,blender,cesium
scripts/reimport-gpkg.js 先把全部图层导出到临时目录并逐个校验,全部通过
才写回 osm2streets_web_out/。ogr2ogr 对不存在的图层退出码非 0 但仍会留下
0 字节文件,原先逐图层 mv 会静默用空图层覆盖好数据。
intermediates 与 reimport 同时指定直接报错——前者用 OSM 重建 GeoPackage,
正好抹掉后者要读回的手工修改。reimport 不含在 all 中。
同时新增 scripts/lib/scene-layers.js 作为 9 个渲染图层的唯一定义源。此前
该表在合并场景、场景样式 JSON、生成的 QGIS 工程、README 手工流程中各有
一份副本,改一处漏其余会导致图层叠放顺序出错并流入 Blender/Cesium。
验证:同一 OSM 输入下,改动前后 intermediates 产物逐字节一致
(scene_style.json、.qgz 符号定义、9 个图层几何与属性);reimport 对
未修改图层无损回导。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,15 @@ const path = require("path");
|
||||
const os = require("os");
|
||||
const { execFileSync } = require("child_process");
|
||||
const { JsStreetNetwork } = require("osm2streets-js-node");
|
||||
const {
|
||||
SCENE_LAYERS,
|
||||
SCENE_FILE,
|
||||
SCENE_STYLE_FILE,
|
||||
layerFile,
|
||||
mergeScene,
|
||||
sceneStyle,
|
||||
qgisRgba,
|
||||
} = require("./lib/scene-layers");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
@@ -79,22 +88,16 @@ const split = splitLayers(
|
||||
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);
|
||||
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);
|
||||
for (const layer of SCENE_LAYERS) {
|
||||
writeJson(path.join(outDir, layerFile(layer)), split[layer.splitKey]);
|
||||
}
|
||||
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));
|
||||
writeJson(path.join(outDir, SCENE_FILE), mergeScene((layer) => split[layer.splitKey]));
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, "osm2streets_scene_style.json"),
|
||||
path.join(outDir, SCENE_STYLE_FILE),
|
||||
JSON.stringify(sceneStyle(), null, 2),
|
||||
);
|
||||
|
||||
@@ -102,15 +105,10 @@ if (fs.existsSync(gpkgPath)) {
|
||||
fs.unlinkSync(gpkgPath);
|
||||
}
|
||||
const ogrEnv = qgisEnv();
|
||||
importLayer(gpkgPath, path.join(outDir, "road_surface.geojson"), "road_surface", false, ogrEnv);
|
||||
importLayer(gpkgPath, path.join(outDir, "intersection_surface.geojson"), "intersection_surface", true, ogrEnv);
|
||||
importLayer(gpkgPath, path.join(outDir, "sidewalks.geojson"), "sidewalks", true, ogrEnv);
|
||||
importLayer(gpkgPath, path.join(outDir, "sidewalk_corners.geojson"), "sidewalk_corners", true, ogrEnv);
|
||||
importLayer(gpkgPath, path.join(outDir, "lane_separators.geojson"), "lane_separators", true, ogrEnv);
|
||||
importLayer(gpkgPath, path.join(outDir, "center_lines.geojson"), "center_lines", true, ogrEnv);
|
||||
importLayer(gpkgPath, path.join(outDir, "vehicle_stop_lines.geojson"), "vehicle_stop_lines", true, ogrEnv);
|
||||
importLayer(gpkgPath, path.join(outDir, "lane_arrows_webscale.geojson"), "lane_arrows_webscale", true, ogrEnv);
|
||||
importLayer(gpkgPath, path.join(outDir, "crosswalks.geojson"), "crosswalks", true, ogrEnv);
|
||||
// First layer creates the GeoPackage; the rest append into it.
|
||||
SCENE_LAYERS.forEach((layer, index) => {
|
||||
importLayer(gpkgPath, path.join(outDir, layerFile(layer)), layer.id, index > 0, ogrEnv);
|
||||
});
|
||||
|
||||
const qgisScript = path.join(outDir, "_create_qgis_project.py");
|
||||
const previewFeature = split.crosswalks.features[0] || split.laneArrows.features[0] || split.roadSurface.features[0];
|
||||
@@ -406,53 +404,6 @@ 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, 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"));
|
||||
@@ -1360,6 +1311,16 @@ function importLayer(gpkg, source, layerName, update, env) {
|
||||
execFileSync(ogr2ogr, args, { stdio: "inherit", env: { ...process.env, ...env } });
|
||||
}
|
||||
|
||||
function qgisLayerSpecs() {
|
||||
return SCENE_LAYERS.map((layer) => ({
|
||||
id: layer.id,
|
||||
title: layer.title,
|
||||
fill: qgisRgba(layer.fill),
|
||||
outline: qgisRgba(layer.outline, layer.outlineAlpha ?? 255),
|
||||
outlineWidth: String(layer.outlineWidth),
|
||||
}));
|
||||
}
|
||||
|
||||
function makeQgisScript(options) {
|
||||
return `
|
||||
from pathlib import Path
|
||||
@@ -1384,6 +1345,7 @@ PROJECT_PATH = ${JSON.stringify(options.projectPath)}
|
||||
PREVIEW_PATH = ${JSON.stringify(options.previewPath)}
|
||||
PREVIEW_EXTENT = [${options.previewExtent.split(",").map(Number).join(", ")}]
|
||||
LAYER_PREFIX = ${JSON.stringify(options.layerPrefix || "osm2streets")}
|
||||
LAYER_SPECS = ${JSON.stringify(qgisLayerSpecs(), null, 4)}
|
||||
|
||||
def fill_symbol(color, outline="0,0,0,0", outline_width="0"):
|
||||
return QgsFillSymbol.createSimple({
|
||||
@@ -1412,17 +1374,16 @@ project.setCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
|
||||
project.setPresetHomePath(str(Path(PROJECT_PATH).parent))
|
||||
|
||||
layers = {
|
||||
"road_surface": make_layer("road_surface", f"{LAYER_PREFIX} road surface", "43,43,40,255", "30,30,28,255", "0.04"),
|
||||
"intersection_surface": make_layer("intersection_surface", f"{LAYER_PREFIX} intersection surface", "43,43,40,255", "30,30,28,255", "0.04"),
|
||||
"sidewalks": make_layer("sidewalks", f"{LAYER_PREFIX} sidewalks", "190,190,182,255", "156,156,148,255", "0.025"),
|
||||
"sidewalk_corners": make_layer("sidewalk_corners", f"{LAYER_PREFIX} sidewalk corners", "190,190,182,255", "156,156,148,255", "0.025"),
|
||||
"crosswalks": make_layer("crosswalks", f"{LAYER_PREFIX} crosswalks", "255,255,246,255"),
|
||||
"lane_separators": make_layer("lane_separators", f"{LAYER_PREFIX} lane separators", "238,238,230,255"),
|
||||
"center_lines": make_layer("center_lines", f"{LAYER_PREFIX} center lines", "245,190,42,255"),
|
||||
"vehicle_stop_lines": make_layer("vehicle_stop_lines", f"{LAYER_PREFIX} vehicle stop lines", "255,255,246,255"),
|
||||
"lane_arrows": make_layer("lane_arrows_webscale", f"{LAYER_PREFIX} lane arrows", "255,255,246,255", "43,43,40,200", "0.015"),
|
||||
spec["id"]: make_layer(
|
||||
spec["id"],
|
||||
f"{LAYER_PREFIX} {spec['title']}",
|
||||
spec["fill"],
|
||||
spec["outline"],
|
||||
spec["outlineWidth"],
|
||||
)
|
||||
for spec in LAYER_SPECS
|
||||
}
|
||||
draw_order = ["road_surface", "intersection_surface", "sidewalks", "sidewalk_corners", "lane_separators", "center_lines", "crosswalks", "vehicle_stop_lines", "lane_arrows"]
|
||||
draw_order = [spec["id"] for spec in LAYER_SPECS]
|
||||
for key in draw_order:
|
||||
project.addMapLayer(layers[key], False)
|
||||
root = project.layerTreeRoot()
|
||||
|
||||
Reference in New Issue
Block a user