feat(preview): pause vehicles for traffic signals

This commit is contained in:
2026-08-06 08:39:31 +08:00
parent 8b3a1d79d9
commit 9fbc218e10
5 changed files with 85 additions and 24 deletions

View File

@@ -40,8 +40,9 @@
const viewer = createViewer();
setLoadingMessage("Loading model", config.glbName || "");
const assets = await loadSceneAssets(viewer, metadata, placement);
const cruise = addVehicleCruises(viewer, routeData, config.vehicleModelNames, config.vehicleModelName);
const trafficSignals = addTrafficSignals(viewer, signalData);
const trafficStart = Cesium.JulianDate.now();
const trafficSignals = addTrafficSignals(viewer, signalData, trafficStart);
const cruise = addVehicleCruises(viewer, routeData, signalData, trafficStart, config.vehicleModelNames, config.vehicleModelName);
const cameras = createCameraPresets(viewer, metadata, placement, cruise);
buildAssetToggles(assets);
@@ -434,12 +435,12 @@
return stopFollow;
}
function addVehicleCruises(viewer, routeData, vehicleModelNames, fallbackVehicleModelName) {
function addVehicleCruises(viewer, routeData, signalData, trafficStart, vehicleModelNames, fallbackVehicleModelName) {
const segments = ((routeData && (routeData.routes || routeData.segments)) || [])
.filter((segment) => segment.coordinates && segment.coordinates.length >= 2)
.slice(0, 5);
const speed = Number((routeData && routeData.speedMetersPerSecond) || 8);
const start = Cesium.JulianDate.now();
const start = trafficStart;
viewer.clock.startTime = start.clone();
viewer.clock.currentTime = start.clone();
@@ -448,7 +449,7 @@
viewer.clock.shouldAnimate = segments.length > 0;
const vehicles = segments.map((segment, index) => {
const vehicle = addCruiseVehicle(viewer, segment, index, start, speed,
const vehicle = addCruiseVehicle(viewer, segment, index, start, speed, signalData,
selectedVehicleModelName(vehicleModelNames, fallbackVehicleModelName));
const option = document.createElement("option");
option.value = String(index);
@@ -463,10 +464,9 @@
};
}
function addTrafficSignals(viewer, signalData) {
function addTrafficSignals(viewer, signalData, start) {
const anchors = (signalData?.signals || []).filter((signal) => Number.isFinite(signal.longitude) && Number.isFinite(signal.latitude));
const entities = [];
const start = Cesium.JulianDate.now();
for (const signal of anchors) {
const mastReach = Number(signal.mastReachMeters) || 4.5;
const countdownOffset = -1.15;
@@ -639,11 +639,9 @@
return usable[Math.floor(Math.random() * usable.length)] || "";
}
function addCruiseVehicle(viewer, segment, index, start, speed, vehicleModelName) {
const route = prepareRoute(segment);
const positions = new Cesium.CallbackProperty((time, result) => {
return routePosition(route, start, time, speed, result);
}, false);
function addCruiseVehicle(viewer, segment, index, start, speed, signalData, vehicleModelName) {
const route = prepareRoute(segment, signalData);
const positions = createTrafficAwarePositions(viewer, route, start, speed);
const flat = [];
for (const coord of segment.coordinates) {
flat.push(coord[0], coord[1], 1.05);
@@ -668,7 +666,7 @@
const vehicle = viewer.entities.add({
name: "Cruise vehicle " + (index + 1),
position: positions,
orientation: routeOrientation(route, start, speed, 0.0),
orientation: routeOrientationFromPositions(positions),
model: {
uri: vehicleModelName,
// Keep the library's world scale visible. A minimum screen size made
@@ -705,7 +703,7 @@
return cruise.vehicles[cruise.state.selectedIndex] || cruise.vehicles[0];
}
function prepareRoute(segment) {
function prepareRoute(segment, signalData) {
const distances = [0.0];
for (let i = 1; i < segment.coordinates.length; i += 1) {
distances.push(distances[i - 1] + distanceMeters(segment.coordinates[i - 1], segment.coordinates[i]));
@@ -713,13 +711,61 @@
return {
coordinates: segment.coordinates,
distances,
length: Math.max(1.0, distances[distances.length - 1])
length: Math.max(1.0, distances[distances.length - 1]),
stops: routeStops(segment.coordinates, distances, signalData),
};
}
function routeStops(coordinates, distances, signalData) {
const found = new Map();
for (const signal of signalData?.signals || []) {
if (!Number.isFinite(signal.stopLongitude) || !Number.isFinite(signal.stopLatitude)) continue;
let best = { index: -1, distance: Infinity };
for (let index = 0; index < coordinates.length; index += 1) {
const distance = distanceMeters(coordinates[index], [signal.stopLongitude, signal.stopLatitude]);
if (distance < best.distance) best = { index, distance };
}
if (best.index >= 0 && best.distance <= 7) {
const routeDistance = distances[best.index];
const existing = found.get(Math.round(routeDistance));
if (!existing || best.distance < existing.matchDistance) found.set(Math.round(routeDistance), { distance: routeDistance, matchDistance: best.distance, signal });
}
}
return [...found.values()].sort((a, b) => a.distance - b.distance);
}
function createTrafficAwarePositions(viewer, route, start, speed) {
const state = { distance: 0, lastTime: start.clone() };
const scratchTime = new Cesium.JulianDate();
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;
const next = nextRouteStop(route, state.distance);
const advance = elapsed * speed;
if (next && signalPhase(next.signal.phaseGroup, clock.currentTime, start).active !== "green") {
const untilStop = (next.distance - state.distance + route.length) % route.length;
if (untilStop <= advance + 1.7) {
state.distance = (next.distance - 1.7 + route.length) % route.length;
return;
}
}
state.distance = (state.distance + advance) % route.length;
});
return new Cesium.CallbackProperty((time, result) => routePositionAtDistance(route, state.distance, result), false);
}
function nextRouteStop(route, distance) {
return route.stops.find((stop) => stop.distance > distance + 0.05) || route.stops[0] || null;
}
function routePosition(route, start, time, speed, result) {
const seconds = Math.max(0, Cesium.JulianDate.secondsDifference(time, start));
const distance = (seconds * speed) % route.length;
return routePositionAtDistance(route, distance, result);
}
function routePositionAtDistance(route, distance, result) {
let index = 1;
while (index < route.distances.length - 1 && route.distances[index] < distance) {
index += 1;
@@ -780,6 +826,10 @@
}, false);
}
function routeOrientationFromPositions(positions) {
return new Cesium.VelocityOrientationProperty(positions);
}
function createChaseFollow(viewer, positionsProvider) {
const scratchPosition = new Cesium.Cartesian3();
const scratchPrevious = new Cesium.Cartesian3();

View File

@@ -33,6 +33,8 @@ function buildTrafficSignals(stopLines, intersections) {
phaseGroup: signals.length % 2,
longitude: point[0],
latitude: point[1],
stopLongitude: center[0],
stopLatitude: center[1],
headingDegrees: Math.atan2(axis[0], axis[1]) * 180 / Math.PI,
mastReachMeters: MAST_REACH_METERS,
});