Extract Cesium preview assets
This commit is contained in:
@@ -316,6 +316,7 @@ function writeCesiumPreview(area) {
|
||||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||
writeVehicleRoute(area);
|
||||
writeVehicleModel(area);
|
||||
writeCesiumPreviewSupportFiles(path.dirname(htmlPath));
|
||||
const glbName = path.basename(area.outputs.glb);
|
||||
const metadataName = path.basename(area.outputs.metadata);
|
||||
const routeName = path.basename(area.outputs.vehicleRoute);
|
||||
@@ -324,6 +325,14 @@ function writeCesiumPreview(area) {
|
||||
console.log(`Cesium preview: ${htmlPath}`);
|
||||
}
|
||||
|
||||
function writeCesiumPreviewSupportFiles(outDir) {
|
||||
for (const file of ["cesium-preview.css", "cesium-preview.js"]) {
|
||||
const source = path.join(repoRoot, "scripts", "lib", file);
|
||||
ensureFile(source, `Cesium preview support file '${file}'`);
|
||||
fs.copyFileSync(source, path.join(outDir, file));
|
||||
}
|
||||
}
|
||||
|
||||
function writeVehicleRoute(area) {
|
||||
const route = buildVehicleRoute(area.input);
|
||||
fs.mkdirSync(path.dirname(area.outputs.vehicleRoute), { recursive: true });
|
||||
@@ -686,6 +695,13 @@ function cylinderY(cx, lateral, up, radius, width, segments) {
|
||||
}
|
||||
|
||||
function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, areaId) {
|
||||
const previewConfig = {
|
||||
areaId,
|
||||
glbName,
|
||||
metadataName,
|
||||
routeName,
|
||||
vehicleModelName,
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@@ -694,383 +710,37 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
<title>${escapeHtml(areaId)} Cesium Preview</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/cesium@1.121.1/Build/Cesium/Cesium.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/cesium@1.121.1/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
|
||||
<style>
|
||||
html, body, #cesiumContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #d9e0e2;
|
||||
}
|
||||
#status {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
bottom: 12px;
|
||||
z-index: 1;
|
||||
max-width: 520px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
background: rgba(20, 24, 28, 0.78);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
#controls {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(20, 24, 28, 0.78);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
#controls button {
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
background: #f2f5f7;
|
||||
color: #111;
|
||||
cursor: pointer;
|
||||
}
|
||||
#controls input {
|
||||
width: 120px;
|
||||
}
|
||||
</style>
|
||||
<link href="cesium-preview.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="cesiumContainer"></div>
|
||||
<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 class="control-group">
|
||||
<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>
|
||||
<div class="control-group">
|
||||
<label><input id="toggleScene" type="checkbox" checked> Scene</label>
|
||||
<span id="assetToggles" class="control-subgroup"></span>
|
||||
<label><input id="toggleRoutes" type="checkbox" checked> Routes</label>
|
||||
<label><input id="toggleVehicles" type="checkbox" checked> Vehicles</label>
|
||||
<label><input id="toggleFps" type="checkbox"> FPS</label>
|
||||
<label><input id="toggleDiagnostics" type="checkbox" checked> Info</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button data-camera="overview">Overview</button>
|
||||
<button data-camera="oblique">Oblique</button>
|
||||
<button data-camera="detail">Detail</button>
|
||||
<button data-camera="route">Route</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="diagnostics">Loading diagnostics...</div>
|
||||
<div id="status">Loading ${escapeHtml(glbName)}...</div>
|
||||
<script>
|
||||
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 = "";
|
||||
|
||||
async function main() {
|
||||
const metadata = await fetch("${escapeJs(metadataName)}").then((r) => r.json());
|
||||
const routeData = await fetch("${escapeJs(routeName)}").then((r) => r.json());
|
||||
const anchor = metadata.anchor || {};
|
||||
const longitude = Number(anchor.longitude || 0);
|
||||
const latitude = Number(anchor.latitude || 0);
|
||||
const height = Number(anchor.height || 0);
|
||||
const heading = Number(metadata.heading_correction_degrees || 0);
|
||||
|
||||
const viewer = new Cesium.Viewer("cesiumContainer", {
|
||||
animation: false,
|
||||
timeline: false,
|
||||
baseLayerPicker: false,
|
||||
geocoder: false,
|
||||
navigationHelpButton: false,
|
||||
sceneModePicker: false,
|
||||
homeButton: true,
|
||||
fullscreenButton: true,
|
||||
infoBox: false,
|
||||
selectionIndicator: false,
|
||||
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);
|
||||
const correction = Cesium.Matrix3.fromRotationZ(Cesium.Math.toRadians(heading));
|
||||
const modelMatrix = Cesium.Matrix4.multiplyByMatrix3(enu, correction, new Cesium.Matrix4());
|
||||
const model = await Cesium.Model.fromGltfAsync({
|
||||
url: "${escapeJs(glbName)}",
|
||||
modelMatrix,
|
||||
scale: 1.0
|
||||
});
|
||||
viewer.scene.primitives.add(model);
|
||||
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)} | vehicles " + cruise.vehicles.length;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
viewer.clock.startTime = start.clone();
|
||||
viewer.clock.currentTime = start.clone();
|
||||
viewer.clock.clockRange = Cesium.ClockRange.UNBOUNDED;
|
||||
viewer.clock.multiplier = 1;
|
||||
viewer.clock.shouldAnimate = true;
|
||||
|
||||
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";
|
||||
});
|
||||
speedControl.addEventListener("input", () => {
|
||||
const value = Number(speedControl.value);
|
||||
viewer.clock.multiplier = value / speed;
|
||||
speedLabel.textContent = value + " m/s";
|
||||
});
|
||||
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();
|
||||
toggleFollow.textContent = "Follow";
|
||||
} else {
|
||||
follow.start();
|
||||
toggleFollow.textContent = "Free";
|
||||
}
|
||||
});
|
||||
statusEl.textContent = selectedVehicle().label;
|
||||
return { vehicles };
|
||||
}
|
||||
|
||||
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) => {
|
||||
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, positionsProvider) {
|
||||
const scratchPosition = new Cesium.Cartesian3();
|
||||
const scratchPrevious = new Cesium.Cartesian3();
|
||||
const scratchDirection = new Cesium.Cartesian3();
|
||||
const scratchEast = new Cesium.Cartesian3();
|
||||
const scratchNorth = new Cesium.Cartesian3();
|
||||
const scratchUp = new Cesium.Cartesian3();
|
||||
const offset = new Cesium.Cartesian3();
|
||||
const state = { enabled: false, distance: 36.0, height: 18.0 };
|
||||
|
||||
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());
|
||||
const previous = positions.getValue(previousTime, scratchPrevious);
|
||||
if (previous) {
|
||||
Cesium.Cartesian3.subtract(position, previous, scratchDirection);
|
||||
} else {
|
||||
Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_X, scratchDirection);
|
||||
}
|
||||
if (Cesium.Cartesian3.magnitudeSquared(scratchDirection) < 0.0001) {
|
||||
Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_X, scratchDirection);
|
||||
}
|
||||
Cesium.Cartesian3.normalize(scratchDirection, scratchDirection);
|
||||
Cesium.Cartesian3.normalize(position, scratchUp);
|
||||
Cesium.Cartesian3.cross(Cesium.Cartesian3.UNIT_Z, scratchUp, scratchEast);
|
||||
if (Cesium.Cartesian3.magnitudeSquared(scratchEast) < 0.0001) {
|
||||
Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_X, scratchEast);
|
||||
} else {
|
||||
Cesium.Cartesian3.normalize(scratchEast, scratchEast);
|
||||
}
|
||||
Cesium.Cartesian3.cross(scratchUp, scratchEast, scratchNorth);
|
||||
Cesium.Cartesian3.normalize(scratchNorth, scratchNorth);
|
||||
|
||||
const eastComponent = Cesium.Cartesian3.dot(scratchDirection, scratchEast);
|
||||
const northComponent = Cesium.Cartesian3.dot(scratchDirection, scratchNorth);
|
||||
const heading = Math.atan2(eastComponent, northComponent);
|
||||
Cesium.Cartesian3.fromElements(0.0, -state.distance, state.height, offset);
|
||||
const transform = Cesium.Transforms.headingPitchRollToFixedFrame(
|
||||
position,
|
||||
new Cesium.HeadingPitchRoll(heading, 0.0, 0.0)
|
||||
);
|
||||
viewer.camera.lookAtTransform(transform, offset);
|
||||
}
|
||||
|
||||
function onWheel(event) {
|
||||
if (!state.enabled) return;
|
||||
event.preventDefault();
|
||||
const zoom = event.deltaY > 0 ? 1.12 : 0.88;
|
||||
state.distance = Cesium.Math.clamp(state.distance * zoom, 12.0, 160.0);
|
||||
state.height = Cesium.Math.clamp(state.height * zoom, 6.0, 90.0);
|
||||
update(viewer.clock);
|
||||
}
|
||||
|
||||
return {
|
||||
get enabled() {
|
||||
return state.enabled;
|
||||
},
|
||||
start() {
|
||||
if (state.enabled) return;
|
||||
state.enabled = true;
|
||||
viewer.trackedEntity = undefined;
|
||||
viewer.canvas.addEventListener("wheel", onWheel, { passive: false });
|
||||
viewer.clock.onTick.addEventListener(update);
|
||||
update(viewer.clock);
|
||||
},
|
||||
stop() {
|
||||
if (!state.enabled) return;
|
||||
state.enabled = false;
|
||||
viewer.clock.onTick.removeEventListener(update);
|
||||
viewer.canvas.removeEventListener("wheel", onWheel);
|
||||
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function distanceMeters(a, b) {
|
||||
const radius = 6371008.8;
|
||||
const lat1 = Cesium.Math.toRadians(a[1]);
|
||||
const lat2 = Cesium.Math.toRadians(b[1]);
|
||||
const dLat = Cesium.Math.toRadians(b[1] - a[1]);
|
||||
const dLon = Cesium.Math.toRadians(b[0] - a[0]);
|
||||
const sinLat = Math.sin(dLat / 2);
|
||||
const sinLon = Math.sin(dLon / 2);
|
||||
const h = sinLat * sinLat + Math.cos(lat1) * Math.cos(lat2) * sinLon * sinLon;
|
||||
return 2 * radius * Math.asin(Math.min(1, Math.sqrt(h)));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
statusEl.textContent = "Failed to load Cesium preview: " + error.message;
|
||||
});
|
||||
</script>
|
||||
<script>window.OSM_ASSET_PREVIEW_CONFIG = ${escapeScriptJson(JSON.stringify(previewConfig))};</script>
|
||||
<script src="cesium-preview.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
@@ -1084,6 +754,11 @@ function escapeHtml(value) {
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
function escapeJs(value) {
|
||||
return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
function escapeScriptJson(value) {
|
||||
return String(value)
|
||||
.replaceAll("<", "\\u003c")
|
||||
.replaceAll(">", "\\u003e")
|
||||
.replaceAll("&", "\\u0026")
|
||||
.replaceAll("\u2028", "\\u2028")
|
||||
.replaceAll("\u2029", "\\u2029");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user