feat(preview): add vehicle incident cards
This commit is contained in:
@@ -16,6 +16,12 @@
|
||||
const toggleFps = document.getElementById("toggleFps");
|
||||
const toggleDiagnostics = document.getElementById("toggleDiagnostics");
|
||||
const assetToggles = document.getElementById("assetToggles");
|
||||
const vehicleInfoCard = document.getElementById("vehicleInfoCard");
|
||||
const vehicleInfoTitle = document.getElementById("vehicleInfoTitle");
|
||||
const vehicleInfoDetails = document.getElementById("vehicleInfoDetails");
|
||||
const vehicleIncidentNote = document.getElementById("vehicleIncidentNote");
|
||||
const closeVehicleInfo = document.getElementById("closeVehicleInfo");
|
||||
const vehicleStatusButtons = Array.from(document.querySelectorAll("[data-vehicle-status]"));
|
||||
// The exported road surface sits at the 0.35m scene anchor plus 0.03m.
|
||||
// Vehicle models have their wheels at local Y=0, so keep them just clear
|
||||
// of the asphalt instead of using the old visibly floating 1.15m height.
|
||||
@@ -56,6 +62,7 @@
|
||||
buildAssetToggles(assets);
|
||||
buildSemanticToggles(viewer, assets, placement);
|
||||
bindRuntimeControls(viewer, assets, cruise, cameras, placement, trafficSignals);
|
||||
bindVehicleInfoCard(viewer, cruise);
|
||||
startDiagnostics(viewer, metadata, assets, cruise, placement, trafficSignals);
|
||||
cameras.overview();
|
||||
baseStatus = summaryText(metadata, assets, cruise);
|
||||
@@ -726,7 +733,8 @@
|
||||
|
||||
function addCruiseVehicle(viewer, segment, index, start, speed, signalData, vehicleModelName) {
|
||||
const route = prepareRoute(segment, signalData);
|
||||
const trafficMotion = createTrafficAwarePositions(viewer, route, start, speed);
|
||||
let record = null;
|
||||
const trafficMotion = createTrafficAwarePositions(viewer, route, start, speed, () => record?.status === "normal");
|
||||
const positions = trafficMotion.positions;
|
||||
const flat = [];
|
||||
for (const coord of segment.coordinates) {
|
||||
@@ -761,14 +769,41 @@
|
||||
minimumPixelSize: 0
|
||||
}
|
||||
});
|
||||
return {
|
||||
record = {
|
||||
id: "vehicle-" + (index + 1),
|
||||
entity: vehicle,
|
||||
routeEntity,
|
||||
positions,
|
||||
route,
|
||||
segment,
|
||||
label: routeLabel(segment, index),
|
||||
modelName: vehicleModelLabel(vehicleModelName),
|
||||
status: "normal",
|
||||
incidentNote: "",
|
||||
motion: trafficMotion,
|
||||
routeColor,
|
||||
markerEntity: null,
|
||||
};
|
||||
vehicle.properties = new Cesium.PropertyBag({ vehicleId: record.id });
|
||||
record.markerEntity = viewer.entities.add({
|
||||
name: "Vehicle incident marker " + (index + 1),
|
||||
position: positions,
|
||||
billboard: {
|
||||
image: "",
|
||||
show: false,
|
||||
width: 36,
|
||||
height: 44,
|
||||
pixelOffset: new Cesium.Cartesian2(0, -40),
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
});
|
||||
return record;
|
||||
}
|
||||
|
||||
function vehicleModelLabel(name) {
|
||||
const file = String(name || "").split("/").pop().replace(/\.gltf$/i, "");
|
||||
return file.replace(/^.*-vehicle-/, "").replaceAll("_", " ");
|
||||
}
|
||||
|
||||
function routeLabel(route, index) {
|
||||
@@ -789,6 +824,96 @@
|
||||
return cruise.vehicles[cruise.state.selectedIndex] || cruise.vehicles[0];
|
||||
}
|
||||
|
||||
function vehicleById(cruise, id) {
|
||||
return cruise.vehicles.find((vehicle) => vehicle.id === id) || null;
|
||||
}
|
||||
|
||||
function setVehicleStatus(viewer, vehicle, status) {
|
||||
if (!vehicle || !["normal", "breakdown", "accident"].includes(status)) return;
|
||||
vehicle.status = status;
|
||||
const incident = status !== "normal";
|
||||
const accident = status === "accident";
|
||||
vehicle.markerEntity.billboard.image = status === "breakdown"
|
||||
? "vehicle-breakdown.png"
|
||||
: accident ? "vehicle-accident.png" : "";
|
||||
vehicle.markerEntity.billboard.show = incident;
|
||||
vehicle.routeEntity.polyline.material = accident ? Cesium.Color.RED : vehicle.routeColor;
|
||||
if (viewer.scene.requestRender) viewer.scene.requestRender();
|
||||
}
|
||||
|
||||
function bindVehicleInfoCard(viewer, cruise) {
|
||||
if (!vehicleInfoCard || !cruise.vehicles.length) return;
|
||||
let activeVehicle = null;
|
||||
const render = () => {
|
||||
if (!activeVehicle) return;
|
||||
vehicleInfoTitle.textContent = activeVehicle.label;
|
||||
vehicleIncidentNote.value = activeVehicle.incidentNote;
|
||||
const details = [
|
||||
["Vehicle", activeVehicle.id],
|
||||
["Model", activeVehicle.modelName],
|
||||
["Route", activeVehicle.label],
|
||||
["Speed", cruise.baseSpeed + " m/s"],
|
||||
["Status", activeVehicle.status],
|
||||
];
|
||||
vehicleInfoDetails.replaceChildren(...details.flatMap(([term, value]) => {
|
||||
const dt = document.createElement("dt");
|
||||
const dd = document.createElement("dd");
|
||||
dt.textContent = term;
|
||||
dd.textContent = value;
|
||||
return [dt, dd];
|
||||
}));
|
||||
for (const button of vehicleStatusButtons) {
|
||||
button.disabled = button.dataset.vehicleStatus === activeVehicle.status;
|
||||
}
|
||||
};
|
||||
const open = (vehicle) => {
|
||||
activeVehicle = vehicle;
|
||||
const index = cruise.vehicles.indexOf(vehicle);
|
||||
cruise.state.selectedIndex = index;
|
||||
vehicleSelect.value = String(index);
|
||||
syncSelectedRouteVisibility(cruise);
|
||||
vehicleInfoCard.classList.remove("hidden");
|
||||
render();
|
||||
};
|
||||
const positionCard = () => {
|
||||
if (!activeVehicle || vehicleInfoCard.classList.contains("hidden")) return;
|
||||
const position = activeVehicle.entity.position?.getValue(viewer.clock.currentTime);
|
||||
const windowPosition = position && Cesium.SceneTransforms.worldToWindowCoordinates(viewer.scene, position);
|
||||
if (!windowPosition) {
|
||||
vehicleInfoCard.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
const margin = 12;
|
||||
const width = vehicleInfoCard.offsetWidth || 300;
|
||||
const x = Cesium.Math.clamp(windowPosition.x, margin + width / 2, window.innerWidth - margin - width / 2);
|
||||
vehicleInfoCard.style.left = x + "px";
|
||||
vehicleInfoCard.style.top = Math.max(windowPosition.y, margin + 20) + "px";
|
||||
};
|
||||
closeVehicleInfo.addEventListener("click", () => {
|
||||
activeVehicle = null;
|
||||
vehicleInfoCard.classList.add("hidden");
|
||||
});
|
||||
vehicleIncidentNote.addEventListener("input", () => {
|
||||
if (activeVehicle) activeVehicle.incidentNote = vehicleIncidentNote.value.trim();
|
||||
});
|
||||
for (const button of vehicleStatusButtons) {
|
||||
button.addEventListener("click", () => {
|
||||
if (!activeVehicle) return;
|
||||
setVehicleStatus(viewer, activeVehicle, button.dataset.vehicleStatus);
|
||||
render();
|
||||
setStatus(activeVehicle.label + " " + activeVehicle.status);
|
||||
});
|
||||
}
|
||||
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||
handler.setInputAction((movement) => {
|
||||
const picked = viewer.scene.pick(movement.position);
|
||||
const id = picked?.id?.properties?.vehicleId?.getValue?.();
|
||||
const vehicle = vehicleById(cruise, id);
|
||||
if (vehicle) open(vehicle);
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
viewer.scene.postRender.addEventListener(positionCard);
|
||||
}
|
||||
|
||||
function prepareRoute(segment, signalData) {
|
||||
const distances = [0.0];
|
||||
for (let i = 1; i < segment.coordinates.length; i += 1) {
|
||||
@@ -820,12 +945,13 @@
|
||||
return [...found.values()].sort((a, b) => a.distance - b.distance);
|
||||
}
|
||||
|
||||
function createTrafficAwarePositions(viewer, route, start, speed) {
|
||||
function createTrafficAwarePositions(viewer, route, start, speed, isMoving = () => true) {
|
||||
const state = { distance: 0, lastTime: start.clone() };
|
||||
viewer.clock.onTick.addEventListener((clock) => {
|
||||
const elapsed = Cesium.JulianDate.secondsDifference(clock.currentTime, state.lastTime);
|
||||
Cesium.JulianDate.clone(clock.currentTime, state.lastTime);
|
||||
if (elapsed <= 0) return;
|
||||
if (!isMoving()) return;
|
||||
const next = nextRouteStop(route, state.distance);
|
||||
const advance = elapsed * speed;
|
||||
if (next && signalPhase(next.signal.phaseGroup, clock.currentTime, start).active !== "green") {
|
||||
|
||||
Reference in New Issue
Block a user