fix: use live V2X vehicles only in preview
This commit is contained in:
@@ -13,11 +13,13 @@
|
||||
const state = {
|
||||
token: sessionStorage.getItem(TOKEN_KEY) || "",
|
||||
entities: [],
|
||||
vehicles: new Map(),
|
||||
linkPhases: new Map(),
|
||||
socket: null,
|
||||
sockets: [],
|
||||
status: "Sign in to load live V2X data.",
|
||||
crossCode: settings.crossCode || "",
|
||||
metrics: null,
|
||||
vehicleSource: "waiting",
|
||||
};
|
||||
const ui = buildUi(state, settings);
|
||||
|
||||
@@ -29,14 +31,25 @@
|
||||
function clearEntities() {
|
||||
state.entities.forEach((entity) => context.viewer.entities.remove(entity));
|
||||
state.entities = [];
|
||||
state.vehicles.forEach((vehicle) => {
|
||||
context.viewer.entities.remove(vehicle.entity);
|
||||
context.viewer.entities.remove(vehicle.trace);
|
||||
});
|
||||
state.vehicles.clear();
|
||||
state.linkPhases.clear();
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (state.socket) state.socket.close();
|
||||
state.socket = null;
|
||||
state.sockets.forEach((socket) => socket.close());
|
||||
state.sockets = [];
|
||||
}
|
||||
|
||||
function useLiveVehicles() {
|
||||
if (state.vehicleSource === "live") return;
|
||||
state.vehicleSource = "live";
|
||||
}
|
||||
state.activateLiveVehicles = useLiveVehicles;
|
||||
|
||||
async function request(path) {
|
||||
const response = await fetch(joinUrl(settings.apiBaseUrl || "/api", path), {
|
||||
headers: state.token ? { Authorization: state.token } : {},
|
||||
@@ -77,6 +90,7 @@
|
||||
function signOut(message) {
|
||||
disconnect();
|
||||
clearEntities();
|
||||
state.vehicleSource = "waiting";
|
||||
state.token = "";
|
||||
sessionStorage.removeItem(TOKEN_KEY);
|
||||
ui.form.hidden = false;
|
||||
@@ -92,6 +106,7 @@
|
||||
}
|
||||
state.crossCode = crossCode;
|
||||
clearEntities();
|
||||
state.vehicleSource = "waiting";
|
||||
setStatus("Loading live V2X intersection data...");
|
||||
const results = await Promise.allSettled([
|
||||
request(`/network/api/link/network/queryCrossLinkInfo/${encodeURIComponent(crossCode)}`),
|
||||
@@ -111,25 +126,55 @@
|
||||
const targetCount = deviceConfig.status === "fulfilled" ? arrayValue(deviceConfig.value?.deviceConfig?.target).length : 0;
|
||||
state.metrics = flow.status === "fulfilled" ? flow.value : null;
|
||||
if (flow.status !== "fulfilled") warnings.push("traffic metrics unavailable");
|
||||
connectSignalSocket();
|
||||
connectLiveSockets(deviceConfig.status === "fulfilled" ? deviceConfig.value : null);
|
||||
const linkCount = links.status === "fulfilled" ? arrayValue(links.value?.inLinkList).length : 0;
|
||||
const metricCount = arrayValue(state.metrics).length;
|
||||
setStatus(`Live V2X: ${linkCount} links, ${deviceCount} devices, ${poleCount} poles, ${targetCount} configured targets, ${metricCount} flow metrics. GCJ-02 -> WGS84 once.${warnings.length ? ` ${warnings.join(", ")}.` : ""}`);
|
||||
}
|
||||
|
||||
function connectSignalSocket() {
|
||||
function connectLiveSockets(deviceConfig) {
|
||||
disconnect();
|
||||
const socketUrl = toWebSocketUrl(joinUrl(settings.wsBaseUrl || "/websocket", "/network/ws/network/signal"), state.token);
|
||||
connectSignalSocket();
|
||||
connectObuSocket();
|
||||
connectTargetSocket(deviceConfig);
|
||||
}
|
||||
|
||||
function openSocket(path, onOpen, onMessage, unavailable) {
|
||||
const socketUrl = toWebSocketUrl(joinUrl(settings.wsBaseUrl || "/websocket", path), state.token);
|
||||
try {
|
||||
state.socket = new WebSocket(socketUrl);
|
||||
state.socket.onopen = () => state.socket?.send(JSON.stringify({ junctionId: state.crossCode }));
|
||||
state.socket.onmessage = (event) => updateSignalPhases(event.data, state);
|
||||
state.socket.onerror = () => setStatus(`${state.status} Signal WebSocket unavailable.`);
|
||||
const socket = new WebSocket(socketUrl);
|
||||
state.sockets.push(socket);
|
||||
socket.onopen = () => onOpen?.(socket);
|
||||
socket.onmessage = (event) => onMessage(event.data);
|
||||
socket.onerror = () => setStatus(`${state.status} ${unavailable}.`);
|
||||
return socket;
|
||||
} catch (_) {
|
||||
setStatus(`${state.status} Signal WebSocket unavailable.`);
|
||||
setStatus(`${state.status} ${unavailable}.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function connectSignalSocket() {
|
||||
openSocket("/network/ws/network/signal", (socket) => socket.send(JSON.stringify({ junctionId: state.crossCode })),
|
||||
(value) => updateSignalPhases(value, state), "Signal WebSocket unavailable");
|
||||
}
|
||||
|
||||
function connectObuSocket() {
|
||||
openSocket("/network/ws/network/obuPosition", null, (value) => {
|
||||
const vehicle = normalizeObuVehicle(value);
|
||||
if (vehicle) updateLiveVehicle(context.viewer, vehicle, state);
|
||||
}, "OBU vehicle WebSocket unavailable");
|
||||
}
|
||||
|
||||
function connectTargetSocket(deviceConfig) {
|
||||
openSocket("/network/ws/network/targetPosition", (socket) => {
|
||||
const targetIds = arrayValue(deviceConfig?.deviceConfig?.target);
|
||||
socket.send(JSON.stringify({ deviceId: targetIds.length ? targetIds.join(",") : null }));
|
||||
}, (value) => {
|
||||
normalizeTargetVehicles(value).forEach((vehicle) => updateLiveVehicle(context.viewer, vehicle, state));
|
||||
}, "Target vehicle WebSocket unavailable");
|
||||
}
|
||||
|
||||
ui.form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
@@ -147,13 +192,13 @@
|
||||
ui.live.hidden = false;
|
||||
loadLiveData().catch((error) => setStatus(error.message || "Live V2X data unavailable"));
|
||||
}
|
||||
return { state, load: loadLiveData, signOut, dispose: () => { disconnect(); clearEntities(); ui.root.remove(); } };
|
||||
return { state, load: loadLiveData, signOut, dispose: () => { disconnect(); clearEntities(); state.vehicleSource = "waiting"; ui.root.remove(); } };
|
||||
}
|
||||
|
||||
function buildUi(state, settings) {
|
||||
const root = document.createElement("aside");
|
||||
root.id = "v2xPanel";
|
||||
root.innerHTML = `<div class="v2x-heading"><strong>Live V2X Overlay</strong><span>Native: WGS84/ENU | V2X: GCJ-02</span></div>
|
||||
root.innerHTML = `<div class="v2x-heading"><strong>Live V2X Data</strong><span>Native: WGS84/ENU | V2X: GCJ-02</span></div>
|
||||
<form id="v2xLoginForm"><label>Username<input name="userName" autocomplete="username" required></label><label>Password<input name="password" type="password" autocomplete="current-password" required></label><button type="submit">Sign in</button></form>
|
||||
<div id="v2xLiveControls" hidden><label>Intersection code<input name="crossCode" value="${escapeAttribute(settings.crossCode || "")}" required></label><div><button type="button" data-action="reload">Refresh</button><button type="button" data-action="signout">Sign out</button></div></div>
|
||||
<output id="v2xStatus"></output>`;
|
||||
@@ -202,6 +247,64 @@
|
||||
});
|
||||
}
|
||||
|
||||
function updateLiveVehicle(viewer, vehicle, state) {
|
||||
const [longitude, latitude] = gcj02ToWgs84([vehicle.longitude, vehicle.latitude]);
|
||||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return;
|
||||
state.activateLiveVehicles?.();
|
||||
const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, .65);
|
||||
let record = state.vehicles.get(vehicle.id);
|
||||
if (!record) {
|
||||
const tracePositions = [position];
|
||||
record = {
|
||||
tracePositions,
|
||||
entity: viewer.entities.add({
|
||||
name: vehicle.label,
|
||||
position,
|
||||
point: { pixelSize: 10, color: vehicle.kind === "obu" ? Cesium.Color.LIME : Cesium.Color.ORANGE, outlineColor: Cesium.Color.BLACK, outlineWidth: 1 },
|
||||
label: { text: vehicle.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) },
|
||||
}),
|
||||
trace: viewer.entities.add({
|
||||
name: `${vehicle.label} live trace`,
|
||||
polyline: { positions: new Cesium.CallbackProperty(() => tracePositions, false), width: 3, material: vehicle.kind === "obu" ? Cesium.Color.LIME : Cesium.Color.ORANGE, arcType: Cesium.ArcType.NONE },
|
||||
}),
|
||||
};
|
||||
state.vehicles.set(vehicle.id, record);
|
||||
} else {
|
||||
record.entity.position = position;
|
||||
record.tracePositions.push(position);
|
||||
if (record.tracePositions.length > 24) record.tracePositions.shift();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function parseSocketJson(value) {
|
||||
if (typeof value !== "string" || value.includes('"heartBeat":"pong"')) return null;
|
||||
try { return JSON.parse(value); } catch (_) { return null; }
|
||||
}
|
||||
|
||||
function normalizeObuVehicle(value) {
|
||||
const source = parseSocketJson(value);
|
||||
const longitude = Number(source?.lon);
|
||||
const latitude = Number(source?.lat);
|
||||
const code = source?.carCode || source?.obuCode;
|
||||
if (!code || !Number.isFinite(longitude) || !Number.isFinite(latitude)) return null;
|
||||
return { id: `obu-${code}`, label: source.plateNumber || String(code), kind: "obu", longitude, latitude, angle: Number(source.angle) || 0, speed: Number(source.speed) || 0 };
|
||||
}
|
||||
|
||||
function normalizeTargetVehicles(value) {
|
||||
const source = parseSocketJson(value);
|
||||
const devices = source?.data;
|
||||
if (!devices || typeof devices !== "object" || Array.isArray(devices)) return [];
|
||||
return Object.keys(devices).flatMap((deviceId) => arrayValue(devices[deviceId]).map((target) => {
|
||||
const longitude = Number(target?.longitude);
|
||||
const latitude = Number(target?.latitude);
|
||||
if (!target?.id || !Number.isFinite(longitude) || !Number.isFinite(latitude)) return null;
|
||||
const type = target.type || 1;
|
||||
const subType = target.subType || 1;
|
||||
return { id: `${deviceId}-${target.id}-${type}${subType}`, label: target.plate || `${deviceId}-${target.id}`, kind: "target", longitude, latitude, angle: Number(target.angle) || 0, speed: Number(target.speed) || 0 };
|
||||
}).filter(Boolean));
|
||||
}
|
||||
|
||||
function updateSignalPhases(value, state) {
|
||||
let lamps;
|
||||
try { lamps = JSON.parse(value).lamps; } catch (_) { return; }
|
||||
@@ -275,6 +378,6 @@
|
||||
function rotate(value, count) { return (value << count) | (value >>> (32 - count)); }
|
||||
function hex(value) { let output = ""; for (let index = 0; index < 4; index += 1) output += (`0${(value >>> (index * 8) & 255).toString(16)}`).slice(-2); return output; }
|
||||
|
||||
createV2xCesiumOverlay.utils = { gcj02ToWgs84, joinUrl, md5, toWebSocketUrl };
|
||||
createV2xCesiumOverlay.utils = { gcj02ToWgs84, joinUrl, md5, toWebSocketUrl, normalizeObuVehicle, normalizeTargetVehicles };
|
||||
window.createV2xCesiumOverlay = createV2xCesiumOverlay;
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user