Add multi-vehicle cruise selection preview
This commit is contained in:
@@ -661,6 +661,7 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #d9e0e2;
|
||||
}
|
||||
#status {
|
||||
position: absolute;
|
||||
@@ -708,6 +709,7 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
<div id="controls">
|
||||
<button id="toggleCruise">Pause</button>
|
||||
<button id="toggleFollow">Follow</button>
|
||||
<label>Vehicle <select id="vehicleSelect"></select></label>
|
||||
<label>Speed <input id="speedControl" type="range" min="2" max="22" step="1" value="8"></label>
|
||||
<span id="speedLabel">8 m/s</span>
|
||||
</div>
|
||||
@@ -716,6 +718,7 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
const statusEl = document.getElementById("status");
|
||||
const toggleCruise = document.getElementById("toggleCruise");
|
||||
const toggleFollow = document.getElementById("toggleFollow");
|
||||
const vehicleSelect = document.getElementById("vehicleSelect");
|
||||
const speedControl = document.getElementById("speedControl");
|
||||
const speedLabel = document.getElementById("speedLabel");
|
||||
Cesium.Ion.defaultAccessToken = "";
|
||||
@@ -743,6 +746,12 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
baseLayer: false
|
||||
});
|
||||
viewer.scene.globe.depthTestAgainstTerrain = true;
|
||||
viewer.scene.backgroundColor = Cesium.Color.fromCssColorString("#d9e0e2");
|
||||
viewer.scene.skyAtmosphere.show = false;
|
||||
viewer.scene.skyBox.show = false;
|
||||
viewer.scene.sun.show = false;
|
||||
viewer.scene.moon.show = false;
|
||||
viewer.scene.globe.baseColor = Cesium.Color.fromCssColorString("#d4dcde");
|
||||
|
||||
const p = Cesium.Cartesian3.fromDegrees(longitude, latitude, height);
|
||||
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(p);
|
||||
@@ -754,83 +763,42 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
scale: 1.0
|
||||
});
|
||||
viewer.scene.primitives.add(model);
|
||||
const cruise = addVehicleCruise(viewer, routeData);
|
||||
const cruise = addVehicleCruises(viewer, routeData);
|
||||
|
||||
const center = Cesium.Cartesian3.fromDegrees(longitude, latitude, height);
|
||||
viewer.camera.flyToBoundingSphere(
|
||||
new Cesium.BoundingSphere(center, 900.0),
|
||||
{ duration: 0.0 }
|
||||
);
|
||||
statusEl.textContent = "${escapeJs(glbName)} | route " + cruise.segment.id + " | " + Math.round(cruise.segment.lengthMeters) + "m";
|
||||
statusEl.textContent = "${escapeJs(glbName)} | vehicles " + cruise.vehicles.length;
|
||||
}
|
||||
|
||||
function addVehicleCruise(viewer, routeData) {
|
||||
const segment = (routeData.segments || [])[0];
|
||||
if (!segment || !segment.coordinates || segment.coordinates.length < 2) {
|
||||
function addVehicleCruises(viewer, routeData) {
|
||||
const segments = (routeData.segments || [])
|
||||
.filter((segment) => segment.coordinates && segment.coordinates.length >= 2)
|
||||
.slice(0, 5);
|
||||
if (!segments.length) {
|
||||
throw new Error("No drivable route found in ${escapeJs(routeName)}");
|
||||
}
|
||||
const speed = Number(routeData.speedMetersPerSecond || 8);
|
||||
const start = Cesium.JulianDate.now();
|
||||
const duration = Math.max(8, segment.lengthMeters / speed);
|
||||
const stop = Cesium.JulianDate.addSeconds(start, duration, new Cesium.JulianDate());
|
||||
const positions = new Cesium.SampledPositionProperty();
|
||||
let elapsed = 0;
|
||||
let previous = segment.coordinates[0];
|
||||
positions.addSample(start, Cesium.Cartesian3.fromDegrees(previous[0], previous[1], 1.15));
|
||||
for (let i = 1; i < segment.coordinates.length; i += 1) {
|
||||
const coord = segment.coordinates[i];
|
||||
elapsed += distanceMeters(previous, coord) / speed;
|
||||
const time = Cesium.JulianDate.addSeconds(start, elapsed, new Cesium.JulianDate());
|
||||
positions.addSample(time, Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 1.15));
|
||||
previous = coord;
|
||||
}
|
||||
positions.setInterpolationOptions({
|
||||
interpolationDegree: 1,
|
||||
interpolationAlgorithm: Cesium.LinearApproximation
|
||||
});
|
||||
|
||||
viewer.clock.startTime = start.clone();
|
||||
viewer.clock.stopTime = stop.clone();
|
||||
viewer.clock.currentTime = start.clone();
|
||||
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;
|
||||
viewer.clock.clockRange = Cesium.ClockRange.UNBOUNDED;
|
||||
viewer.clock.multiplier = 1;
|
||||
viewer.clock.shouldAnimate = true;
|
||||
|
||||
const flat = [];
|
||||
for (const coord of segment.coordinates) {
|
||||
flat.push(coord[0], coord[1], 1.05);
|
||||
}
|
||||
viewer.entities.add({
|
||||
name: "Cruise route",
|
||||
polyline: {
|
||||
positions: Cesium.Cartesian3.fromDegreesArrayHeights(flat),
|
||||
width: 4,
|
||||
material: Cesium.Color.CYAN.withAlpha(0.85),
|
||||
clampToGround: false
|
||||
}
|
||||
});
|
||||
const vehicleOrientation = correctedVehicleOrientation(positions, -90.0);
|
||||
const vehicle = viewer.entities.add({
|
||||
name: "Cruise vehicle",
|
||||
availability: new Cesium.TimeIntervalCollection([
|
||||
new Cesium.TimeInterval({ start, stop })
|
||||
]),
|
||||
position: positions,
|
||||
orientation: vehicleOrientation,
|
||||
model: {
|
||||
uri: "${escapeJs(vehicleModelName)}",
|
||||
scale: 1.0,
|
||||
minimumPixelSize: 24,
|
||||
maximumScale: 80
|
||||
},
|
||||
path: {
|
||||
resolution: 1,
|
||||
leadTime: 4,
|
||||
trailTime: 10,
|
||||
width: 2,
|
||||
material: Cesium.Color.ORANGE.withAlpha(0.9)
|
||||
}
|
||||
const vehicles = segments.map((segment, index) => {
|
||||
const vehicle = addCruiseVehicle(viewer, segment, index, start, speed);
|
||||
const option = document.createElement("option");
|
||||
option.value = String(index);
|
||||
option.textContent = "#" + (index + 1) + " " + segment.name + " " + Math.round(segment.lengthMeters) + "m";
|
||||
vehicleSelect.appendChild(option);
|
||||
return vehicle;
|
||||
});
|
||||
const state = { selectedIndex: 0 };
|
||||
|
||||
toggleCruise.addEventListener("click", () => {
|
||||
viewer.clock.shouldAnimate = !viewer.clock.shouldAnimate;
|
||||
toggleCruise.textContent = viewer.clock.shouldAnimate ? "Pause" : "Play";
|
||||
@@ -840,7 +808,14 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
viewer.clock.multiplier = value / speed;
|
||||
speedLabel.textContent = value + " m/s";
|
||||
});
|
||||
const follow = createChaseFollow(viewer, positions);
|
||||
vehicleSelect.addEventListener("change", () => {
|
||||
state.selectedIndex = Number(vehicleSelect.value || 0);
|
||||
statusEl.textContent = selectedVehicle().label;
|
||||
});
|
||||
function selectedVehicle() {
|
||||
return vehicles[state.selectedIndex] || vehicles[0];
|
||||
}
|
||||
const follow = createChaseFollow(viewer, () => selectedVehicle().positions);
|
||||
toggleFollow.addEventListener("click", () => {
|
||||
if (follow.enabled) {
|
||||
follow.stop();
|
||||
@@ -850,23 +825,118 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
toggleFollow.textContent = "Free";
|
||||
}
|
||||
});
|
||||
return { segment, vehicle };
|
||||
statusEl.textContent = selectedVehicle().label;
|
||||
return { vehicles };
|
||||
}
|
||||
|
||||
function correctedVehicleOrientation(positions, yawDegrees) {
|
||||
const velocityOrientation = new Cesium.VelocityOrientationProperty(positions);
|
||||
function addCruiseVehicle(viewer, segment, index, start, speed) {
|
||||
const route = prepareRoute(segment);
|
||||
const positions = new Cesium.CallbackProperty((time, result) => {
|
||||
return routePosition(route, start, time, speed, result);
|
||||
}, false);
|
||||
const flat = [];
|
||||
for (const coord of segment.coordinates) {
|
||||
flat.push(coord[0], coord[1], 1.05);
|
||||
}
|
||||
const routeColor = [Cesium.Color.CYAN, Cesium.Color.LIME, Cesium.Color.YELLOW, Cesium.Color.ORANGE, Cesium.Color.DEEPSKYBLUE][index % 5];
|
||||
viewer.entities.add({
|
||||
name: "Cruise route " + (index + 1),
|
||||
polyline: {
|
||||
positions: Cesium.Cartesian3.fromDegreesArrayHeights(flat),
|
||||
width: 2,
|
||||
material: routeColor.withAlpha(0.75),
|
||||
clampToGround: false
|
||||
}
|
||||
});
|
||||
const vehicle = viewer.entities.add({
|
||||
name: "Cruise vehicle " + (index + 1),
|
||||
position: positions,
|
||||
orientation: routeOrientation(route, start, speed, 0.0),
|
||||
model: {
|
||||
uri: "${escapeJs(vehicleModelName)}",
|
||||
scale: 1.0,
|
||||
minimumPixelSize: 24,
|
||||
maximumScale: 80
|
||||
}
|
||||
});
|
||||
return {
|
||||
entity: vehicle,
|
||||
positions,
|
||||
segment,
|
||||
label: "Vehicle #" + (index + 1) + " | route " + segment.id + " | " + Math.round(segment.lengthMeters) + "m"
|
||||
};
|
||||
}
|
||||
|
||||
function prepareRoute(segment) {
|
||||
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]));
|
||||
}
|
||||
return {
|
||||
coordinates: segment.coordinates,
|
||||
distances,
|
||||
length: Math.max(1.0, distances[distances.length - 1])
|
||||
};
|
||||
}
|
||||
|
||||
function routePosition(route, start, time, speed, result) {
|
||||
const seconds = Math.max(0, Cesium.JulianDate.secondsDifference(time, start));
|
||||
const distance = (seconds * speed) % route.length;
|
||||
let index = 1;
|
||||
while (index < route.distances.length - 1 && route.distances[index] < distance) {
|
||||
index += 1;
|
||||
}
|
||||
const prevDist = route.distances[index - 1];
|
||||
const nextDist = route.distances[index];
|
||||
const t = nextDist > prevDist ? (distance - prevDist) / (nextDist - prevDist) : 0;
|
||||
const a = route.coordinates[index - 1];
|
||||
const b = route.coordinates[index];
|
||||
const lon = a[0] + (b[0] - a[0]) * t;
|
||||
const lat = a[1] + (b[1] - a[1]) * t;
|
||||
return Cesium.Cartesian3.fromDegrees(lon, lat, 1.15, Cesium.Ellipsoid.WGS84, result);
|
||||
}
|
||||
|
||||
function routeOrientation(route, start, speed, yawDegrees) {
|
||||
const correction = Cesium.Quaternion.fromAxisAngle(
|
||||
Cesium.Cartesian3.UNIT_Z,
|
||||
Cesium.Math.toRadians(yawDegrees)
|
||||
);
|
||||
const current = new Cesium.Cartesian3();
|
||||
const ahead = new Cesium.Cartesian3();
|
||||
const direction = new Cesium.Cartesian3();
|
||||
const up = new Cesium.Cartesian3();
|
||||
const east = new Cesium.Cartesian3();
|
||||
const north = new Cesium.Cartesian3();
|
||||
return new Cesium.CallbackProperty((time, result) => {
|
||||
const base = velocityOrientation.getValue(time);
|
||||
if (!base) return result;
|
||||
routePosition(route, start, time, speed, current);
|
||||
const aheadTime = Cesium.JulianDate.addSeconds(time, 0.8, new Cesium.JulianDate());
|
||||
routePosition(route, start, aheadTime, speed, ahead);
|
||||
Cesium.Cartesian3.subtract(ahead, current, direction);
|
||||
if (Cesium.Cartesian3.magnitudeSquared(direction) < 0.0001) {
|
||||
return result;
|
||||
}
|
||||
Cesium.Cartesian3.normalize(direction, direction);
|
||||
Cesium.Cartesian3.normalize(current, up);
|
||||
Cesium.Cartesian3.cross(Cesium.Cartesian3.UNIT_Z, up, east);
|
||||
if (Cesium.Cartesian3.magnitudeSquared(east) < 0.0001) {
|
||||
Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_X, east);
|
||||
} else {
|
||||
Cesium.Cartesian3.normalize(east, east);
|
||||
}
|
||||
Cesium.Cartesian3.cross(up, east, north);
|
||||
Cesium.Cartesian3.normalize(north, north);
|
||||
const eastComponent = Cesium.Cartesian3.dot(direction, east);
|
||||
const northComponent = Cesium.Cartesian3.dot(direction, north);
|
||||
const heading = Math.atan2(eastComponent, northComponent);
|
||||
const base = Cesium.Transforms.headingPitchRollQuaternion(
|
||||
current,
|
||||
new Cesium.HeadingPitchRoll(heading, 0.0, 0.0)
|
||||
);
|
||||
return Cesium.Quaternion.multiply(base, correction, result || new Cesium.Quaternion());
|
||||
}, false);
|
||||
}
|
||||
|
||||
function createChaseFollow(viewer, positions) {
|
||||
function createChaseFollow(viewer, positionsProvider) {
|
||||
const scratchPosition = new Cesium.Cartesian3();
|
||||
const scratchPrevious = new Cesium.Cartesian3();
|
||||
const scratchDirection = new Cesium.Cartesian3();
|
||||
@@ -878,6 +948,7 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
|
||||
function update(clock) {
|
||||
const time = clock.currentTime;
|
||||
const positions = positionsProvider();
|
||||
const position = positions.getValue(time, scratchPosition);
|
||||
if (!position) return;
|
||||
const previousTime = Cesium.JulianDate.addSeconds(time, -0.8, new Cesium.JulianDate());
|
||||
|
||||
Reference in New Issue
Block a user