feat: import native road packages from ZIP
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "fengshu-er-road",
|
"id": "fengshu-er-road",
|
||||||
"input": "/Users/que01/osm2streets-qgis-workflow/inputs/osm/枫树二路.osm",
|
"input": "/Users/que01/osm2streets-qgis-workflow/inputs/osm/枫树二路.osm",
|
||||||
|
"nativeRoadPackage": "../../inputs/native-road-packages/fengshu-er-road.zip",
|
||||||
"outputRoot": "/Users/que01/osm2streets-qgis-workflow/outputs",
|
"outputRoot": "/Users/que01/osm2streets-qgis-workflow/outputs",
|
||||||
"qgisApp": "/Applications/QGIS.app",
|
"qgisApp": "/Applications/QGIS.app",
|
||||||
"blenderApp": "/Applications/Blender.app",
|
"blenderApp": "/Applications/Blender.app",
|
||||||
@@ -24,31 +25,6 @@
|
|||||||
"turnLaneArrows": {
|
"turnLaneArrows": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
"nativeRoad": {
|
|
||||||
"edgeLines": false,
|
|
||||||
"junctionTemplates": {
|
|
||||||
"enabled": true,
|
|
||||||
"references": [],
|
|
||||||
"clusters": [
|
|
||||||
{
|
|
||||||
"id": "zhushanhu-fengshu-complex",
|
|
||||||
"template": "complex-junction-v1",
|
|
||||||
"referenceFile": "../../inputs/osm/珠山湖大道(枫树二路)口.geojson",
|
|
||||||
"approachWidthMultiplier": 1.45,
|
|
||||||
"approachLengthMeters": 32,
|
|
||||||
"coreRadiusMeters": 28,
|
|
||||||
"cornerRadiusMeters": 25,
|
|
||||||
"outerRadiusExtraMeters": 24,
|
|
||||||
"nodeIds": [
|
|
||||||
"8005332807",
|
|
||||||
"8024512135",
|
|
||||||
"8024512145",
|
|
||||||
"8024512147"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"osm2streets": {
|
"osm2streets": {
|
||||||
"debug_each_step": false,
|
"debug_each_step": false,
|
||||||
"dual_carriageway_experiment": false,
|
"dual_carriageway_experiment": false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "nantaizi-lake-innovation-valley",
|
"id": "nantaizi-lake-innovation-valley",
|
||||||
"input": "/Users/que01/Desktop/南台子湖创新谷OSM.osm",
|
"input": "/Users/que01/Desktop/南台子湖创新谷OSM.osm",
|
||||||
|
"nativeRoadPackage": "../../inputs/native-road-packages/nantaizi-lake-innovation-valley.zip",
|
||||||
"outputRoot": "/Users/que01/osm2streets-qgis-workflow/outputs",
|
"outputRoot": "/Users/que01/osm2streets-qgis-workflow/outputs",
|
||||||
"qgisApp": "/Applications/QGIS.app",
|
"qgisApp": "/Applications/QGIS.app",
|
||||||
"blenderApp": "/Applications/Blender.app",
|
"blenderApp": "/Applications/Blender.app",
|
||||||
@@ -24,9 +25,6 @@
|
|||||||
"turnLaneArrows": {
|
"turnLaneArrows": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"nativeRoad": {
|
|
||||||
"edgeLines": false
|
|
||||||
},
|
|
||||||
"osm2streets": {
|
"osm2streets": {
|
||||||
"debug_each_step": false,
|
"debug_each_step": false,
|
||||||
"dual_carriageway_experiment": false,
|
"dual_carriageway_experiment": false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "my-area",
|
"id": "my-area",
|
||||||
"input": "/absolute/path/to/input.osm",
|
"input": "/absolute/path/to/input.osm",
|
||||||
|
"nativeRoadPackage": "/absolute/path/to/exported-native-road-package.zip",
|
||||||
"outputRoot": "/absolute/path/to/outputs",
|
"outputRoot": "/absolute/path/to/outputs",
|
||||||
"qgisApp": "/Applications/QGIS.app",
|
"qgisApp": "/Applications/QGIS.app",
|
||||||
"blenderApp": "/Applications/Blender.app",
|
"blenderApp": "/Applications/Blender.app",
|
||||||
@@ -24,14 +25,6 @@
|
|||||||
"turnLaneArrows": {
|
"turnLaneArrows": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
"nativeRoad": {
|
|
||||||
"edgeLines": false,
|
|
||||||
"junctionTemplates": {
|
|
||||||
"enabled": false,
|
|
||||||
"references": [],
|
|
||||||
"clusters": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"osm2streets": {
|
"osm2streets": {
|
||||||
"debug_each_step": false,
|
"debug_each_step": false,
|
||||||
"dual_carriageway_experiment": false,
|
"dual_carriageway_experiment": false,
|
||||||
|
|||||||
BIN
inputs/native-road-packages/fengshu-er-road.zip
Normal file
BIN
inputs/native-road-packages/fengshu-er-road.zip
Normal file
Binary file not shown.
BIN
inputs/native-road-packages/nantaizi-lake-innovation-valley.zip
Normal file
BIN
inputs/native-road-packages/nantaizi-lake-innovation-valley.zip
Normal file
Binary file not shown.
1
package-lock.json
generated
1
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/prompts": "^8.5.2",
|
"@inquirer/prompts": "^8.5.2",
|
||||||
"@osm-asset/road-compiler": "git+https://git.app.que01.top/que01/road-compiler.git#v0.2.2",
|
"@osm-asset/road-compiler": "git+https://git.app.que01.top/que01/road-compiler.git#v0.2.2",
|
||||||
|
"fflate": "0.8.3",
|
||||||
"ol": "^10.10.0",
|
"ol": "^10.10.0",
|
||||||
"osm2streets-js-node": "0.1.4"
|
"osm2streets-js-node": "0.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"serve:v2x-preview": "node scripts/v2x-preview-server.js",
|
"serve:v2x-preview": "node scripts/v2x-preview-server.js",
|
||||||
"test:road-workbench": "node scripts/test-road-workbench.js",
|
"test:road-workbench": "node scripts/test-road-workbench.js",
|
||||||
"test:native-road": "node scripts/test-native-road.js",
|
"test:native-road": "node scripts/test-native-road.js",
|
||||||
|
"test:native-road-package": "node scripts/test-native-road-package.js",
|
||||||
"test:road-compiler-cli": "node scripts/test-road-compiler-cli.js",
|
"test:road-compiler-cli": "node scripts/test-road-compiler-cli.js",
|
||||||
"test:road-parity": "node scripts/test-road-parity.js",
|
"test:road-parity": "node scripts/test-road-parity.js",
|
||||||
"test:gaode-junction-reference": "node scripts/test-gaode-junction-reference.js",
|
"test:gaode-junction-reference": "node scripts/test-gaode-junction-reference.js",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/prompts": "^8.5.2",
|
"@inquirer/prompts": "^8.5.2",
|
||||||
"@osm-asset/road-compiler": "git+https://git.app.que01.top/que01/road-compiler.git#v0.2.2",
|
"@osm-asset/road-compiler": "git+https://git.app.que01.top/que01/road-compiler.git#v0.2.2",
|
||||||
|
"fflate": "0.8.3",
|
||||||
"ol": "^10.10.0",
|
"ol": "^10.10.0",
|
||||||
"osm2streets-js-node": "0.1.4"
|
"osm2streets-js-node": "0.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { spawnSync } = require("child_process");
|
const { spawnSync } = require("child_process");
|
||||||
const { readAreaConfig } = require("./lib/area-config");
|
const { readAreaConfig } = require("./lib/area-config");
|
||||||
const { compileArea: compileNativeRoads } = require("./lib/road-compiler-cli");
|
const { importNativeRoadPackage } = require("./lib/native-road-package");
|
||||||
const { resolveStages } = require("./lib/build-stages");
|
const { resolveStages } = require("./lib/build-stages");
|
||||||
const { validateManifest, addIntegrity } = require("./lib/package-contract");
|
const { validateManifest, addIntegrity } = require("./lib/package-contract");
|
||||||
const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths");
|
const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths");
|
||||||
@@ -225,10 +225,7 @@ function buildBlenderScene(area, roadProvider) {
|
|||||||
}
|
}
|
||||||
fs.mkdirSync(path.dirname(area.outputs.blend), { recursive: true });
|
fs.mkdirSync(path.dirname(area.outputs.blend), { recursive: true });
|
||||||
fs.mkdirSync(path.dirname(area.outputs.render), { recursive: true });
|
fs.mkdirSync(path.dirname(area.outputs.render), { recursive: true });
|
||||||
if (roadProvider === "native") {
|
const nativePackage = roadProvider === "native" ? importNativeRoadPackage(area) : null;
|
||||||
compileNativeRoads(area);
|
|
||||||
ensureNativeRoadLayers(area);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blenderArgs = [
|
const blenderArgs = [
|
||||||
"--background",
|
"--background",
|
||||||
@@ -246,8 +243,8 @@ function buildBlenderScene(area, roadProvider) {
|
|||||||
area.blender.treeStyle,
|
area.blender.treeStyle,
|
||||||
];
|
];
|
||||||
if (roadProvider === "native") {
|
if (roadProvider === "native") {
|
||||||
blenderArgs.push("--native-road", area.outputs.nativeRoadDir);
|
blenderArgs.push("--native-road", nativePackage.rootDir);
|
||||||
blenderArgs.push("--traffic-signals", path.join(area.outputs.nativeRoadDir, "traffic-signals.json"));
|
blenderArgs.push("--traffic-signals", path.join(nativePackage.rootDir, "traffic-signals.json"));
|
||||||
} else {
|
} else {
|
||||||
blenderArgs.push("--geojson", area.outputs.geojsonDir);
|
blenderArgs.push("--geojson", area.outputs.geojsonDir);
|
||||||
}
|
}
|
||||||
@@ -270,7 +267,7 @@ function buildBlenderScene(area, roadProvider) {
|
|||||||
inputs: {
|
inputs: {
|
||||||
config: fileRecord(configPath),
|
config: fileRecord(configPath),
|
||||||
osm: fileRecord(area.input),
|
osm: fileRecord(area.input),
|
||||||
...(roadProvider === "native" ? nativeRoadRecords(area) : sceneGeojsonRecords(area)),
|
...(roadProvider === "native" ? nativeRoadRecords(nativePackage) : sceneGeojsonRecords(area)),
|
||||||
...(roadProvider === "osm2streets" ? {
|
...(roadProvider === "osm2streets" ? {
|
||||||
geojsonDir: fileRecord(area.outputs.geojsonDir),
|
geojsonDir: fileRecord(area.outputs.geojsonDir),
|
||||||
trafficSignalAssemblies: fileRecord(area.outputs.trafficSignalAssemblies),
|
trafficSignalAssemblies: fileRecord(area.outputs.trafficSignalAssemblies),
|
||||||
@@ -282,7 +279,7 @@ function buildBlenderScene(area, roadProvider) {
|
|||||||
render: fileRecord(area.outputs.render),
|
render: fileRecord(area.outputs.render),
|
||||||
},
|
},
|
||||||
summary: {
|
summary: {
|
||||||
...(roadProvider === "native" ? { nativeRoad: nativeRoadFeatureCounts(area) } : { geojson: geojsonFeatureCounts(area) }),
|
...(roadProvider === "native" ? { nativeRoad: nativeRoadFeatureCounts(nativePackage) } : { geojson: geojsonFeatureCounts(area) }),
|
||||||
roadProvider,
|
roadProvider,
|
||||||
blendBytes: fileRecord(area.outputs.blend).bytes,
|
blendBytes: fileRecord(area.outputs.blend).bytes,
|
||||||
renderBytes: fileRecord(area.outputs.render).bytes,
|
renderBytes: fileRecord(area.outputs.render).bytes,
|
||||||
@@ -291,17 +288,13 @@ function buildBlenderScene(area, roadProvider) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNativeRoadLayers(area) {
|
function nativeRoadRecords(nativePackage) {
|
||||||
ensureFile(path.join(area.outputs.nativeRoadDir, "compiled.json"), "Native road compilation");
|
const root = path.join(nativePackage.rootDir, "layers");
|
||||||
for (const file of ["road_surface.geojson", "edge_lines.geojson", "intersection_surface.geojson", "sidewalk_surface.geojson", "lane_separators.geojson", "center_lines.geojson", "direction_arrows.geojson", "turn_arrows.geojson", "crosswalks.geojson", "vehicle_stop_lines.geojson"]) {
|
|
||||||
ensureFile(path.join(area.outputs.nativeRoadDir, "layers", file), `Native road layer ${file}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nativeRoadRecords(area) {
|
|
||||||
const root = path.join(area.outputs.nativeRoadDir, "layers");
|
|
||||||
return {
|
return {
|
||||||
nativeRoadCompiled: fileRecord(path.join(area.outputs.nativeRoadDir, "compiled.json")),
|
nativeRoadZip: nativePackage.zipRecord,
|
||||||
|
nativeRoadImport: fileRecord(nativePackage.rootDir),
|
||||||
|
nativeRoadCompiled: fileRecord(path.join(nativePackage.rootDir, "compiled.json")),
|
||||||
|
nativeRoadSignals: fileRecord(path.join(nativePackage.rootDir, "traffic-signals.json")),
|
||||||
nativeRoadSurface: fileRecord(path.join(root, "road_surface.geojson")),
|
nativeRoadSurface: fileRecord(path.join(root, "road_surface.geojson")),
|
||||||
nativeEdgeLines: fileRecord(path.join(root, "edge_lines.geojson")),
|
nativeEdgeLines: fileRecord(path.join(root, "edge_lines.geojson")),
|
||||||
nativeIntersectionSurface: fileRecord(path.join(root, "intersection_surface.geojson")),
|
nativeIntersectionSurface: fileRecord(path.join(root, "intersection_surface.geojson")),
|
||||||
@@ -315,8 +308,8 @@ function nativeRoadRecords(area) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function nativeRoadFeatureCounts(area) {
|
function nativeRoadFeatureCounts(nativePackage) {
|
||||||
const root = path.join(area.outputs.nativeRoadDir, "layers");
|
const root = path.join(nativePackage.rootDir, "layers");
|
||||||
return {
|
return {
|
||||||
roadSurface: featureCount(path.join(root, "road_surface.geojson")),
|
roadSurface: featureCount(path.join(root, "road_surface.geojson")),
|
||||||
edgeLines: featureCount(path.join(root, "edge_lines.geojson")),
|
edgeLines: featureCount(path.join(root, "edge_lines.geojson")),
|
||||||
@@ -487,8 +480,9 @@ function compressCesiumGlb(area) {
|
|||||||
|
|
||||||
function publishPackage(area, roadProvider) {
|
function publishPackage(area, roadProvider) {
|
||||||
ensureFile(area.outputs.packageStagingManifest, "Staged package manifest");
|
ensureFile(area.outputs.packageStagingManifest, "Staged package manifest");
|
||||||
const trafficSignalsSource = roadProvider === "native"
|
const nativePackage = roadProvider === "native" ? importNativeRoadPackage(area) : null;
|
||||||
? path.join(area.outputs.nativeRoadDir, "traffic-signals.json")
|
const trafficSignalsSource = nativePackage
|
||||||
|
? path.join(nativePackage.rootDir, "traffic-signals.json")
|
||||||
: area.outputs.trafficSignals;
|
: area.outputs.trafficSignals;
|
||||||
ensureFile(trafficSignalsSource, "Traffic signal anchors");
|
ensureFile(trafficSignalsSource, "Traffic signal anchors");
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
@@ -574,7 +568,7 @@ function writeCesiumPreview(area, roadProvider) {
|
|||||||
intersectionSurface: fileRecord(intersectionSurface),
|
intersectionSurface: fileRecord(intersectionSurface),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Object.assign(previewInputs, nativeRoadRecords(area));
|
Object.assign(previewInputs, nativeRoadRecords(importNativeRoadPackage(area)));
|
||||||
// This operational preview intentionally has no synthetic vehicle source.
|
// This operational preview intentionally has no synthetic vehicle source.
|
||||||
// A stale descriptor from an older build must not resurrect simulated routes.
|
// A stale descriptor from an older build must not resurrect simulated routes.
|
||||||
fs.rmSync(area.outputs.trafficSimulation, { force: true });
|
fs.rmSync(area.outputs.trafficSimulation, { force: true });
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ function normalizeAreaConfig(raw, options = {}) {
|
|||||||
const fileStem = outputOverrides.fileStem || id;
|
const fileStem = outputOverrides.fileStem || id;
|
||||||
const compress = normalizeCompressConfig(raw.compress);
|
const compress = normalizeCompressConfig(raw.compress);
|
||||||
const budget = normalizeBudgetConfig(raw.budget);
|
const budget = normalizeBudgetConfig(raw.budget);
|
||||||
|
const roadProvider = roadProviderOption(raw.blender?.roadProvider ?? "native");
|
||||||
|
if (roadProvider === "native" && raw.nativeRoadPackage === undefined) {
|
||||||
|
throw new Error("Missing config key: nativeRoadPackage");
|
||||||
|
}
|
||||||
const pipelineDir = path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline"));
|
const pipelineDir = path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline"));
|
||||||
const packageDir = path.resolve(outputOverrides.packageDir || path.join(areaDir, "package"));
|
const packageDir = path.resolve(outputOverrides.packageDir || path.join(areaDir, "package"));
|
||||||
const packageStagingDir = path.resolve(outputOverrides.packageStagingDir || path.join(pipelineDir, "package-staging"));
|
const packageStagingDir = path.resolve(outputOverrides.packageStagingDir || path.join(pipelineDir, "package-staging"));
|
||||||
@@ -32,13 +36,9 @@ function normalizeAreaConfig(raw, options = {}) {
|
|||||||
const packageStagingRuntimeDir = path.resolve(outputOverrides.packageStagingRuntimeDir || path.join(packageStagingDir, "runtime"));
|
const packageStagingRuntimeDir = path.resolve(outputOverrides.packageStagingRuntimeDir || path.join(packageStagingDir, "runtime"));
|
||||||
const previewDir = path.resolve(outputOverrides.previewDir || path.join(areaDir, "_preview"));
|
const previewDir = path.resolve(outputOverrides.previewDir || path.join(areaDir, "_preview"));
|
||||||
const geojsonDir = path.resolve(outputOverrides.geojsonDir || path.join(areaDir, "osm2streets_web_out"));
|
const geojsonDir = path.resolve(outputOverrides.geojsonDir || path.join(areaDir, "osm2streets_web_out"));
|
||||||
const nativeRoadDir = path.resolve(outputOverrides.nativeRoadDir || path.join(areaDir, "native-road"));
|
|
||||||
const outputs = {
|
const outputs = {
|
||||||
areaDir,
|
areaDir,
|
||||||
geojsonDir,
|
geojsonDir,
|
||||||
nativeRoadDir,
|
|
||||||
nativeRoadOverrides: path.resolve(outputOverrides.nativeRoadOverrides || path.join(areaDir, "native-road-overrides.json")),
|
|
||||||
nativeTrafficSignals: path.resolve(outputOverrides.nativeTrafficSignals || path.join(areaDir, "native-traffic-signals.json")),
|
|
||||||
gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`)),
|
gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`)),
|
||||||
qgisProject: path.resolve(outputOverrides.qgisProject || path.join(areaDir, `${fileStem}.qgz`)),
|
qgisProject: path.resolve(outputOverrides.qgisProject || path.join(areaDir, `${fileStem}.qgz`)),
|
||||||
qgisPreview: path.resolve(outputOverrides.qgisPreview || path.join(areaDir, `${fileStem}-preview.png`)),
|
qgisPreview: path.resolve(outputOverrides.qgisPreview || path.join(areaDir, `${fileStem}-preview.png`)),
|
||||||
@@ -81,6 +81,9 @@ function normalizeAreaConfig(raw, options = {}) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
nativeRoadPackage: raw.nativeRoadPackage === undefined
|
||||||
|
? null
|
||||||
|
: path.resolve(options.configDir || repoRoot, requireText(raw.nativeRoadPackage, "nativeRoadPackage")),
|
||||||
input,
|
input,
|
||||||
outputRoot,
|
outputRoot,
|
||||||
qgisApp: raw.qgisApp || (process.platform === "darwin" ? "/Applications/QGIS.app" : "/usr"),
|
qgisApp: raw.qgisApp || (process.platform === "darwin" ? "/Applications/QGIS.app" : "/usr"),
|
||||||
@@ -111,10 +114,6 @@ function normalizeAreaConfig(raw, options = {}) {
|
|||||||
turnLaneArrows: {
|
turnLaneArrows: {
|
||||||
enabled: booleanOption(raw.turnLaneArrows?.enabled, false, "turnLaneArrows.enabled"),
|
enabled: booleanOption(raw.turnLaneArrows?.enabled, false, "turnLaneArrows.enabled"),
|
||||||
},
|
},
|
||||||
nativeRoad: {
|
|
||||||
edgeLines: booleanOption(raw.nativeRoad?.edgeLines, false, "nativeRoad.edgeLines"),
|
|
||||||
junctionTemplates: normalizeJunctionTemplates(raw.nativeRoad?.junctionTemplates, repoRoot, options.configDir || repoRoot),
|
|
||||||
},
|
|
||||||
osm2streets: raw.osm2streets || {
|
osm2streets: raw.osm2streets || {
|
||||||
debug_each_step: false,
|
debug_each_step: false,
|
||||||
dual_carriageway_experiment: false,
|
dual_carriageway_experiment: false,
|
||||||
@@ -125,7 +124,7 @@ function normalizeAreaConfig(raw, options = {}) {
|
|||||||
blender: {
|
blender: {
|
||||||
treeStyle: raw.blender?.treeStyle || "natural",
|
treeStyle: raw.blender?.treeStyle || "natural",
|
||||||
officeOverrides: raw.blender?.officeOverrides || raw.blender?.office_overrides || "",
|
officeOverrides: raw.blender?.officeOverrides || raw.blender?.office_overrides || "",
|
||||||
roadProvider: roadProviderOption(raw.blender?.roadProvider ?? "native"),
|
roadProvider,
|
||||||
},
|
},
|
||||||
v2xPreview: normalizeV2xPreviewConfig(raw.v2xPreview),
|
v2xPreview: normalizeV2xPreviewConfig(raw.v2xPreview),
|
||||||
compress,
|
compress,
|
||||||
@@ -272,24 +271,8 @@ function booleanOption(value, fallback, label) {
|
|||||||
throw new Error(`${label} must be boolean`);
|
throw new Error(`${label} must be boolean`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRoadCompilerInput(area) {
|
|
||||||
return {
|
|
||||||
areaId: area.id,
|
|
||||||
osmFile: area.input,
|
|
||||||
outDir: area.outputs.nativeRoadDir,
|
|
||||||
stagingDir: area.outputs.pipelineDir,
|
|
||||||
overridesFile: area.outputs.nativeRoadOverrides,
|
|
||||||
trafficSignalsFile: area.outputs.nativeTrafficSignals,
|
|
||||||
comparisonDir: area.outputs.geojsonDir,
|
|
||||||
options: {
|
|
||||||
edgeLines: area.nativeRoad.edgeLines,
|
|
||||||
junctionTemplates: area.nativeRoad.junctionTemplates,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
normalizeAreaConfig,
|
normalizeAreaConfig,
|
||||||
readAreaConfig,
|
readAreaConfig,
|
||||||
toRoadCompilerInput,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { readAreaConfig } = require("./area-config");
|
const { readAreaConfig } = require("./area-config");
|
||||||
|
const { importNativeRoadPackage } = require("./native-road-package");
|
||||||
const {
|
const {
|
||||||
SCENE_LAYERS,
|
SCENE_LAYERS,
|
||||||
SCENE_FILE,
|
SCENE_FILE,
|
||||||
@@ -324,6 +325,11 @@ function addEndpoint(map, ref) {
|
|||||||
|
|
||||||
function artifactStatus(area) {
|
function artifactStatus(area) {
|
||||||
const native = area.blender.roadProvider === "native";
|
const native = area.blender.roadProvider === "native";
|
||||||
|
let nativePackage = null;
|
||||||
|
let nativePackageError = null;
|
||||||
|
if (native) {
|
||||||
|
try { nativePackage = importNativeRoadPackage(area); } catch (error) { nativePackageError = error.message; }
|
||||||
|
}
|
||||||
const entries = [
|
const entries = [
|
||||||
...(native ? [] : [
|
...(native ? [] : [
|
||||||
["GeoJSON dir", area.outputs.geojsonDir, true, "dir"],
|
["GeoJSON dir", area.outputs.geojsonDir, true, "dir"],
|
||||||
@@ -332,15 +338,16 @@ function artifactStatus(area) {
|
|||||||
["QGIS preview", area.outputs.qgisPreview, true, "file"],
|
["QGIS preview", area.outputs.qgisPreview, true, "file"],
|
||||||
["Traffic signal assemblies", area.outputs.trafficSignalAssemblies, true, "file"],
|
["Traffic signal assemblies", area.outputs.trafficSignalAssemblies, true, "file"],
|
||||||
]),
|
]),
|
||||||
["Native road directory", area.outputs.nativeRoadDir, native, "dir"],
|
["Native road ZIP", area.nativeRoadPackage, native, "file"],
|
||||||
["Traffic signal runtime", native ? path.join(area.outputs.nativeRoadDir, "traffic-signals.json") : area.outputs.packageTrafficSignals, true, "file"],
|
["Native road import", nativePackage?.rootDir || path.join(area.outputs.pipelineDir, "native-road-import"), native, "dir"],
|
||||||
|
["Traffic signal runtime", native ? path.join(nativePackage?.rootDir || "", "traffic-signals.json") : area.outputs.packageTrafficSignals, true, "file"],
|
||||||
["Blend scene", area.outputs.blend, true, "file"],
|
["Blend scene", area.outputs.blend, true, "file"],
|
||||||
["Render PNG", area.outputs.render, true, "file"],
|
["Render PNG", area.outputs.render, true, "file"],
|
||||||
["Package manifest", area.outputs.packageManifest, true, "file"],
|
["Package manifest", area.outputs.packageManifest, true, "file"],
|
||||||
["Package primary GLB", area.outputs.packagePrimaryGlb, true, "file"],
|
["Package primary GLB", area.outputs.packagePrimaryGlb, true, "file"],
|
||||||
["Cesium preview", area.outputs.cesiumPreview, true, "file"],
|
["Cesium preview", area.outputs.cesiumPreview, true, "file"],
|
||||||
];
|
];
|
||||||
return entries.map(([label, file, expected, type]) => {
|
const result = entries.map(([label, file, expected, type]) => {
|
||||||
const exists = fs.existsSync(file);
|
const exists = fs.existsSync(file);
|
||||||
const stat = exists ? fs.statSync(file) : null;
|
const stat = exists ? fs.statSync(file) : null;
|
||||||
const validType = !exists || (type === "dir" ? stat.isDirectory() : stat.isFile());
|
const validType = !exists || (type === "dir" ? stat.isDirectory() : stat.isFile());
|
||||||
@@ -358,6 +365,8 @@ function artifactStatus(area) {
|
|||||||
geojsonFiles,
|
geojsonFiles,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
if (nativePackageError) result.push({ label: "Native road ZIP validation", path: area.nativeRoadPackage, expected: true, type: "file", exists: false, validType: false, error: nativePackageError });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function metadataSummary(file, warnings) {
|
function metadataSummary(file, warnings) {
|
||||||
@@ -378,6 +387,10 @@ function metadataSummary(file, warnings) {
|
|||||||
|
|
||||||
function stageManifestStatus(area, configPath = null) {
|
function stageManifestStatus(area, configPath = null) {
|
||||||
const native = area.blender.roadProvider === "native";
|
const native = area.blender.roadProvider === "native";
|
||||||
|
let nativePackage = null;
|
||||||
|
if (native) {
|
||||||
|
try { nativePackage = importNativeRoadPackage(area); } catch (_) { /* artifact status reports the validation failure */ }
|
||||||
|
}
|
||||||
const compressionComplete = fs.existsSync(stageManifestPath(area, "compress"));
|
const compressionComplete = fs.existsSync(stageManifestPath(area, "compress"));
|
||||||
const reimportManifest = stageManifestPath(area, "reimport");
|
const reimportManifest = stageManifestPath(area, "reimport");
|
||||||
const hasReimportManifest = fs.existsSync(reimportManifest);
|
const hasReimportManifest = fs.existsSync(reimportManifest);
|
||||||
@@ -435,7 +448,7 @@ function stageManifestStatus(area, configPath = null) {
|
|||||||
inputs: {
|
inputs: {
|
||||||
...(configPath ? { config: configPath } : {}),
|
...(configPath ? { config: configPath } : {}),
|
||||||
osm: area.input,
|
osm: area.input,
|
||||||
...(native ? nativeRoadRecords(area) : {
|
...(native && nativePackage ? nativeRoadRecords(nativePackage) : {
|
||||||
geojsonDir: area.outputs.geojsonDir,
|
geojsonDir: area.outputs.geojsonDir,
|
||||||
...sceneGeojsonFiles(area),
|
...sceneGeojsonFiles(area),
|
||||||
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
||||||
@@ -468,7 +481,7 @@ function stageManifestStatus(area, configPath = null) {
|
|||||||
glb: area.outputs.glb,
|
glb: area.outputs.glb,
|
||||||
metadata: area.outputs.metadata,
|
metadata: area.outputs.metadata,
|
||||||
}),
|
}),
|
||||||
...(native ? nativeRoadRecords(area) : {
|
...(native && nativePackage ? nativeRoadRecords(nativePackage) : {
|
||||||
lanePolygons: path.join(area.outputs.geojsonDir, "lane_polygons.geojson"),
|
lanePolygons: path.join(area.outputs.geojsonDir, "lane_polygons.geojson"),
|
||||||
network: path.join(area.outputs.geojsonDir, "network.json"),
|
network: path.join(area.outputs.geojsonDir, "network.json"),
|
||||||
intersectionSurface: path.join(area.outputs.geojsonDir, "intersection_surface.geojson"),
|
intersectionSurface: path.join(area.outputs.geojsonDir, "intersection_surface.geojson"),
|
||||||
@@ -609,11 +622,13 @@ function sceneGeojsonFiles(area) {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nativeRoadRecords(area) {
|
function nativeRoadRecords(nativePackage) {
|
||||||
const root = path.join(area.outputs.nativeRoadDir, "layers");
|
const root = path.join(nativePackage.rootDir, "layers");
|
||||||
return {
|
return {
|
||||||
nativeRoadCompiled: path.join(area.outputs.nativeRoadDir, "compiled.json"),
|
nativeRoadZip: nativePackage.zipFile,
|
||||||
nativeRoadSignals: path.join(area.outputs.nativeRoadDir, "traffic-signals.json"),
|
nativeRoadImport: nativePackage.rootDir,
|
||||||
|
nativeRoadCompiled: path.join(nativePackage.rootDir, "compiled.json"),
|
||||||
|
nativeRoadSignals: path.join(nativePackage.rootDir, "traffic-signals.json"),
|
||||||
nativeRoadSurface: path.join(root, "road_surface.geojson"),
|
nativeRoadSurface: path.join(root, "road_surface.geojson"),
|
||||||
nativeEdgeLines: path.join(root, "edge_lines.geojson"),
|
nativeEdgeLines: path.join(root, "edge_lines.geojson"),
|
||||||
nativeIntersectionSurface: path.join(root, "intersection_surface.geojson"),
|
nativeIntersectionSurface: path.join(root, "intersection_surface.geojson"),
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function buildNativeTrafficSimulation(area, options = {}) {
|
function buildNativeTrafficSimulation(area, options = {}) {
|
||||||
const root = area.outputs.nativeRoadDir;
|
const root = options.nativePackage?.rootDir;
|
||||||
|
if (!root) throw new Error("Native traffic simulation requires an imported native road package");
|
||||||
const compiledPath = path.join(root, "compiled.json");
|
const compiledPath = path.join(root, "compiled.json");
|
||||||
const signalPath = path.join(root, "traffic-signals.json");
|
const signalPath = path.join(root, "traffic-signals.json");
|
||||||
const connectorPath = path.join(root, "layers", "connectors.geojson");
|
const connectorPath = path.join(root, "layers", "connectors.geojson");
|
||||||
|
|||||||
87
scripts/lib/native-road-package.js
Normal file
87
scripts/lib/native-road-package.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { unzipSync } = require("fflate");
|
||||||
|
|
||||||
|
const CONTRACT = "native-road-package/v1.1";
|
||||||
|
const ROOT_FILES = ["manifest.json", "compiled.json", "diagnostics.json", "comparison.json", "traffic-signal-assemblies.json", "traffic-signals.json"];
|
||||||
|
|
||||||
|
function sha256(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|
||||||
|
|
||||||
|
function readCentralEntries(bytes) {
|
||||||
|
const entries = [];
|
||||||
|
for (let i = 0; i + 46 <= bytes.length; i += 1) {
|
||||||
|
if (bytes.readUInt32LE(i) !== 0x02014b50) continue;
|
||||||
|
const nameLength = bytes.readUInt16LE(i + 28);
|
||||||
|
const extraLength = bytes.readUInt16LE(i + 30);
|
||||||
|
const commentLength = bytes.readUInt16LE(i + 32);
|
||||||
|
const name = bytes.subarray(i + 46, i + 46 + nameLength).toString("utf8");
|
||||||
|
const externalAttrs = bytes.readUInt32LE(i + 38);
|
||||||
|
entries.push({ name, externalAttrs, directory: name.endsWith("/") });
|
||||||
|
i += 45 + nameLength + extraLength + commentLength;
|
||||||
|
}
|
||||||
|
if (!entries.length) throw new Error("ZIP central directory is missing");
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateName(name) {
|
||||||
|
if (!name || path.posix.isAbsolute(name) || name.split("/").includes("..") || name.includes("\\")) throw new Error(`Unsafe ZIP entry path: ${name}`);
|
||||||
|
if (name.startsWith("./") || (name.includes("/") && !name.startsWith("layers/")) || (name.startsWith("layers/") && name.split("/").length !== 2)) throw new Error(`Native road ZIP must be root-flat: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateManifest(rootDir, areaId) {
|
||||||
|
let manifest;
|
||||||
|
try { manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "manifest.json"), "utf8")); } catch (error) { throw new Error(`Invalid native road manifest: ${error.message}`); }
|
||||||
|
if (!manifest || manifest.contract !== CONTRACT) throw new Error(`Unsupported native road contract: ${manifest?.contract || "missing"}`);
|
||||||
|
if (manifest.areaId !== areaId) throw new Error(`Native road areaId mismatch: expected ${areaId}, got ${manifest.areaId}`);
|
||||||
|
if (!Array.isArray(manifest.layers) || !manifest.layers.length) throw new Error("Native road manifest layers are missing");
|
||||||
|
const declared = new Set();
|
||||||
|
for (const layer of manifest.layers) {
|
||||||
|
if (!layer || typeof layer.source !== "string" || layer.source !== path.posix.basename(layer.source) || declared.has(layer.source)) throw new Error("Invalid native road layer declaration");
|
||||||
|
if (!["surface", "marking", "semantic"].includes(layer.role)) throw new Error(`Invalid native road layer role: ${layer.role}`);
|
||||||
|
if (layer.role !== "semantic" && typeof layer.materialLayer !== "string") throw new Error(`Renderable layer ${layer.source} has no materialLayer`);
|
||||||
|
declared.add(layer.source);
|
||||||
|
}
|
||||||
|
const published = new Set(fs.readdirSync(path.join(rootDir, "layers")).filter((name) => name.endsWith(".geojson")).map((name) => name.slice(0, -8)));
|
||||||
|
if (declared.size !== published.size || [...declared].some((name) => !published.has(name))) throw new Error("Native road manifest/source mismatch");
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function importNativeRoadPackage(area) {
|
||||||
|
const zipFile = area.nativeRoadPackage;
|
||||||
|
if (!zipFile || !fs.existsSync(zipFile) || !fs.statSync(zipFile).isFile()) throw new Error(`Native road ZIP not found: ${zipFile || "missing nativeRoadPackage"}`);
|
||||||
|
const bytes = fs.readFileSync(zipFile);
|
||||||
|
const hash = crypto.createHash("sha256").update(bytes).digest("hex");
|
||||||
|
const entries = readCentralEntries(bytes);
|
||||||
|
const names = new Set();
|
||||||
|
for (const entry of entries) {
|
||||||
|
validateName(entry.name);
|
||||||
|
if (names.has(entry.name)) throw new Error(`Duplicate ZIP entry: ${entry.name}`);
|
||||||
|
names.add(entry.name);
|
||||||
|
const unixMode = entry.externalAttrs >>> 16;
|
||||||
|
if ((unixMode & 0xf000) === 0xa000) throw new Error(`Symbolic links are not allowed: ${entry.name}`);
|
||||||
|
}
|
||||||
|
let files;
|
||||||
|
try { files = unzipSync(bytes); } catch (error) { throw new Error(`Invalid ZIP archive: ${error.message}`); }
|
||||||
|
const expected = new Set([...ROOT_FILES, ...[...names].filter((name) => name.startsWith("layers/") && name.endsWith(".geojson"))]);
|
||||||
|
for (const name of ROOT_FILES) if (!names.has(name)) throw new Error(`Missing native road ZIP entry: ${name}`);
|
||||||
|
for (const name of names) if (!expected.has(name) || (!name.startsWith("layers/") && !ROOT_FILES.includes(name))) throw new Error(`Unexpected native road ZIP entry: ${name}`);
|
||||||
|
const rootDir = path.join(area.outputs.pipelineDir, "native-road-import", hash);
|
||||||
|
if (!fs.existsSync(rootDir)) {
|
||||||
|
fs.mkdirSync(path.join(rootDir, "layers"), { recursive: true });
|
||||||
|
for (const [name, data] of Object.entries(files)) {
|
||||||
|
if (!names.has(name) || name.endsWith("/")) continue;
|
||||||
|
const destination = path.join(rootDir, name);
|
||||||
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||||
|
fs.writeFileSync(destination, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const manifest = validateManifest(rootDir, area.id);
|
||||||
|
const records = {};
|
||||||
|
for (const name of names) { const file = path.join(rootDir, name); if (fs.existsSync(file) && fs.statSync(file).isFile()) records[name] = { path: file, bytes: fs.statSync(file).size, sha256: sha256(file) }; }
|
||||||
|
return Object.freeze({ zipFile, zipRecord: { path: zipFile, bytes: bytes.length, sha256: hash }, rootDir, manifest, records });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { CONTRACT, importNativeRoadPackage };
|
||||||
@@ -19,7 +19,7 @@ assert.match(areaBuildSource, /exportCesium\(area, roadProvider\)/);
|
|||||||
assert.match(areaBuildSource, /"--dynamic-glb", area\.outputs\.trafficSignalsDynamicGlb/);
|
assert.match(areaBuildSource, /"--dynamic-glb", area\.outputs\.trafficSignalsDynamicGlb/);
|
||||||
assert.match(areaBuildSource, /trafficSignalsCountdown0Glb: fileRecord/);
|
assert.match(areaBuildSource, /trafficSignalsCountdown0Glb: fileRecord/);
|
||||||
assert.match(areaBuildSource, /if \(roadProvider === "osm2streets"\) \{[\s\S]*?buildPreviewVehicleRoute/);
|
assert.match(areaBuildSource, /if \(roadProvider === "osm2streets"\) \{[\s\S]*?buildPreviewVehicleRoute/);
|
||||||
assert.match(areaBuildSource, /Object\.assign\(previewInputs, nativeRoadRecords\(area\)\)/);
|
assert.match(areaBuildSource, /Object\.assign\(previewInputs, nativeRoadRecords\(/);
|
||||||
assert.match(areaBuildSource, /vehicleRoute: optionalFileRecord\(area\.outputs\.vehicleRoute\)/);
|
assert.match(areaBuildSource, /vehicleRoute: optionalFileRecord\(area\.outputs\.vehicleRoute\)/);
|
||||||
|
|
||||||
const gltf = {
|
const gltf = {
|
||||||
@@ -48,7 +48,7 @@ assert.equal(evaluateGlbBudget(digest, budget).violations[0].key, "glbBytes");
|
|||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "asset-budget-"));
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "asset-budget-"));
|
||||||
const input = path.join(tempDir, "input.osm");
|
const input = path.join(tempDir, "input.osm");
|
||||||
fs.writeFileSync(input, "<osm/>");
|
fs.writeFileSync(input, "<osm/>");
|
||||||
const base = { id: "test-area", input, outputRoot: tempDir };
|
const base = { id: "test-area", input, outputRoot: tempDir, blender: { roadProvider: "osm2streets" } };
|
||||||
assert.equal(
|
assert.equal(
|
||||||
normalizeAreaConfig(base).outputs.trafficSignals,
|
normalizeAreaConfig(base).outputs.trafficSignals,
|
||||||
path.join(tempDir, "test-area", "osm2streets_web_out", "traffic_signals.json"),
|
path.join(tempDir, "test-area", "osm2streets_web_out", "traffic_signals.json"),
|
||||||
@@ -63,7 +63,7 @@ assert.equal(
|
|||||||
);
|
);
|
||||||
assert.equal(normalizeAreaConfig({ ...base, budget: { nodes: 800 } }).budget.glbNodes, 800);
|
assert.equal(normalizeAreaConfig({ ...base, budget: { nodes: 800 } }).budget.glbNodes, 800);
|
||||||
assert.equal(normalizeAreaConfig(base).stages.intermediates, false);
|
assert.equal(normalizeAreaConfig(base).stages.intermediates, false);
|
||||||
assert.equal(normalizeAreaConfig(base).blender.roadProvider, "native");
|
assert.equal(normalizeAreaConfig(base).blender.roadProvider, "osm2streets");
|
||||||
assert.deepEqual(normalizeAreaConfig(base).v2xPreview, {
|
assert.deepEqual(normalizeAreaConfig(base).v2xPreview, {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
apiBaseUrl: "/api",
|
apiBaseUrl: "/api",
|
||||||
@@ -80,7 +80,7 @@ assert.throws(
|
|||||||
() => normalizeAreaConfig({ ...base, v2xPreview: "enabled" }),
|
() => normalizeAreaConfig({ ...base, v2xPreview: "enabled" }),
|
||||||
/v2xPreview must be an object/,
|
/v2xPreview must be an object/,
|
||||||
);
|
);
|
||||||
assert.equal(normalizeAreaConfig({ ...base, blender: { roadProvider: "native" } }).blender.roadProvider, "native");
|
assert.equal(normalizeAreaConfig({ ...base, nativeRoadPackage: path.join(tempDir, "roads.zip"), blender: { roadProvider: "native" } }).blender.roadProvider, "native");
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => normalizeAreaConfig({ ...base, blender: { roadProvider: "other" } }),
|
() => normalizeAreaConfig({ ...base, blender: { roadProvider: "other" } }),
|
||||||
/blender\.roadProvider/,
|
/blender\.roadProvider/,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ fs.writeFileSync(path.join(layersDir, "connectors.geojson"), JSON.stringify({ ty
|
|||||||
fs.writeFileSync(path.join(layersDir, "vehicle_stop_lines.geojson"), JSON.stringify({ type: "FeatureCollection", features: [{ type: "Feature", properties: { road_id: "road:a" }, geometry: { type: "Polygon", coordinates: [] } }] }));
|
fs.writeFileSync(path.join(layersDir, "vehicle_stop_lines.geojson"), JSON.stringify({ type: "FeatureCollection", features: [{ type: "Feature", properties: { road_id: "road:a" }, geometry: { type: "Polygon", coordinates: [] } }] }));
|
||||||
fs.writeFileSync(path.join(layersDir, "lane_centerlines.geojson"), JSON.stringify({ type: "FeatureCollection", features: roads.map((road) => ({ type: "Feature", properties: { road_id: road.id, lane_index: 1 }, geometry: { type: "LineString", coordinates: road.centerline } })) }));
|
fs.writeFileSync(path.join(layersDir, "lane_centerlines.geojson"), JSON.stringify({ type: "FeatureCollection", features: roads.map((road) => ({ type: "Feature", properties: { road_id: road.id, lane_index: 1 }, geometry: { type: "LineString", coordinates: road.centerline } })) }));
|
||||||
|
|
||||||
const descriptor = buildNativeTrafficSimulation({ id: "fixture", outputs: { areaDir, nativeRoadDir } });
|
const descriptor = buildNativeTrafficSimulation({ id: "fixture", outputs: { areaDir } }, { nativePackage: { rootDir: nativeRoadDir } });
|
||||||
assert.equal(descriptor.schema, SCHEMA);
|
assert.equal(descriptor.schema, SCHEMA);
|
||||||
assert.equal(descriptor.coordinateSystem.route, "WGS84");
|
assert.equal(descriptor.coordinateSystem.route, "WGS84");
|
||||||
assert.ok(descriptor.routes.length >= 1);
|
assert.ok(descriptor.routes.length >= 1);
|
||||||
|
|||||||
50
scripts/test-native-road-package.js
Normal file
50
scripts/test-native-road-package.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const { zipSync } = require("fflate");
|
||||||
|
const { importNativeRoadPackage } = require("./lib/native-road-package");
|
||||||
|
|
||||||
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "native-road-package-"));
|
||||||
|
const area = { id: "fixture", nativeRoadPackage: "", outputs: { pipelineDir: path.join(temp, "pipeline") } };
|
||||||
|
const layers = ["road_surface", "connectors"];
|
||||||
|
const manifest = { contract: "native-road-package/v1.1", areaId: "fixture", layers: [
|
||||||
|
{ source: "road_surface", role: "surface", materialLayer: "road_surface" },
|
||||||
|
{ source: "connectors", role: "semantic" },
|
||||||
|
] };
|
||||||
|
function validEntries() {
|
||||||
|
return {
|
||||||
|
"manifest.json": Buffer.from(JSON.stringify(manifest)),
|
||||||
|
"compiled.json": Buffer.from("{}"), "diagnostics.json": Buffer.from("{}"), "comparison.json": Buffer.from("{}"),
|
||||||
|
"traffic-signal-assemblies.json": Buffer.from("{}"), "traffic-signals.json": Buffer.from("{}"),
|
||||||
|
...Object.fromEntries(layers.map((layer) => [`layers/${layer}.geojson`, Buffer.from('{"type":"FeatureCollection","features":[]}')])),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function write(name, entries = validEntries()) { const file = path.join(temp, name); fs.writeFileSync(file, zipSync(entries)); area.nativeRoadPackage = file; return file; }
|
||||||
|
write("valid.zip");
|
||||||
|
const first = importNativeRoadPackage(area);
|
||||||
|
assert.equal(first.manifest.areaId, "fixture");
|
||||||
|
assert.equal(Object.keys(first.records).length, 8);
|
||||||
|
assert.ok(first.rootDir.includes(first.zipRecord.sha256));
|
||||||
|
assert.equal(importNativeRoadPackage(area).rootDir, first.rootDir);
|
||||||
|
assert.throws(() => { area.nativeRoadPackage = path.join(temp, "missing.zip"); importNativeRoadPackage(area); }, /ZIP not found/);
|
||||||
|
write("corrupt.zip", { "manifest.json": Buffer.from("{") });
|
||||||
|
assert.throws(() => importNativeRoadPackage(area), /ZIP central directory|Invalid ZIP|Invalid native road manifest|Missing native road ZIP entry/);
|
||||||
|
write("nested.zip", { "nested/manifest.json": Buffer.from("{}") });
|
||||||
|
assert.throws(() => importNativeRoadPackage(area), /root-flat/);
|
||||||
|
write("traversal.zip", { "../manifest.json": Buffer.from("{}") });
|
||||||
|
assert.throws(() => importNativeRoadPackage(area), /Unsafe ZIP entry/);
|
||||||
|
const wrongContract = validEntries(); wrongContract["manifest.json"] = Buffer.from(JSON.stringify({ ...manifest, contract: "v1" })); write("contract.zip", wrongContract);
|
||||||
|
assert.throws(() => importNativeRoadPackage(area), /Unsupported native road contract/);
|
||||||
|
const wrongArea = validEntries(); wrongArea["manifest.json"] = Buffer.from(JSON.stringify({ ...manifest, areaId: "other" })); write("area.zip", wrongArea);
|
||||||
|
assert.throws(() => importNativeRoadPackage(area), /areaId mismatch/);
|
||||||
|
const mismatch = validEntries(); delete mismatch["layers/connectors.geojson"]; write("mismatch.zip", mismatch);
|
||||||
|
assert.throws(() => importNativeRoadPackage(area), /manifest\/source mismatch/);
|
||||||
|
const symlink = write("symlink.zip");
|
||||||
|
const data = fs.readFileSync(symlink); const index = data.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02])); data.writeUInt32LE((0xa000 << 16) >>> 0, index + 38); fs.writeFileSync(symlink, data);
|
||||||
|
assert.throws(() => importNativeRoadPackage(area), /Symbolic links/);
|
||||||
|
fs.rmSync(temp, { recursive: true, force: true });
|
||||||
|
console.log("Native road ZIP package tests passed.");
|
||||||
Reference in New Issue
Block a user