#!/usr/bin/env node "use strict"; const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); const { normalizeAreaConfig } = require("./lib/area-config"); const { stageManifestStatus } = require("./lib/area-diagnostics"); const { digestGltf } = require("./glb-digest"); const { evaluateGlbBudget, BUDGETS, fileRecord, writeStageManifest } = require("./lib/stage-manifest"); const qgisBuildSource = fs.readFileSync(path.join(__dirname, "build-osm2streets-qgis.js"), "utf8"); const areaBuildSource = fs.readFileSync(path.join(__dirname, "build-area.js"), "utf8"); assert.match(qgisBuildSource, /QgsFieldConstraints\.Constraint\.ConstraintNotNull/); assert.match(qgisBuildSource, /QgsFieldConstraints\.ConstraintNotNull/); assert.doesNotMatch(qgisBuildSource, /setFieldConstraint\(index, 1\)/); assert.match(areaBuildSource, /exportCesium\(area, roadProvider\)/); assert.match(areaBuildSource, /if \(roadProvider === "osm2streets"\) \{\n exporterArgs\.splice/); const gltf = { nodes: [ { name: "Building_1", mesh: 0 }, { name: "Shapespark_tree-01", mesh: 1 }, ], meshes: [ { name: "Building", primitives: [{ indices: 0, attributes: { POSITION: 1 } }] }, { name: "Tree", primitives: [{ indices: 2, attributes: { POSITION: 3 } }] }, ], accessors: [{ count: 12 }, { count: 4 }, { count: 9 }, { count: 3 }], images: [{ name: "large", bufferView: 0 }, { name: "small", bufferView: 1 }], bufferViews: [{ byteLength: 800 }, { byteLength: 200 }], }; const digest = digestGltf(gltf, { file: "fixture.glb", fileBytes: 1500 }); assert.equal(digest.counts.triangles, 7); assert.equal(digest.counts.renderTriangles, 7); assert.equal(digest.embeddedImageBytes, 1000); assert.deepEqual(digest.sourceSummary.map((entry) => [entry.source, entry.triangles]), [["buildings", 4], ["vegetation", 3]]); assert.deepEqual(digest.images.map((image) => image.bytes), [800, 200]); const budget = { ...BUDGETS, glbBytes: 1000 }; assert.equal(evaluateGlbBudget(digest, budget).violations[0].key, "glbBytes"); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "asset-budget-")); const input = path.join(tempDir, "input.osm"); fs.writeFileSync(input, ""); const base = { id: "test-area", input, outputRoot: tempDir }; assert.equal( normalizeAreaConfig(base).outputs.trafficSignals, path.join(tempDir, "test-area", "osm2streets_web_out", "traffic_signals.json"), ); assert.equal( normalizeAreaConfig(base).outputs.trafficSignalAssemblies, path.join(tempDir, "test-area", "osm2streets_web_out", "traffic_signal_assemblies.geojson"), ); assert.equal(normalizeAreaConfig({ ...base, budget: { nodes: 800 } }).budget.glbNodes, 800); assert.equal(normalizeAreaConfig(base).blender.roadProvider, "osm2streets"); assert.equal(normalizeAreaConfig({ ...base, blender: { roadProvider: "native" } }).blender.roadProvider, "native"); assert.throws( () => normalizeAreaConfig({ ...base, blender: { roadProvider: "other" } }), /blender\.roadProvider/, ); assert.throws( () => normalizeAreaConfig({ ...base, budget: { nodes: 1200 } }), /budget.reason is required/, ); assert.throws( () => normalizeAreaConfig({ ...base, budget: { triangles: 0 } }), /budget.triangles must be a positive integer/, ); assert.throws( () => normalizeAreaConfig({ ...base, budget: "large" }), /budget must be an object/, ); assert.equal( normalizeAreaConfig({ ...base, budget: { nodes: 1200, reason: "Dense campus vegetation" } }).budget.glbNodes, 1200, ); const configPath = path.join(tempDir, "area.json"); fs.writeFileSync(configPath, `${JSON.stringify(base)}\n`); const area = normalizeAreaConfig(base); assert.equal(area.stages.compress, true); assert.equal("compressedGlb" in area.outputs, false); assert.equal("compressedMetadata" in area.outputs, false); assert.equal("compressedCesiumPreview" in area.outputs, false); fs.mkdirSync(area.outputs.geojsonDir, { recursive: true }); for (const file of [area.outputs.glb, area.outputs.metadata, area.outputs.cesiumPreview, area.outputs.vehicleRoute, area.outputs.vehicleModel]) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, "fixture\n"); } const lanePolygons = path.join(area.outputs.geojsonDir, "lane_polygons.geojson"); const emptyFeatureCollection = '{"type":"FeatureCollection","features":[]}\n'; const emptyNetwork = '{"roads":[],"intersections":[],"gps_bounds":{}}\n'; fs.writeFileSync(lanePolygons, emptyFeatureCollection); const network = path.join(area.outputs.geojsonDir, "network.json"); fs.writeFileSync(network, emptyNetwork); const intersectionSurface = path.join(area.outputs.geojsonDir, "intersection_surface.geojson"); fs.writeFileSync(intersectionSurface, emptyFeatureCollection); const previewCss = path.join(__dirname, "lib", "cesium-preview.css"); const previewJs = path.join(__dirname, "lib", "cesium-preview.js"); writeStageManifest(area, { stage: "preview", status: "ok", config: configPath, inputs: { config: fileRecord(configPath), osm: fileRecord(input), glb: fileRecord(area.outputs.glb), metadata: fileRecord(area.outputs.metadata), lanePolygons: fileRecord(lanePolygons), network: fileRecord(network), intersectionSurface: fileRecord(intersectionSurface), previewCss: fileRecord(previewCss), previewJs: fileRecord(previewJs), }, outputs: { cesiumPreview: fileRecord(area.outputs.cesiumPreview), vehicleRoute: fileRecord(area.outputs.vehicleRoute), vehicleModel: fileRecord(area.outputs.vehicleModel), }, summary: {}, warnings: [], }); let previewManifest = stageManifestStatus(area, configPath).find((manifest) => manifest.stage === "preview"); assert.equal(previewManifest.fresh, true); fs.appendFileSync(lanePolygons, " \n"); previewManifest = stageManifestStatus(area, configPath).find((manifest) => manifest.stage === "preview"); assert.equal(previewManifest.fresh, false); assert.ok(previewManifest.issues.some((issue) => issue.includes("lanePolygons"))); fs.writeFileSync(lanePolygons, emptyFeatureCollection); fs.appendFileSync(network, " \n"); previewManifest = stageManifestStatus(area, configPath).find((manifest) => manifest.stage === "preview"); assert.equal(previewManifest.fresh, false); assert.ok(previewManifest.issues.some((issue) => issue.includes("network"))); fs.writeFileSync(network, emptyNetwork); fs.appendFileSync(intersectionSurface, " \n"); previewManifest = stageManifestStatus(area, configPath).find((manifest) => manifest.stage === "preview"); assert.equal(previewManifest.fresh, false); assert.ok(previewManifest.issues.some((issue) => issue.includes("intersectionSurface"))); fs.rmSync(tempDir, { recursive: true, force: true }); console.log("Asset budget tests passed.");