The ported overlay used the correct REST paths but lost the data handling
from the source dashboard's live-intersection view (HologramCross), so
signals rendered permanently red and vehicles often never appeared.
- Lamp status codes now follow the dashboard dictionary (11/21/22/23/31).
The previous 2/3 reading made every real push fall through to red. The
dictionary lives only in the overlay; the preview consumes normalized
{nodeKeys, color, countDown} entries so the two copies cannot drift.
- Bind V2X phases to native signal heads geometrically. The runtime
document has no phaseNo, so the old lookup fell back to signal.id and
never matched, leaving the dynamic assembly dark. Travel heading is
recovered as faceHeadingDegrees + 180, per the generator's
mast = travel - 90 / face = travel + 180. Verified 7/7 exact matches
against the fengshu-er-road runtime document.
- A phase now lights every approach it drives; the phase -> single entity
map silently overwrote all but the last.
- Drive the countdown assets from the push's countDown field.
- All three sockets heartbeat every 30s and reconnect with backoff,
replaying their subscription frame. Without this the service dropped
the connection and the scene emptied after about a minute.
- The OBU socket sends its bounds frame on connect and on camera move;
it previously sent nothing at all.
- Vehicles are swept when a push goes stale and their slots reused, so
they no longer accumulate as ghosts. Models follow the dashboard's
car_obu.glb / ${type}${subType}.glb naming.
- Parse vehicle pushes leniently, since the dashboard uses saferEval and
the payload is not guaranteed to be strict JSON. Failures are counted
and surfaced rather than dropped; no eval is introduced.
- Drop FlowTravelRatio/queryListWeek, which is not part of this view.
Also corrects a stale spec rule that required vehicleModelNames to be
empty. Live V2X vehicles need packaged models; the real invariant is no
generated routes or traffic simulation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
6.2 KiB
JavaScript
146 lines
6.2 KiB
JavaScript
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
function writeCesiumPreviewSupportFiles(outDir) {
|
|
const files = [
|
|
[path.join(__dirname, "cesium-preview.css"), "cesium-preview.css"],
|
|
[path.join(__dirname, "cesium-preview.js"), "cesium-preview.js"],
|
|
[path.join(__dirname, "v2x-cesium-overlay.js"), "v2x-cesium-overlay.js"],
|
|
[path.join(__dirname, "..", "..", "assets", "preview", "vehicle-breakdown.png"), "vehicle-breakdown.png"],
|
|
[path.join(__dirname, "..", "..", "assets", "preview", "vehicle-accident.png"), "vehicle-accident.png"],
|
|
];
|
|
for (const [source, file] of files) {
|
|
if (!fs.existsSync(source)) {
|
|
throw new Error(`Cesium preview support file '${file}' not found: ${source}`);
|
|
}
|
|
fs.copyFileSync(source, path.join(outDir, file));
|
|
}
|
|
}
|
|
|
|
function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, areaId, vehicleModelNames = [], trafficSignalsName = null, previewDescriptorName = null, v2xPreview = null) {
|
|
const previewConfig = {
|
|
areaId,
|
|
glbName,
|
|
metadataName,
|
|
routeName,
|
|
vehicleModelName,
|
|
vehicleModelNames,
|
|
trafficSignalsName,
|
|
previewDescriptorName,
|
|
v2xPreview,
|
|
};
|
|
return `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>${escapeHtml(areaId)} Cesium Preview</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/cesium@1.121.1/Build/Cesium/Widgets/widgets.css" rel="stylesheet" onerror="this.onerror=null;this.href='https://unpkg.com/cesium@1.121.1/Build/Cesium/Widgets/widgets.css'">
|
|
<script src="https://cdn.jsdelivr.net/npm/cesium@1.121.1/Build/Cesium/Cesium.js" onerror="this.onerror=null;this.src='https://unpkg.com/cesium@1.121.1/Build/Cesium/Cesium.js';"></script>
|
|
<link href="cesium-preview.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div id="cesiumContainer"></div>
|
|
<div id="controls">
|
|
<div class="control-group">
|
|
<button id="toggleCruise">Pause</button>
|
|
<button id="toggleFollow">Follow</button>
|
|
<label>Vehicle <select id="vehicleSelect"></select></label>
|
|
<label>Speed <input id="speedControl" type="range" min="2" max="22" step="1" value="8"></label>
|
|
<span id="speedLabel">8 m/s</span>
|
|
</div>
|
|
<div class="control-group">
|
|
<span class="control-label">View</span>
|
|
<span id="viewMode" class="segmented-control" role="group" aria-label="Scene view mode">
|
|
<button type="button" data-view-mode="scene" aria-pressed="true">Scene</button>
|
|
<button type="button" data-view-mode="inspect" aria-pressed="false">Inspect</button>
|
|
</span>
|
|
<label><input id="toggleScene" type="checkbox" checked> Scene</label>
|
|
<label><input id="toggleBuildingGhost" type="checkbox"> Building ghost</label>
|
|
<span id="assetToggles" class="control-subgroup"></span>
|
|
<span id="semanticToggles" class="control-subgroup hidden"></span>
|
|
<label><input id="toggleRoutes" type="checkbox" checked> Routes</label>
|
|
<label><input id="toggleVehicles" type="checkbox" checked> Vehicles</label>
|
|
<label id="signalsControl"><input id="toggleSignals" type="checkbox" checked> Signals</label>
|
|
<label><input id="toggleFps" type="checkbox"> FPS</label>
|
|
<label><input id="toggleDiagnostics" type="checkbox" checked> Info</label>
|
|
</div>
|
|
<div class="control-group">
|
|
<button data-camera="overview">Overview</button>
|
|
<button data-camera="oblique">Oblique</button>
|
|
<button data-camera="detail">Detail</button>
|
|
<button data-camera="route">Route</button>
|
|
</div>
|
|
</div>
|
|
<div id="diagnostics">Loading diagnostics...</div>
|
|
<div id="status">Loading ${escapeHtml(glbName)}...</div>
|
|
<aside id="vehicleInfoCard" class="hidden" aria-live="polite">
|
|
<div class="vehicle-card-heading">
|
|
<strong id="vehicleInfoTitle">Vehicle</strong>
|
|
<button id="closeVehicleInfo" type="button" aria-label="Close vehicle information">Close</button>
|
|
</div>
|
|
<dl id="vehicleInfoDetails"></dl>
|
|
<label class="vehicle-card-field">Event note
|
|
<textarea id="vehicleIncidentNote" rows="2" maxlength="160" placeholder="Optional note"></textarea>
|
|
</label>
|
|
<div class="vehicle-card-actions" role="group" aria-label="Vehicle status">
|
|
<button type="button" data-vehicle-status="normal">Normal</button>
|
|
<button type="button" data-vehicle-status="breakdown">Breakdown</button>
|
|
<button type="button" data-vehicle-status="accident">Accident</button>
|
|
</div>
|
|
</aside>
|
|
<div id="loadingOverlay">
|
|
<div class="loading-card">
|
|
<div class="loading-spinner" aria-hidden="true"></div>
|
|
<div class="loading-copy">
|
|
<strong>Loading scene</strong>
|
|
<span>${escapeHtml(areaId)}</span>
|
|
</div>
|
|
<div class="loading-bar" aria-hidden="true"><span></span></div>
|
|
</div>
|
|
</div>
|
|
<script>window.OSM_ASSET_PREVIEW_CONFIG = ${escapeScriptJson(JSON.stringify(previewConfig))};</script>
|
|
<script src="v2x-cesium-overlay.js"></script>
|
|
<script src="cesium-preview.js"></script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|
|
|
|
function escapeScriptJson(value) {
|
|
return String(value)
|
|
.replaceAll("<", "\\u003c")
|
|
.replaceAll(">", "\\u003e")
|
|
.replaceAll("&", "\\u0026")
|
|
.replaceAll("\u2028", "\\u2028")
|
|
.replaceAll("\u2029", "\\u2029");
|
|
}
|
|
|
|
function previewSummary(area, routePath = area.outputs.vehicleRoute) {
|
|
const route = routePath && fs.existsSync(routePath) ? JSON.parse(fs.readFileSync(routePath, "utf8")) : null;
|
|
return {
|
|
glbName: "package/manifest.json",
|
|
metadataName: "package/manifest.json",
|
|
routeName: route ? path.relative(area.outputs.areaDir, routePath).split(path.sep).join("/") : null,
|
|
vehicleModelName: path.relative(area.outputs.areaDir, area.outputs.vehicleModel).split(path.sep).join("/"),
|
|
trafficSignalsName: "package/runtime/traffic-signals.json",
|
|
routeSegments: Array.isArray(route?.routes) ? route.routes.length : Array.isArray(route?.segments) ? route.segments.length : null,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
cesiumPreviewHtml,
|
|
previewSummary,
|
|
writeCesiumPreviewSupportFiles,
|
|
};
|