feat: add native preview traffic simulation
This commit is contained in:
@@ -555,16 +555,27 @@
|
||||
.slice(0, 5);
|
||||
const speed = Number((routeData && routeData.speedMetersPerSecond) || 8);
|
||||
const start = trafficStart;
|
||||
const assignments = [];
|
||||
segments.forEach((segment, routeIndex) => {
|
||||
// Put two vehicles on the first route so the native preview visibly
|
||||
// exercises leader following and queue formation.
|
||||
const vehicleCount = routeIndex === 0 ? 2 : 1;
|
||||
for (let vehicleIndex = 0; vehicleIndex < vehicleCount; vehicleIndex += 1) {
|
||||
assignments.push({ segment, routeIndex, vehicleIndex });
|
||||
}
|
||||
});
|
||||
const simulation = createTrafficSimulation(viewer, assignments, start, speed, signalData, routeData?.settings);
|
||||
|
||||
viewer.clock.startTime = start.clone();
|
||||
viewer.clock.currentTime = start.clone();
|
||||
viewer.clock.clockRange = Cesium.ClockRange.UNBOUNDED;
|
||||
viewer.clock.multiplier = 1;
|
||||
viewer.clock.shouldAnimate = segments.length > 0;
|
||||
viewer.clock.shouldAnimate = assignments.length > 0;
|
||||
|
||||
const vehicles = segments.map((segment, index) => {
|
||||
const vehicles = assignments.map((assignment, index) => {
|
||||
const { segment } = assignment;
|
||||
const vehicle = addCruiseVehicle(viewer, segment, index, start, speed, signalData,
|
||||
selectedVehicleModelName(vehicleModelNames, fallbackVehicleModelName));
|
||||
selectedVehicleModelName(vehicleModelNames, fallbackVehicleModelName), simulation.motions[index]);
|
||||
const option = document.createElement("option");
|
||||
option.value = String(index);
|
||||
option.textContent = routeLabel(segment, index);
|
||||
@@ -574,7 +585,8 @@
|
||||
const cruise = {
|
||||
vehicles,
|
||||
baseSpeed: speed,
|
||||
state: { selectedIndex: 0 }
|
||||
state: { selectedIndex: 0 },
|
||||
simulation,
|
||||
};
|
||||
syncSelectedRouteVisibility(cruise);
|
||||
return cruise;
|
||||
@@ -732,10 +744,10 @@
|
||||
return usable[Math.floor(Math.random() * usable.length)] || "";
|
||||
}
|
||||
|
||||
function addCruiseVehicle(viewer, segment, index, start, speed, signalData, vehicleModelName) {
|
||||
const route = prepareRoute(segment, signalData);
|
||||
function addCruiseVehicle(viewer, segment, index, start, speed, signalData, vehicleModelName, trafficMotion) {
|
||||
const route = trafficMotion.route;
|
||||
let record = null;
|
||||
const trafficMotion = createTrafficAwarePositions(viewer, route, start, speed, () => record?.status === "normal");
|
||||
trafficMotion.setMoving(() => record?.status === "normal");
|
||||
const positions = trafficMotion.positions;
|
||||
const flat = [];
|
||||
for (const coord of segment.coordinates) {
|
||||
@@ -946,6 +958,106 @@
|
||||
return [...found.values()].sort((a, b) => a.distance - b.distance);
|
||||
}
|
||||
|
||||
function createTrafficSimulation(viewer, assignments, start, speed, signalData, settings = {}) {
|
||||
const config = {
|
||||
desiredSpeedMetersPerSecond: Number(settings.desiredSpeedMetersPerSecond || speed),
|
||||
accelerationMetersPerSecondSquared: Number(settings.accelerationMetersPerSecondSquared || 1.8),
|
||||
decelerationMetersPerSecondSquared: Number(settings.decelerationMetersPerSecondSquared || 3.5),
|
||||
reactionTimeSeconds: Number(settings.reactionTimeSeconds || 0.8),
|
||||
vehicleLengthMeters: Number(settings.vehicleLengthMeters || 4.6),
|
||||
minimumGapMeters: Number(settings.minimumGapMeters || 7),
|
||||
};
|
||||
const motions = assignments.map((assignment) => {
|
||||
const route = prepareRoute(assignment.segment, signalData);
|
||||
const initialGap = config.vehicleLengthMeters + config.minimumGapMeters;
|
||||
const state = {
|
||||
distance: assignment.vehicleIndex * -initialGap,
|
||||
speed: 0,
|
||||
targetSpeed: config.desiredSpeedMetersPerSecond,
|
||||
stopReason: null,
|
||||
queueAhead: 0,
|
||||
lastTime: start.clone(),
|
||||
};
|
||||
state.distance = (state.distance % route.length + route.length) % route.length;
|
||||
return {
|
||||
route,
|
||||
state,
|
||||
setMoving(callback) { state.isMoving = callback; },
|
||||
positions: new Cesium.CallbackProperty((time, result) => routePositionAtDistance(route, state.distance, result), false),
|
||||
};
|
||||
});
|
||||
const update = (clock) => {
|
||||
const elapsed = Cesium.JulianDate.secondsDifference(clock.currentTime, motions[0]?.state.lastTime || start);
|
||||
if (!(elapsed > 0) || elapsed > 2) {
|
||||
for (const motion of motions) Cesium.JulianDate.clone(clock.currentTime, motion.state.lastTime);
|
||||
return;
|
||||
}
|
||||
const grouped = new Map();
|
||||
assignments.forEach((assignment, index) => {
|
||||
const key = assignment.routeIndex;
|
||||
if (!grouped.has(key)) grouped.set(key, []);
|
||||
grouped.get(key).push(index);
|
||||
});
|
||||
for (const indexes of grouped.values()) {
|
||||
indexes.sort((a, b) => motions[a].state.distance - motions[b].state.distance);
|
||||
}
|
||||
motions.forEach((motion, index) => {
|
||||
const state = motion.state;
|
||||
Cesium.JulianDate.clone(clock.currentTime, state.lastTime);
|
||||
if (state.isMoving && !state.isMoving()) {
|
||||
state.speed = 0;
|
||||
state.stopReason = "incident";
|
||||
return;
|
||||
}
|
||||
const route = motion.route;
|
||||
let target = config.desiredSpeedMetersPerSecond;
|
||||
let stopReason = null;
|
||||
const nextStop = nextRouteStop(route, state.distance);
|
||||
if (nextStop) {
|
||||
const phase = signalPhase(nextStop.signal.phaseGroup, clock.currentTime, start).active;
|
||||
const untilStop = (nextStop.distance - state.distance + route.length) % route.length;
|
||||
if (phase !== "green" && untilStop < Math.max(30, state.speed * config.reactionTimeSeconds + 8)) {
|
||||
target = Math.min(target, Math.max(0, (untilStop - 1.7) / Math.max(config.reactionTimeSeconds, 0.1)));
|
||||
stopReason = "traffic-signal";
|
||||
}
|
||||
}
|
||||
const peers = grouped.get(assignments[index].routeIndex) || [];
|
||||
const peerPosition = peers.indexOf(index);
|
||||
if (peerPosition >= 0 && peers.length > 1) {
|
||||
const leaderIndex = peers[(peerPosition + 1) % peers.length];
|
||||
if (leaderIndex !== index) {
|
||||
const leader = motions[leaderIndex].state;
|
||||
const gap = (leader.distance - state.distance + route.length) % route.length - config.vehicleLengthMeters;
|
||||
const safeGap = config.minimumGapMeters + state.speed * config.reactionTimeSeconds;
|
||||
if (gap < safeGap + 12) {
|
||||
target = Math.min(target, Math.max(0, (gap - config.minimumGapMeters) / Math.max(config.reactionTimeSeconds, 0.1)));
|
||||
if (target < 0.2) stopReason = "leader-gap";
|
||||
state.queueAhead = leader.stopReason ? 1 : 0;
|
||||
} else {
|
||||
state.queueAhead = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
state.targetSpeed = target;
|
||||
const limit = (target >= state.speed ? config.accelerationMetersPerSecondSquared : config.decelerationMetersPerSecondSquared) * elapsed;
|
||||
state.speed += Math.sign(target - state.speed) * Math.min(Math.abs(target - state.speed), limit);
|
||||
state.distance = (state.distance + Math.max(0, state.speed) * elapsed) % route.length;
|
||||
state.stopReason = state.speed < 0.2 ? stopReason : null;
|
||||
});
|
||||
};
|
||||
viewer.clock.onTick.addEventListener(update);
|
||||
return {
|
||||
motions,
|
||||
settings: config,
|
||||
diagnostics() {
|
||||
return {
|
||||
stoppedVehicles: motions.filter((motion) => motion.state.stopReason).length,
|
||||
queueLength: motions.filter((motion) => motion.state.stopReason === "leader-gap").length,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTrafficAwarePositions(viewer, route, start, speed, isMoving = () => true) {
|
||||
const state = { distance: 0, lastTime: start.clone() };
|
||||
viewer.clock.onTick.addEventListener((clock) => {
|
||||
@@ -1230,6 +1342,12 @@
|
||||
"Trees: " + Number(stats.trees || 0),
|
||||
"Road layer source: " + (metadata.source_geojson ? "osm2streets" : "OSM fallback")
|
||||
];
|
||||
if (cruise.simulation) {
|
||||
const simulation = cruise.simulation.diagnostics();
|
||||
lines.push("Simulation: native-preview-traffic-simulation/v1");
|
||||
lines.push("Stopped: " + simulation.stoppedVehicles);
|
||||
lines.push("Queue: " + simulation.queueLength);
|
||||
}
|
||||
if (failed.length) {
|
||||
lines.push("Failed assets: " + failed.map((asset) => asset.url).join(", "));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user