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
6.9 KiB
JavaScript
199 lines
6.9 KiB
JavaScript
/**
|
||
* Pure monitor-plane footprint math shared by the client geometry
|
||
* (src/layers/cctv/geometry.js), the server sidecar join
|
||
* (server/providers/cctv/groundHeights.js) and the offline precompute
|
||
* (scripts/precompute-cctv-heights.mjs). Lives outside every ownership
|
||
* package on purpose. No Cesium, no DOM: the same function
|
||
* decides where a plane's support points are on the ground for both.
|
||
*
|
||
* The plane is the pitched far cap of the camera frustum: center at range R
|
||
* along the heading (horizontal R·cos(pitch)), half-width R·tan(hFov/2),
|
||
* half-height R·tan(vFov/2) with vFov from a 16:9 aspect, tilted by the
|
||
* pitch. Support points are a 3×3 grid over that rectangle: rows bottom /
|
||
* middle / top, columns left / center / right, named `bl bm br ml mc mr tl tm
|
||
* tr`. Their horizontal positions depend on the pose only, never on ground
|
||
* height, so an offline job can sample the ground under them for the nominal
|
||
* pose and the client can reuse those samples as long as the pose is the same.
|
||
*/
|
||
|
||
const EARTH_RADIUS_M = 6371000;
|
||
const PLANE_VERT_ASPECT = 16 / 9;
|
||
export const SUPPORT_KEYS = Object.freeze([
|
||
'bl',
|
||
'bm',
|
||
'br',
|
||
'ml',
|
||
'mc',
|
||
'mr',
|
||
'tl',
|
||
'tm',
|
||
'tr',
|
||
]);
|
||
|
||
const toRad = (deg) => (deg * Math.PI) / 180;
|
||
const toDeg = (rad) => (rad * 180) / Math.PI;
|
||
|
||
/**
|
||
* Offset a lat/lon by a distance along a compass heading (spherical earth;
|
||
* sub-centimetre at the ≤ 5 km ranges cameras use).
|
||
* @param {number} lat @param {number} lon @param {number} headingDeg @param {number} distM
|
||
* @returns {{lat:number, lon:number}}
|
||
*/
|
||
export function projectPoint(lat, lon, headingDeg, distM) {
|
||
const b = toRad(headingDeg);
|
||
const la = toRad(lat);
|
||
const lo = toRad(lon);
|
||
const ad = distM / EARTH_RADIUS_M;
|
||
const la2 = Math.asin(
|
||
Math.sin(la) * Math.cos(ad) + Math.cos(la) * Math.sin(ad) * Math.cos(b),
|
||
);
|
||
const lo2 =
|
||
lo +
|
||
Math.atan2(
|
||
Math.sin(b) * Math.sin(ad) * Math.cos(la),
|
||
Math.cos(ad) - Math.sin(la) * Math.sin(la2),
|
||
);
|
||
return { lat: toDeg(la2), lon: toDeg(lo2) };
|
||
}
|
||
|
||
/**
|
||
* Plane dimensions and offsets for a pose, before any ground is known.
|
||
* @param {{pitchDeg:number, fovDeg:number, rangeM:number}} pose
|
||
* @returns {{R:number, pitch:number, halfW:number, halfH:number, horiz:number,
|
||
* vert:number, upVert:number, upHoriz:number, vFovDeg:number}}
|
||
*/
|
||
export function planeDimensions(pose) {
|
||
const R = Math.max(1, Number(pose.rangeM) || 1);
|
||
const pitch = toRad(Math.max(-89, Math.min(89, Number(pose.pitchDeg) || 0)));
|
||
const hFov = toRad(Math.max(8, Math.min(160, Number(pose.fovDeg) || 74)));
|
||
const halfW = R * Math.tan(hFov / 2);
|
||
const vFovRad = 2 * Math.atan(Math.tan(hFov / 2) / PLANE_VERT_ASPECT);
|
||
const halfH = R * Math.tan(vFovRad / 2);
|
||
return {
|
||
R,
|
||
pitch,
|
||
halfW,
|
||
halfH,
|
||
horiz: R * Math.cos(pitch),
|
||
vert: R * Math.sin(pitch),
|
||
// In-plane "up", decomposed into a vertical part and a horizontal part
|
||
// along the heading (a downward pitch tilts the plane's top forward).
|
||
upVert: Math.cos(pitch) * halfH,
|
||
upHoriz: -Math.sin(pitch) * halfH,
|
||
vFovDeg: toDeg(vFovRad),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Horizontal (lat/lon) positions of the mount and the nine plane support
|
||
* points for a pose. Altitudes are not part of this: the caller adds the
|
||
* ground it measured under each point.
|
||
* @param {{lat:number, lon:number, headingDeg:number, pitchDeg:number,
|
||
* fovDeg:number, rangeM:number}} pose
|
||
* @returns {{mount:{lat:number,lon:number}, capCenter:{lat:number,lon:number},
|
||
* supports: Record<string,{lat:number,lon:number, row:-1|0|1, col:-1|0|1}>}}
|
||
*/
|
||
export function planeSupportPoints(pose) {
|
||
const dims = planeDimensions(pose);
|
||
const heading = Number(pose.headingDeg) || 0;
|
||
const cap = projectPoint(pose.lat, pose.lon, heading, dims.horiz);
|
||
const supports = {};
|
||
const rows = { b: -1, m: 0, t: 1 };
|
||
// Column letter: l / r, with the centre spelled `m` in the bottom and top
|
||
// rows (bm, tm) and `c` in the middle row (mc).
|
||
const cols = { l: -1, m: 0, c: 0, r: 1 };
|
||
for (const key of SUPPORT_KEYS) {
|
||
const row = rows[key[0]];
|
||
const col = cols[key[1]];
|
||
const across = projectPoint(
|
||
cap.lat,
|
||
cap.lon,
|
||
heading + 90,
|
||
col * dims.halfW,
|
||
);
|
||
const along = projectPoint(
|
||
across.lat,
|
||
across.lon,
|
||
heading,
|
||
row * dims.upHoriz,
|
||
);
|
||
supports[key] = { lat: along.lat, lon: along.lon, row, col };
|
||
}
|
||
return { mount: { lat: pose.lat, lon: pose.lon }, capCenter: cap, supports };
|
||
}
|
||
|
||
/**
|
||
* Altitude of the plane at a support point, given the plane center altitude:
|
||
* the rigid rectangle's rows sit ± upVert around the center.
|
||
* @param {number} capAltM @param {-1|0|1} row @param {{upVert:number}} dims
|
||
* @returns {number}
|
||
*/
|
||
function supportAltitude(capAltM, row, dims) {
|
||
return capAltM + row * dims.upVert;
|
||
}
|
||
|
||
/**
|
||
* The single rigid lift (metres, ≥ 0) that puts every support point at least
|
||
* `clearanceM` above the ground measured under it. Support points without a
|
||
* measured ground fall back to `fallbackGroundM` (normally the ground at the
|
||
* mount), which is what makes the pre-precompute path deterministic instead
|
||
* of buried.
|
||
* @param {number} capAltM - Unlifted plane center altitude.
|
||
* @param {{upVert:number}} dims
|
||
* @param {Record<string, number>|null|undefined} groundUnder - support key → ground altitude.
|
||
* @param {number} fallbackGroundM
|
||
* @param {number} clearanceM
|
||
* @returns {{liftM:number, limitingKey:string|null}}
|
||
*/
|
||
export function requiredPlaneLift(
|
||
capAltM,
|
||
dims,
|
||
groundUnder,
|
||
fallbackGroundM,
|
||
clearanceM,
|
||
) {
|
||
let liftM = 0;
|
||
let limitingKey = null;
|
||
const rows = { b: -1, m: 0, t: 1 };
|
||
for (const key of SUPPORT_KEYS) {
|
||
const raw = groundUnder ? groundUnder[key] : undefined;
|
||
// Strict: a null/absent support must fall back, not read as 0 m.
|
||
const ground =
|
||
typeof raw === 'number' && Number.isFinite(raw) ? raw : fallbackGroundM;
|
||
if (!Number.isFinite(ground)) continue;
|
||
const alt = supportAltitude(capAltM, rows[key[0]], dims);
|
||
const deficit = ground + clearanceM - alt;
|
||
if (deficit > liftM) {
|
||
liftM = deficit;
|
||
limitingKey = key;
|
||
}
|
||
}
|
||
return { liftM, limitingKey };
|
||
}
|
||
|
||
/**
|
||
* Stable hash of the nominal pose a footprint was sampled for, so shipped
|
||
* samples are only reused while the pose they describe is unchanged.
|
||
* @param {{lat:number, lon:number, headingDeg:number, pitchDeg:number,
|
||
* fovDeg:number, rangeM:number, mountHeightM:number}} pose
|
||
* @returns {string}
|
||
*/
|
||
export function poseHash(pose) {
|
||
const text = [
|
||
Number(pose.lat).toFixed(6),
|
||
Number(pose.lon).toFixed(6),
|
||
Number(pose.headingDeg).toFixed(1),
|
||
Number(pose.pitchDeg).toFixed(1),
|
||
Number(pose.fovDeg).toFixed(1),
|
||
Number(pose.rangeM).toFixed(1),
|
||
Number(pose.mountHeightM).toFixed(1),
|
||
].join('|');
|
||
// FNV-1a 32-bit, base36: short, stable, dependency-free.
|
||
let h = 0x811c9dc5;
|
||
for (let i = 0; i < text.length; i += 1) {
|
||
h ^= text.charCodeAt(i);
|
||
h = Math.imul(h, 0x01000193) >>> 0;
|
||
}
|
||
return `p1-${h.toString(36)}`;
|
||
}
|