mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
108 lines
5.1 KiB
JavaScript
108 lines
5.1 KiB
JavaScript
// src/data/renderAltitude.js — pure priority-chain helper for aircraft render
|
|
// altitude (docs/plans/2026-07-05-entity-height-datum-fix.md Task 6).
|
|
//
|
|
// The globe needs ELLIPSOIDAL height (h = H + N). OpenSky's `geo_altitude`
|
|
// (state-vector index 13) is already WGS84 geometric/ellipsoidal — the
|
|
// correct value to hand `Cartesian3.fromDegrees(lon, lat, h)` verbatim.
|
|
// `baro_altitude` (index 7) is barometric/MSL-referenced (and, unlike a true
|
|
// GPS-derived geo_altitude, also carries non-standard-pressure QNH error) —
|
|
// it is only a VISUAL FALLBACK when geo_altitude is missing, corrected by
|
|
// adding the local geoid undulation N (src/data/geoid.js) so it at least
|
|
// lands close to the ellipsoid instead of sinking into high-elevation
|
|
// terrain. This module makes NO claim that baro+N is geometrically exact.
|
|
//
|
|
// Kept as a small standalone module (rather than inlined in flights.js) so
|
|
// it can be unit-tested in isolation and reused verbatim by the military
|
|
// flights layer (Task 7).
|
|
|
|
/**
|
|
* @param {object} params
|
|
* @param {number|null|undefined} params.geoAltM - OpenSky geo_altitude (m, WGS84 ellipsoidal), if reported.
|
|
* @param {number|null|undefined} params.baroAltM - Barometric/MSL altitude (m), if reported.
|
|
* @param {boolean} params.onGround - OpenSky on_ground flag.
|
|
* @param {number|null|undefined} params.surfaceM - Cached ellipsoidal ground height at this lat/lon (Task 3's cachedEllipsoidalGround), if warm.
|
|
* @param {number|null|undefined} [params.geoidN] - Geoid undulation N (m) at this lat/lon; treated as 0 when absent.
|
|
* @returns {number|null} Ellipsoidal render height in metres, or `null` (sentinel) when
|
|
* none of the above are usable — the caller applies its OWN existing sticky/default
|
|
* fallback (this helper never invents one, so it can't drift from the caller's policy).
|
|
*/
|
|
export function pickRenderAltitudeM({
|
|
geoAltM,
|
|
baroAltM,
|
|
onGround,
|
|
surfaceM,
|
|
geoidN,
|
|
}) {
|
|
if (onGround && Number.isFinite(surfaceM)) {
|
|
return surfaceM;
|
|
}
|
|
if (Number.isFinite(geoAltM)) {
|
|
return geoAltM;
|
|
}
|
|
if (Number.isFinite(baroAltM)) {
|
|
return baroAltM + (Number.isFinite(geoidN) ? geoidN : 0);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Ground-surface reuse for a TAXIING on-ground aircraft.
|
|
*
|
|
* The ellipsoidal-ground cache (Task 3) keys by exact 5-decimal-rounded
|
|
* lat/lon (~1.1 m). The warm-up batch resolves the CURRENT poll's fix for a
|
|
* LATER poll, but an aircraft that moves more than ~1 m between polls (any
|
|
* taxiing traffic — polls are tens of seconds apart) rounds to a fresh key
|
|
* every poll, so `cachedEllipsoidalGround(currentLat, currentLon)` misses
|
|
* indefinitely and a no-baro/no-geo aircraft would stick at the 0 m sentinel
|
|
* while it moves. LAST poll's fix, however, WAS resolved by last poll's warm
|
|
* batch, so its cached ground is available now. Airport aprons/taxiways are
|
|
* ~flat, so the previous fix's ellipsoidal ground is a good stand-in for the
|
|
* current one — far better than sinking to 0 m at an elevated field.
|
|
*
|
|
* @param {number|null|undefined} currentM - Ground at the CURRENT poll fix (may be a cache miss → null).
|
|
* @param {number|null|undefined} previousM - Ground at the PREVIOUS poll fix (warmed last poll), if any.
|
|
* @returns {number|null} The first finite of [current, previous], else null (sentinel).
|
|
*/
|
|
export function reuseGroundedSurfaceM(currentM, previousM) {
|
|
if (Number.isFinite(currentM)) return currentM;
|
|
if (Number.isFinite(previousM)) return previousM;
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Last-resort ground prior for a grounded contact whose floor cells are all
|
|
* cold and which reports NO altitude at all.
|
|
*
|
|
* The geoid is a crude stand-in: at a sea-level field it is the local ground to
|
|
* within metres, at an inland field it is tens of metres BELOW the rendered
|
|
* mesh. It earns its place only for a contact nothing else can describe — a
|
|
* first sighting, before any cell has warmed (the documented "born-grounded
|
|
* first poll" residual).
|
|
*
|
|
* It must NOT outrank a render height the contact already had. That was the
|
|
* 2026-08-21 defect: four Re:Earth refreshes in a row timed out, a parked
|
|
* contact's cells went cold, and this guess overwrote a height that had been
|
|
* sitting correctly on the mesh — dropping it through the ground until the
|
|
* proxy recovered. With `priorRenderM` present the caller's existing sticky
|
|
* fallback holds that height instead, which is what the owner asked for:
|
|
* "hold the last known altitude until a fresh one comes in."
|
|
*
|
|
* @param {object} params
|
|
* @param {number|null|undefined} params.geoAltM - Reported geo_altitude.
|
|
* @param {number|null|undefined} params.baroAltM - Reported baro_altitude.
|
|
* @param {number|null|undefined} params.priorRenderM - Render height this
|
|
* contact already had, if any.
|
|
* @param {number|null|undefined} params.geoidN - Geoid undulation N here.
|
|
* @returns {number|null} The geoid guess, or null when it must not be used.
|
|
*/
|
|
export function geoidSurfaceLastResortM({
|
|
geoAltM,
|
|
baroAltM,
|
|
priorRenderM,
|
|
geoidN,
|
|
}) {
|
|
if (Number.isFinite(geoAltM) || Number.isFinite(baroAltM)) return null;
|
|
if (Number.isFinite(priorRenderM)) return null;
|
|
return Number.isFinite(geoidN) ? geoidN : null;
|
|
}
|