|
|
|
|
@@ -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();
|
|
|
|
|
|