mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Main's camera, voice, Cockpit and layer work now runs on the globe contract where this branch draws through it: - The voice tilt to an angle, its pivot about the place under the crosshair and the frame limit from orbit run through the camera rig; src/globeInView.js measures a camera pose with src/geo instead of a Cesium camera. The free-look height limit and the globe-in-view recovery stay Cesium-only for now. - Cockpit takes main's own-layer entry rule. Double-clicking the followed track goes through cockpitHost.routeDoubleClick, so it works on both engines. - Space Missions' failed-entry rollback and GhostMaps' age filter run in the engine-neutral layer code. On Cesium an expired report is dropped in place; on Navara its product is redrawn. - Wildlife Sightings joins the catalog Cesium-only: Navara leaves it out, and the shell builds its card only for the real layer, so a Navara page no longer fails to start on its stand-in. - One instruction sentence is shorter, which keeps the Realtime prompt within its 55,500-character budget with the Sentinel-2 basemap.
158 lines
5.5 KiB
JavaScript
158 lines
5.5 KiB
JavaScript
import * as Cesium from 'cesium';
|
|
import { createHorizonCull } from '../../streetLevel/view.js';
|
|
|
|
const cesiumColor = (rgba, fallback, alpha) =>
|
|
rgba
|
|
? new Cesium.Color(rgba[0], rgba[1], rgba[2], alpha ?? rgba[3])
|
|
: Cesium.Color.fromCssColorString(fallback).withAlpha(alpha ?? 1);
|
|
|
|
export function createGhostMapsCesiumRenderer(
|
|
viewer,
|
|
createHandler,
|
|
markerImage,
|
|
) {
|
|
const allPins = new Set();
|
|
const horizon = createHorizonCull({
|
|
getViewer: () => viewer,
|
|
items: () => allPins,
|
|
onChange: () => viewer.scene.requestRender?.(),
|
|
});
|
|
return {
|
|
requestRender: () => viewer.scene.requestRender?.(),
|
|
onClick(run) {
|
|
const handler = createHandler(viewer);
|
|
handler.setInputAction(
|
|
(click) => run(click, () => viewer.scene.pick(click.position)),
|
|
Cesium.ScreenSpaceEventType.LEFT_CLICK,
|
|
);
|
|
return () => handler.destroy();
|
|
},
|
|
draw(productId, features, color) {
|
|
const dataSource = new Cesium.CustomDataSource(
|
|
`s2-ghostmaps:${productId}`,
|
|
);
|
|
const pins = [];
|
|
dataSource.entities.suspendEvents();
|
|
features.forEach((feature, index) => {
|
|
const key = `${productId}:${index}`;
|
|
const { style } = feature;
|
|
feature.geometries.forEach((geometry, part) => {
|
|
const id = `ghostmaps:${key}:${part}`;
|
|
if (geometry.type === 'point') {
|
|
const position = Cesium.Cartesian3.fromDegrees(
|
|
...geometry.position,
|
|
);
|
|
const entity = dataSource.entities.add({
|
|
id,
|
|
position,
|
|
billboard: {
|
|
image: style.icon || markerImage(color),
|
|
scale: Math.min(
|
|
1.5,
|
|
Math.max(0.35, (style.iconScale ?? 1) * 0.7),
|
|
),
|
|
color:
|
|
style.icon && style.iconColor
|
|
? cesiumColor(style.iconColor)
|
|
: undefined,
|
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
// Ground-clamped icons would sink into terrain and 3D tiles; the
|
|
// horizon cull hides the far side instead (see `_horizon`).
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
},
|
|
});
|
|
pins.push({
|
|
index,
|
|
position,
|
|
get show() {
|
|
return entity.show;
|
|
},
|
|
set show(value) {
|
|
entity.show = value;
|
|
},
|
|
});
|
|
} else if (geometry.type === 'line') {
|
|
dataSource.entities.add({
|
|
id,
|
|
polyline: {
|
|
positions: Cesium.Cartesian3.fromDegreesArray(
|
|
geometry.positions.flat(),
|
|
),
|
|
width: Math.min(8, Math.max(1.5, style.lineWidth ?? 2)),
|
|
material: cesiumColor(style.lineColor, color),
|
|
clampToGround: true,
|
|
},
|
|
});
|
|
} else {
|
|
const ring = (points) =>
|
|
Cesium.Cartesian3.fromDegreesArray(points.flat());
|
|
dataSource.entities.add({
|
|
id,
|
|
polygon: {
|
|
hierarchy: new Cesium.PolygonHierarchy(
|
|
ring(geometry.outer),
|
|
geometry.holes.map(
|
|
(hole) => new Cesium.PolygonHierarchy(ring(hole)),
|
|
),
|
|
),
|
|
material:
|
|
style.fill === false
|
|
? Cesium.Color.TRANSPARENT
|
|
: cesiumColor(
|
|
style.polyColor,
|
|
color,
|
|
style.polyColor ? undefined : 0.25,
|
|
),
|
|
classificationType: Cesium.ClassificationType.BOTH,
|
|
},
|
|
});
|
|
if (style.outline !== false)
|
|
dataSource.entities.add({
|
|
id: `${id}:outline`,
|
|
polyline: {
|
|
positions: ring([...geometry.outer, geometry.outer[0]]),
|
|
width: Math.min(6, Math.max(1.5, style.lineWidth ?? 2)),
|
|
material: cesiumColor(style.lineColor, color),
|
|
clampToGround: true,
|
|
},
|
|
});
|
|
}
|
|
});
|
|
});
|
|
dataSource.entities.resumeEvents();
|
|
for (const pin of pins) allPins.add(pin);
|
|
horizon.update(pins);
|
|
let disposed = false;
|
|
const ready = Promise.resolve(viewer.dataSources.add(dataSource)).finally(
|
|
() => {
|
|
if (disposed) viewer.dataSources.remove(dataSource, true);
|
|
},
|
|
);
|
|
return {
|
|
ready,
|
|
// Take one report off in place: an expired event leaves its edition's
|
|
// data source, which stays mounted for the reports that remain.
|
|
drop(index) {
|
|
const prefix = `ghostmaps:${productId}:${index}:`;
|
|
for (const entity of [...dataSource.entities.values])
|
|
if (entity.id.startsWith(prefix))
|
|
dataSource.entities.remove(entity);
|
|
for (let i = pins.length - 1; i >= 0; i -= 1)
|
|
if (pins[i].index === index) {
|
|
allPins.delete(pins[i]);
|
|
pins.splice(i, 1);
|
|
}
|
|
return true;
|
|
},
|
|
destroy() {
|
|
disposed = true;
|
|
for (const pin of pins) allPins.delete(pin);
|
|
viewer.dataSources.remove(dataSource, true);
|
|
},
|
|
};
|
|
},
|
|
destroy: horizon.stop,
|
|
};
|
|
}
|