#!/usr/bin/env node "use strict"; const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); const { cesiumPreviewHtml } = require("./lib/area-preview"); const { makeVehicleGltf } = require("./lib/vehicle-model"); const { VEHICLE_IDS, REVERSED_MODEL_IDS, writePreviewVehicleLibrary } = require("./lib/vehicle-library"); const { allowedTurns, buildVehicleRoute, classifyConnection } = require("./lib/vehicle-route"); const { buildTrafficSignals } = require("./lib/traffic-signals"); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "preview-assets-")); const osmPath = path.join(tempDir, "fixture.osm"); fs.writeFileSync(osmPath, ` `); const route = buildVehicleRoute(osmPath); assert.equal(route.source, osmPath); assert.deepEqual(route.bounds, { minLon: 120, minLat: 30, maxLon: 120.01, maxLat: 30.01, }); assert.equal(route.speedMetersPerSecond, 8); assert.equal(route.loop, true); assert.equal(route.segments, route.routes, "legacy segments remains an alias for the new route list"); assert.ok(route.routes.length >= 1 && route.routes.length <= 5); assert.ok(route.routes.every((segment) => segment.edgeIds.length >= 6)); assert.ok(route.routes.every((segment) => segment.maneuvers.includes("u_turn"))); assert.ok(route.routes.every((segment) => segment.maneuvers.some((value) => ["left", "right", "through"].includes(value)))); assert.ok(route.routes.every((segment) => JSON.stringify(segment.coordinates[0]) === JSON.stringify(segment.coordinates.at(-1)))); assert.ok(route.routes.every((segment) => !segment.edgeIds.includes("oneway-spur:backward"))); assert.notDeepEqual(route.routes[0].coordinates, route.routes[0].centerlineCoordinates); assert.deepEqual( [...allowedTurns({ "turn:lanes:forward": "left|through;right" }, "forward")].sort(), ["left", "right", "through"], ); const incoming = { wayId: "in", coordinates: [[120, 30], [120.001, 30]] }; assert.equal(classifyConnection(incoming, { wayId: "left", coordinates: [[120.001, 30], [120.001, 30.001]] }), "left"); assert.equal(classifyConnection(incoming, { wayId: "right", coordinates: [[120.001, 30], [120.001, 29.999]] }), "right"); assert.equal(classifyConnection(incoming, { wayId: "through", coordinates: [[120.001, 30], [120.002, 30]] }), "through"); const vehicle = makeVehicleGltf(); assert.equal(vehicle.asset.version, "2.0"); assert.equal(vehicle.asset.generator, "osm-asset-pipeline vehicle preview"); assert.equal(vehicle.scene, 0); assert.equal(vehicle.meshes.length, vehicle.nodes.length); assert.equal(vehicle.scenes[0].nodes.length, vehicle.nodes.length); assert.deepEqual( vehicle.materials.map((material) => material.name), ["paint red", "dark roof", "glass", "tire", "wheel hub", "headlight", "tail light"], ); assert.ok(vehicle.meshes.some((mesh) => mesh.name === "body")); assert.ok(vehicle.meshes.some((mesh) => mesh.name === "wheel_-1.55_-1.02")); assert.match(vehicle.buffers[0].uri, /^data:application\/octet-stream;base64,/); assert.equal( Buffer.from(vehicle.buffers[0].uri.split(",")[1], "base64").length, vehicle.buffers[0].byteLength, ); const vehicleLibraryRoot = path.join(__dirname, "..", "assets", "models", "custom", "lowpoly_cars"); if (fs.existsSync(vehicleLibraryRoot)) { const libraryDir = path.join(tempDir, "vehicle-library"); const models = writePreviewVehicleLibrary(libraryDir, "fixture"); assert.deepEqual(models, VEHICLE_IDS.map((id) => `fixture-vehicle-${id}.gltf`)); assert.equal(models.length, 6); assert.ok(fs.existsSync(path.join(libraryDir, "fixture-vehicle-texture.jpg"))); for (const model of models) { const gltf = JSON.parse(fs.readFileSync(path.join(libraryDir, model), "utf8")); assert.deepEqual(gltf.images.map((image) => image.uri), ["fixture-vehicle-texture.jpg"]); assert.equal(gltf.buffers[0].uri, model.replace(/\.gltf$/, ".bin")); assert.deepEqual(gltf.materials[0].pbrMetallicRoughness.baseColorFactor, [2.1, 2.1, 2.1, 1]); assert.deepEqual(gltf.materials[0].emissiveFactor, [0.25, 0.25, 0.25]); assert.equal(gltf.materials[0].emissiveTexture.index, gltf.materials[0].pbrMetallicRoughness.baseColorTexture.index); const reversed = model.includes("truck_a03_001"); assert.equal(reversed, REVERSED_MODEL_IDS.has(model.match(/vehicle-(.+)\.gltf$/)[1])); assert.equal(gltf.nodes.every((node) => JSON.stringify(node.rotation || []) === JSON.stringify([0, 1, 0, 0])), reversed); } } const html = cesiumPreviewHtml( "scene<&>.glb", "scene.json", "route.json", "vehicle.gltf", "north<&>\u2028valley", ["car-a.gltf", "truck-a.gltf"], "traffic-signals.json", ); assert.match(html, /north<&>\u2028valley Cesium Preview<\/title>/); assert.match(html, /Loading scene<&>\.glb/); assert.match(html, /"areaId":"north\\u003c\\u0026\\u003e\\u2028valley"/); assert.match(html, /"glbName":"scene\\u003c\\u0026\\u003e\.glb"/); assert.match(html, /"vehicleModelNames":\["car-a\.gltf","truck-a\.gltf"\]/); assert.match(html, /"trafficSignalsName":"traffic-signals\.json"/); assert.match(html, /id="toggleSignals"/); assert.match(html, /id="viewMode"/); assert.match(html, /data-view-mode="inspect"/); assert.match(html, /id="semanticToggles" class="control-subgroup hidden"/); const signals = buildTrafficSignals( { type: "FeatureCollection", features: [ rectangle(120.0000, 30.0000, 0.00003, 0.000006), rectangle(120.0002, 30.0000, 0.00003, 0.000006), ] }, { type: "FeatureCollection", features: [ { type: "Feature", geometry: { type: "Polygon", coordinates: [[ [119.9998, 29.9998], [120.0004, 29.9998], [120.0004, 30.0003], [119.9998, 30.0003], [119.9998, 29.9998], ]] } }, ] }, ); assert.equal(signals.version, 1); assert.equal(signals.signals.length, 2); assert.deepEqual(signals.signals.map((signal) => signal.phaseGroup), [0, 1]); assert.ok(signals.signals.every((signal) => Number.isFinite(signal.headingDegrees))); fs.rmSync(tempDir, { recursive: true, force: true }); console.log("Preview asset tests passed."); function rectangle(lon, lat, halfWidth, halfHeight) { return { type: "Feature", geometry: { type: "Polygon", coordinates: [[ [lon - halfWidth, lat - halfHeight], [lon + halfWidth, lat - halfHeight], [lon + halfWidth, lat + halfHeight], [lon - halfWidth, lat + halfHeight], [lon - halfWidth, lat - halfHeight], ]] } }; }