fix: render live V2X vehicles in Cesium preview
This commit is contained in:
@@ -1437,7 +1437,10 @@
|
||||
];
|
||||
if (v2xOverlay?.state) {
|
||||
lines.push("Vehicle data: " + (v2xOverlay.state.vehicleSource === "live" ? "live V2X" : "waiting for live V2X"));
|
||||
lines.push("Live vehicles: " + v2xOverlay.state.vehicles.size);
|
||||
lines.push("Live vehicles: " + (v2xOverlay.state?.registry?.visibleSize ?? 0));
|
||||
lines.push("Target WS: " + (v2xOverlay.state?.targetMessages ?? 0) + " messages / " + (v2xOverlay.state?.targetLastCount ?? 0) + " parsed");
|
||||
lines.push("Target payload: " + (v2xOverlay.state?.targetPayloadShape || "waiting"));
|
||||
if (v2xOverlay.state?.targetLastError) lines.push("Target error: " + v2xOverlay.state.targetLastError);
|
||||
}
|
||||
if (failed.length) {
|
||||
lines.push("Failed assets: " + failed.map((asset) => asset.url).join(", "));
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
const EARTH_A = 6378245.0;
|
||||
const EARTH_EE = 0.00669342162296594323;
|
||||
const TOKEN_KEY = "osm-asset-preview-v2x-token";
|
||||
const LIVE_VEHICLE_HEIGHT_METERS = 1.2;
|
||||
const LIVE_VEHICLE_HEADING_OFFSET_DEGREES = 90;
|
||||
|
||||
function createV2xCesiumOverlay(context) {
|
||||
const settings = context.config.v2xPreview || {};
|
||||
@@ -22,6 +24,13 @@
|
||||
vehicleSource: "waiting",
|
||||
parseFailures: 0,
|
||||
missingModels: new Set(),
|
||||
targetMessages: 0,
|
||||
targetVehiclesReceived: 0,
|
||||
targetLastCount: 0,
|
||||
targetLastError: "",
|
||||
targetPayloadType: "",
|
||||
targetPayloadSample: "",
|
||||
targetPayloadShape: "",
|
||||
};
|
||||
// This preview intentionally starts at the login gate. Do not revive a
|
||||
// previous tab's token and begin REST/WebSocket traffic before sign-in.
|
||||
@@ -47,6 +56,7 @@
|
||||
if (state.phaseBinding.unbound.length) parts.push(`${state.phaseBinding.unbound.length} unbound`);
|
||||
if (state.missingModels.size) parts.push(`${state.missingModels.size} models missing`);
|
||||
if (state.parseFailures) parts.push(`${state.parseFailures} unparsed pushes`);
|
||||
if (state.targetMessages) parts.push(`${state.targetVehiclesReceived} target vehicles`);
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
@@ -55,7 +65,6 @@
|
||||
state.entities = [];
|
||||
registry.list().forEach((record) => {
|
||||
if (record.entity) context.viewer.entities.remove(record.entity);
|
||||
if (record.trace) context.viewer.entities.remove(record.trace);
|
||||
});
|
||||
registry.clear();
|
||||
state.linkEntitiesByPhase.clear();
|
||||
@@ -264,12 +273,29 @@
|
||||
const targetIds = arrayValue(deviceConfig?.deviceConfig?.target);
|
||||
send(JSON.stringify({ deviceId: targetIds.length ? targetIds.join(",") : null }));
|
||||
}, (value) => {
|
||||
const push = normalizeTargetPush(value);
|
||||
if (!push.vehicles.length) { if (!isHeartbeat(value) && parseLoosePayload(value) === null) state.parseFailures += 1; return; }
|
||||
useLiveVehicles();
|
||||
registry.ingest(push.vehicles, push.interval);
|
||||
registry.sweep();
|
||||
renderVehicles();
|
||||
if (isHeartbeat(value)) return;
|
||||
state.targetMessages += 1;
|
||||
try {
|
||||
state.targetPayloadType = Object.prototype.toString.call(value);
|
||||
state.targetPayloadSample = typeof value === "string" ? value.slice(0, 800) : String(value).slice(0, 800);
|
||||
const payload = parseLoosePayload(value);
|
||||
const data = payload?.data;
|
||||
state.targetPayloadShape = data === null ? "data:null" : Array.isArray(data) ? `data:array(${data.length})` : typeof data === "object" ? `data:object(${Object.keys(data || {}).join(",")})` : `data:${typeof data}`;
|
||||
const vehicles = targetVehiclesFrom(payload);
|
||||
state.targetLastCount = vehicles.length;
|
||||
if (!vehicles.length) {
|
||||
if (payload === null) state.parseFailures += 1;
|
||||
return;
|
||||
}
|
||||
state.targetVehiclesReceived += vehicles.length;
|
||||
useLiveVehicles();
|
||||
registry.ingest(vehicles, payload?.interval);
|
||||
registry.sweep();
|
||||
renderVehicles();
|
||||
} catch (error) {
|
||||
state.targetLastError = String(error?.message || error);
|
||||
state.parseFailures += 1;
|
||||
}
|
||||
}, "Target vehicle WebSocket unavailable");
|
||||
}
|
||||
|
||||
@@ -370,10 +396,8 @@
|
||||
function syncVehicleEntity(viewer, record, state) {
|
||||
const [longitude, latitude] = gcj02ToWgs84([record.longitude, record.latitude]);
|
||||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return;
|
||||
const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, .65);
|
||||
const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, LIVE_VEHICLE_HEIGHT_METERS);
|
||||
if (!record.entity) {
|
||||
const tracePositions = [position];
|
||||
record.tracePositions = tracePositions;
|
||||
record.renderedPosition = position;
|
||||
record.targetPosition = position;
|
||||
record.sampleStart = Date.now();
|
||||
@@ -390,35 +414,27 @@
|
||||
() => vehicleOrientation(record.targetPosition, record.angle), false),
|
||||
model: {
|
||||
uri: vehicleModelUri(record, state),
|
||||
scale: 0.9,
|
||||
minimumPixelSize: 14,
|
||||
maximumScale: 2.0,
|
||||
// The full-intersection overview needs more than a couple of screen
|
||||
// pixels for a car to remain distinguishable from road markings.
|
||||
scale: 1.0,
|
||||
minimumPixelSize: 28,
|
||||
maximumScale: 8.0,
|
||||
show: arrayValue(state.vehicleModels).length > 0,
|
||||
},
|
||||
label: { text: record.label, font: "11px sans-serif", fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -16) },
|
||||
});
|
||||
record.trace = viewer.entities.add({
|
||||
name: `${record.label} live trace`,
|
||||
polyline: { positions: new Cesium.CallbackProperty(() => record.tracePositions, false), width: 3, material: record.kind === "obu" ? Cesium.Color.LIME : Cesium.Color.ORANGE, arcType: Cesium.ArcType.NONE },
|
||||
});
|
||||
} else {
|
||||
record.renderedPosition = record.targetPosition || position;
|
||||
record.targetPosition = position;
|
||||
record.sampleStart = Date.now();
|
||||
record.tracePositions.push(position);
|
||||
if (record.tracePositions.length > 24) record.tracePositions.shift();
|
||||
if (record.entity.label) record.entity.label.text = record.label;
|
||||
}
|
||||
// Stale vehicles are hidden, not removed, so the slot stays reusable.
|
||||
record.entity.show = record.visible;
|
||||
record.trace.show = record.visible;
|
||||
if (!record.visible) record.tracePositions.length = 0;
|
||||
}
|
||||
|
||||
function vehicleOrientation(position, angle) {
|
||||
return Cesium.Transforms.headingPitchRollQuaternion(
|
||||
position,
|
||||
new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(Number(angle) || 0), 0, 0),
|
||||
new Cesium.HeadingPitchRoll(Cesium.Math.toRadians((Number(angle) || 0) + LIVE_VEHICLE_HEADING_OFFSET_DEGREES), 0, 0),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -441,7 +457,7 @@
|
||||
function relaxJson(text) {
|
||||
return text
|
||||
.replace(/'/g, "\"")
|
||||
.replace(/([{,]\s*)([A-Za-z_$][\w$]*)\s*:/g, "$1\"$2\":")
|
||||
.replace(/([{,]\s*)([A-Za-z_$][\w$]*|\d+)\s*:/g, "$1\"$2\":")
|
||||
.replace(/,\s*([}\]])/g, "$1")
|
||||
.replace(/:\s*(NaN|-?Infinity)\s*([,}\]])/g, ": null$2");
|
||||
}
|
||||
@@ -620,7 +636,21 @@
|
||||
// dashboard's onConnected. A reconnect without this receives nothing.
|
||||
settings.onOpen?.({ send });
|
||||
};
|
||||
socket.onmessage = (event) => settings.onMessage?.(event?.data);
|
||||
socket.onmessage = (event) => {
|
||||
const payload = event?.data;
|
||||
// The upstream sometimes marks UTF-8 JSON as a binary WS frame. DevTools
|
||||
// renders that Blob as JSON, but JSON.parse cannot consume a Blob; decode
|
||||
// it before applying the same dashboard payload parser.
|
||||
if (typeof Blob !== "undefined" && payload instanceof Blob) {
|
||||
payload.text().then((text) => settings.onMessage?.(text)).catch((error) => settings.onError?.(error));
|
||||
return;
|
||||
}
|
||||
if (typeof ArrayBuffer !== "undefined" && payload instanceof ArrayBuffer) {
|
||||
settings.onMessage?.(new TextDecoder().decode(payload));
|
||||
return;
|
||||
}
|
||||
settings.onMessage?.(payload);
|
||||
};
|
||||
socket.onerror = (event) => settings.onError?.(event);
|
||||
socket.onclose = (event) => {
|
||||
stopHeartbeat();
|
||||
@@ -702,11 +732,11 @@
|
||||
}).filter(Boolean));
|
||||
}
|
||||
|
||||
// OBU vehicles use a fixed model; targets are keyed by type/subType. Mirrors
|
||||
// useObuCars.ts ('car_obu.glb') and useTargetCars.ts (`${type}${subType}.glb`).
|
||||
// The dashboard's CrossCars template renders OBU with 11.glb, while target
|
||||
// vehicles retain their `${type}${subType}.glb` model selection.
|
||||
function modelNameFor(vehicle) {
|
||||
if (!vehicle) return "";
|
||||
if (vehicle.kind === "obu") return "car_obu.glb";
|
||||
if (vehicle.kind === "obu") return "11.glb";
|
||||
return `${vehicle.type || 1}${vehicle.subType || 1}.glb`;
|
||||
}
|
||||
|
||||
@@ -777,6 +807,7 @@
|
||||
list: () => records.slice(),
|
||||
get interval() { return interval; },
|
||||
get size() { return records.length; },
|
||||
get visibleSize() { return records.filter((record) => record.visible).length; },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,12 @@ const VEHICLE_IDS = [
|
||||
"truck_a03_001",
|
||||
];
|
||||
const LIBRARY_ROOT = path.resolve(__dirname, "..", "..", "assets", "models", "custom", "lowpoly_cars");
|
||||
const V2X_LIBRARY_ROOT = path.resolve(__dirname, "..", "..", "assets", "models", "v2x-vehicles");
|
||||
const TEXTURE_NAME = "color_512x512.jpg";
|
||||
const REVERSED_MODEL_IDS = new Set(["truck_a03_001"]);
|
||||
// These are the dashboard's published vehicle assets. OBU rendering uses
|
||||
// 11.glb in CrossCars/index.vue; target vehicles use `${type}${subType}.glb`.
|
||||
const V2X_VEHICLE_MODEL_NAMES = ["11.glb", "12.glb", "13.glb", "14.glb", "15.glb", "16.glb", "17.glb", "31.glb", "221.glb", "222.glb"];
|
||||
|
||||
function writePreviewVehicleLibrary(outDir, fileStem) {
|
||||
const textureOutput = `${fileStem}-vehicle-texture.jpg`;
|
||||
@@ -46,4 +50,21 @@ function writePreviewVehicleLibrary(outDir, fileStem) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { VEHICLE_IDS, REVERSED_MODEL_IDS, writePreviewVehicleLibrary };
|
||||
function writePreviewV2xVehicleLibrary(outDir) {
|
||||
const targetDir = path.join(outDir, "v2x-vehicles");
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
for (const modelName of V2X_VEHICLE_MODEL_NAMES) {
|
||||
const source = path.join(V2X_LIBRARY_ROOT, modelName);
|
||||
if (!fs.existsSync(source)) throw new Error(`V2X vehicle model not found: ${source}`);
|
||||
fs.copyFileSync(source, path.join(targetDir, modelName));
|
||||
}
|
||||
return V2X_VEHICLE_MODEL_NAMES.map((modelName) => `v2x-vehicles/${modelName}`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
VEHICLE_IDS,
|
||||
REVERSED_MODEL_IDS,
|
||||
V2X_VEHICLE_MODEL_NAMES,
|
||||
writePreviewVehicleLibrary,
|
||||
writePreviewV2xVehicleLibrary,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user