mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Knip, configured with the application, package exports, scripts, QA harnesses, workers, tools and tests as entry points, reports exports that no module imports. Each was checked by hand against string-based and namespace imports. - Symbols that are still used inside their own module lose only the `export` keyword (176 constants and helpers). - Unused re-exports are removed from barrel modules (flight, military, vessel, CCTV, directions, transit history, local ADS-B and street-level policy modules). - Declarations that nothing uses at all are deleted, among them AIS_WATCHDOG_STATUSES, applyFilter, isRadioCountryCode, compactText, DENSITY_STOPS, the unused CCTV endpoint constants, and the SDR band constants. - Default exports that nothing imports are removed. The ones in src/data/earthquakes.js, src/data/telegeographySubmarineCables.js, src/data/wind.js and src/data/bhoteKoshiLocator.js each constructed a layer instance at import time that nothing used; those instances are no longer created. Every export of a package.json entry stays, as does TOMTOM_CREDIT, which the traffic layer reads from the credits namespace it is given.
199 lines
7.0 KiB
JavaScript
199 lines
7.0 KiB
JavaScript
/**
|
|
* Camera ground guard: never leave an arrival buried in, or pressed against,
|
|
* the rendered surface.
|
|
*
|
|
* Arrival framing is computed before the destination's tiles exist, so it has
|
|
* to predict the ground height. `globe.getHeight()` answers nothing while the
|
|
* globe is hidden under photoreal tiles, curated `groundElevation` presets
|
|
* cover only a few cities, and the elevation service resolves asynchronously.
|
|
* A missing prediction used to fall back to sea level, which buried the eye by
|
|
* roughly the local elevation (Denver ~1,600 m; Austin ~170 m).
|
|
*
|
|
* After the flight lands and tiles have streamed in, this module measures the
|
|
* rendered surface under the framed target and under the camera itself, and
|
|
* lifts the eye if it sits below a usable clearance. Heading and pitch are
|
|
* held, so the correction reads as the shot settling.
|
|
*/
|
|
import * as Cesium from 'cesium';
|
|
import { ownCameraArrival } from './data/cameraArrival.js';
|
|
|
|
/** Minimum eye height above the rendered surface for a usable view. */
|
|
export const MIN_EYE_CLEARANCE_M = 120;
|
|
/** Retries while tiles stream in; sampling fails until the mesh exists. */
|
|
const GUARD_ATTEMPTS = 8;
|
|
const GUARD_INTERVAL_MS = 600;
|
|
/** Ignore sub-metre noise rather than nudging the camera forever. */
|
|
const GUARD_EPSILON_M = 2;
|
|
|
|
const probeScratch = new Cesium.Cartographic();
|
|
|
|
/**
|
|
* Highest rendered surface across a few cells, with how many cells answered.
|
|
* A cell whose tiles are not streamed yet contributes nothing.
|
|
* @param {Cesium.Scene} scene Live scene.
|
|
* @param {Array<{lat: number, lon: number}>} cells Cells to probe.
|
|
* @returns {{heightM: number, sampled: number}} Reading.
|
|
*/
|
|
function sampleSurfaceM(scene, cells) {
|
|
let heightM = Number.NaN;
|
|
let sampled = 0;
|
|
if (typeof scene?.sampleHeight !== 'function') return { heightM, sampled };
|
|
for (const cell of cells) {
|
|
try {
|
|
const height = scene.sampleHeight(
|
|
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch),
|
|
);
|
|
if (!Number.isFinite(height)) continue;
|
|
sampled += 1;
|
|
heightM = Number.isFinite(heightM) ? Math.max(heightM, height) : height;
|
|
} catch {
|
|
/* tiles not ready for this cell */
|
|
}
|
|
}
|
|
return { heightM, sampled };
|
|
}
|
|
|
|
/**
|
|
* How far the camera must rise to clear the ground, given a measured surface.
|
|
* Pure so the policy is testable without a scene.
|
|
* @param {number} cameraHeightM Eye height above the ellipsoid.
|
|
* @param {number} groundHeightM Measured surface height above the ellipsoid.
|
|
* @param {number} [clearanceM] Desired eye height above that surface.
|
|
* @returns {number} Metres to lift; 0 when the view is already clear.
|
|
*/
|
|
export function groundClearanceDeficitM(
|
|
cameraHeightM,
|
|
groundHeightM,
|
|
clearanceM = MIN_EYE_CLEARANCE_M,
|
|
) {
|
|
if (!Number.isFinite(cameraHeightM) || !Number.isFinite(groundHeightM))
|
|
return 0;
|
|
const deficit = groundHeightM + clearanceM - cameraHeightM;
|
|
return deficit > GUARD_EPSILON_M ? deficit : 0;
|
|
}
|
|
|
|
/**
|
|
* Watch an arrival and lift the camera once the real surface can be measured.
|
|
*
|
|
* Stops quietly on a clear view, exhausted attempts, the user taking the
|
|
* controls (pointer or wheel on the canvas), or `isStale()` reporting that a
|
|
* newer arrival owns the camera.
|
|
* @param {Cesium.Viewer} viewer Live viewer.
|
|
* @param {{lat: number, lon: number}} at Destination being framed.
|
|
* @param {{clearanceM?: number, attempts?: number, intervalMs?: number,
|
|
* isStale?: () => boolean, onLift?: (liftM: number) => void}} [options]
|
|
* @returns {() => void} Cancel function.
|
|
*/
|
|
export function guardCameraAboveGround(viewer, at, options = {}) {
|
|
const {
|
|
clearanceM = MIN_EYE_CLEARANCE_M,
|
|
attempts = GUARD_ATTEMPTS,
|
|
intervalMs = GUARD_INTERVAL_MS,
|
|
isStale = () => false,
|
|
onLift = null,
|
|
} = options;
|
|
if (!viewer?.scene || !Number.isFinite(at?.lat) || !Number.isFinite(at?.lon))
|
|
return () => {};
|
|
|
|
let timer = null;
|
|
let done = false;
|
|
const canvas = viewer.scene.canvas;
|
|
const documentRef = canvas?.ownerDocument;
|
|
const removers = [];
|
|
let release = () => {};
|
|
const stop = () => {
|
|
if (done) return;
|
|
done = true;
|
|
clearTimeout(timer);
|
|
for (const type of ['pointerdown', 'wheel'])
|
|
canvas?.removeEventListener?.(type, stop);
|
|
documentRef?.removeEventListener?.('keydown', stop, true);
|
|
for (const remove of removers) remove();
|
|
release();
|
|
};
|
|
release = ownCameraArrival(viewer, stop);
|
|
// Direct camera API callers (including duration-zero flights) must yield
|
|
// before moving the eye. Restore the exact method ownership on completion.
|
|
for (const name of [
|
|
'flyTo',
|
|
'flyToBoundingSphere',
|
|
'setView',
|
|
'lookAt',
|
|
'cancelFlight',
|
|
]) {
|
|
const camera = viewer.camera;
|
|
const original = camera[name];
|
|
if (typeof original !== 'function') continue;
|
|
const descriptor = Object.getOwnPropertyDescriptor(camera, name);
|
|
const wrapped = function (...args) {
|
|
stop();
|
|
return original.apply(this, args);
|
|
};
|
|
camera[name] = wrapped;
|
|
removers.push(() => {
|
|
if (camera[name] !== wrapped) return;
|
|
if (descriptor) Object.defineProperty(camera, name, descriptor);
|
|
else delete camera[name];
|
|
});
|
|
}
|
|
const removeTracking = viewer.trackedEntityChanged?.addEventListener(stop);
|
|
if (removeTracking) removers.push(removeTracking);
|
|
documentRef?.addEventListener?.('keydown', stop, true);
|
|
// A manual gesture ends the guard, as it interrupts camera motion elsewhere.
|
|
for (const type of ['pointerdown', 'wheel'])
|
|
canvas?.addEventListener?.(type, stop, { once: true, passive: true });
|
|
|
|
let remaining = attempts;
|
|
const attempt = () => {
|
|
if (done) return;
|
|
if (isStale() || viewer.isDestroyed?.()) return stop();
|
|
remaining -= 1;
|
|
const camera = viewer.camera;
|
|
const carto = camera.positionCartographic;
|
|
// The eye must clear both the framed target and the ground beneath it.
|
|
const below = {
|
|
lat: Cesium.Math.toDegrees(carto.latitude),
|
|
lon: Cesium.Math.toDegrees(carto.longitude),
|
|
};
|
|
const { heightM, sampled } = sampleSurfaceM(viewer.scene, [
|
|
{ lat: at.lat, lon: at.lon },
|
|
below,
|
|
]);
|
|
if (sampled > 0 && Number.isFinite(heightM)) {
|
|
const deficit = groundClearanceDeficitM(
|
|
carto.height,
|
|
heightM,
|
|
clearanceM,
|
|
);
|
|
if (deficit > 0) {
|
|
stop();
|
|
camera.flyTo({
|
|
destination: Cesium.Cartesian3.fromRadians(
|
|
carto.longitude,
|
|
carto.latitude,
|
|
carto.height + deficit,
|
|
),
|
|
orientation: {
|
|
heading: camera.heading,
|
|
pitch: camera.pitch,
|
|
roll: camera.roll,
|
|
},
|
|
duration: 0.9,
|
|
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
|
});
|
|
try {
|
|
onLift?.(deficit);
|
|
} catch {
|
|
/* observer only */
|
|
}
|
|
}
|
|
// Both samples answered: the surface is measured, nothing left to wait for.
|
|
if (sampled === 2 || deficit > 0) return stop();
|
|
}
|
|
if (remaining <= 0) return stop();
|
|
timer = setTimeout(attempt, intervalMs);
|
|
};
|
|
timer = setTimeout(attempt, intervalMs);
|
|
return stop;
|
|
}
|