mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
The first formal release. - One-click install via Pinokio; keyless boot lands on a live Esri World Imagery satellite globe with keyless terrain, with automatic OSM fallback and graceful degradation when terrain is unavailable. - Provider Settings (the POWER UP panel): add, replace, or remove API keys inside the app; credential files made owner-only before any secret is written; external keys shown read-only; the panel refuses shared or proxied servers. - Keyless capability responses for the optional HUD summary and place search. - Aircraft-identity voice answers cover operator, type, and route, and say so plainly when enrichment is unavailable. - README rewritten keyless-first; concise 0.1.0 changelog section; CI workflow included. Gates: 2,672 unit tests pass / 0 fail, build clean, tracking regression 108/108, map-source tray QA pass, setup doctor ready.
157 lines
6.8 KiB
JavaScript
157 lines
6.8 KiB
JavaScript
// src/data/geoid.test.mjs — EGM96 geoid-undulation lookup (docs/plans/2026-07-05-entity-height-datum-fix.md Task 1).
|
|
//
|
|
// Locks the module's public interface (later tasks — aircraft altitude
|
|
// correction, CCTV terrain fallback — call this verbatim):
|
|
// ensureGeoidReady(): Promise<void> lazy-loads the grid on first call
|
|
// geoidHeight(latDeg, lonDeg): number N in metres; throws if not ready
|
|
// orthometricToEllipsoidal(hMslM, latDeg, lonDeg): number hMslM + N
|
|
//
|
|
// Tolerance is loose (±2.5 m) by design: the bundled grid is EGM96 while the
|
|
// plan's "Verified facts" reference values are Re:Earth's EGM2008 — the two
|
|
// models differ by up to ~1 m, and the brief's own tolerance absorbs that
|
|
// spread rather than asserting exact agreement.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
ellipsoidalToMslDisplayM,
|
|
ensureGeoidReady,
|
|
geoidHeight,
|
|
orthometricToEllipsoidal,
|
|
} from './geoid.js';
|
|
|
|
const TOLERANCE_M = 2.5;
|
|
|
|
const LONDON = { lat: 51.5072, lon: -0.1275, nExpected: 46.1 };
|
|
const AUSTIN = { lat: 30.2672, lon: -97.7431, nExpected: -26.9 };
|
|
const SF = { lat: 37.7749, lon: -122.4194, nExpected: -32.2 };
|
|
const DENVER = { lat: 39.7392, lon: -104.9903, nExpected: -17.3 };
|
|
/** SFO runway 28R touchdown area — the cockpit/OSD field report's coordinates. */
|
|
const SFO = { lat: 37.616, lon: -122.368 };
|
|
|
|
test('geoidHeight throws before ensureGeoidReady() has resolved', () => {
|
|
// A fresh, never-initialized module instance can't be observed from the
|
|
// same process (ESM module cache), so this asserts the documented
|
|
// contract via the type check further down instead of re-importing.
|
|
// (See "ready-gate" test below for the real not-ready behavior.)
|
|
assert.equal(typeof geoidHeight, 'function');
|
|
});
|
|
|
|
test('ensureGeoidReady() resolves and is idempotent (safe to call repeatedly)', async () => {
|
|
await ensureGeoidReady();
|
|
await ensureGeoidReady();
|
|
await ensureGeoidReady();
|
|
});
|
|
|
|
test('geoidHeight matches known EGM96 undulation values within ±2.5 m', async () => {
|
|
await ensureGeoidReady();
|
|
for (const { lat, lon, nExpected } of [LONDON, AUSTIN, SF, DENVER]) {
|
|
const n = geoidHeight(lat, lon);
|
|
assert.ok(
|
|
Math.abs(n - nExpected) <= TOLERANCE_M,
|
|
`geoidHeight(${lat}, ${lon}) = ${n}, expected ≈ ${nExpected} (±${TOLERANCE_M})`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('orthometricToEllipsoidal adds the geoid undulation to the MSL height', async () => {
|
|
await ensureGeoidReady();
|
|
const hMslM = 15;
|
|
const hEllipsoidal = orthometricToEllipsoidal(hMslM, LONDON.lat, LONDON.lon);
|
|
// London geoid ≈ +46.1 -> 15 + 46.1 = 61.1, expect ≈ 61 within tolerance.
|
|
assert.ok(
|
|
Math.abs(hEllipsoidal - 61) <= TOLERANCE_M,
|
|
`orthometricToEllipsoidal(15, london) = ${hEllipsoidal}, expected ≈ 61 (±${TOLERANCE_M})`
|
|
);
|
|
// Must equal hMslM + geoidHeight exactly (same lookup, no extra fudge).
|
|
const n = geoidHeight(LONDON.lat, LONDON.lon);
|
|
assert.equal(hEllipsoidal, hMslM + n);
|
|
});
|
|
|
|
test('geoidHeight wraps longitude consistently (359.87 === -0.13)', async () => {
|
|
await ensureGeoidReady();
|
|
const wrapped = geoidHeight(51.5, 359.87);
|
|
const normal = geoidHeight(51.5, -0.13);
|
|
// Both longitudes name the same physical point (360° apart); the
|
|
// underlying grid lookup takes different floating-point paths to get
|
|
// there (mod-2π normalization), so equality is asserted to FP epsilon
|
|
// rather than bit-for-bit — this is the "consistent," not "identical
|
|
// bit pattern," invariant the brief calls for.
|
|
assert.ok(
|
|
Math.abs(wrapped - normal) < 1e-9,
|
|
`geoidHeight(51.5, 359.87) = ${wrapped}, geoidHeight(51.5, -0.13) = ${normal}`
|
|
);
|
|
});
|
|
|
|
// ── ellipsoidalToMslDisplayM — the ALT-readout datum correction ─────────────
|
|
//
|
|
// Field report (2026-08-22, cockpit parked at SFO): the camera OSD read
|
|
// "ALT: -15M" because Cesium's camera height is ELLIPSOIDAL and San Francisco
|
|
// sits ~32 m above the geoid's dip under the ellipsoid. Same family as the
|
|
// earlier JFK "ALT: -18M".
|
|
|
|
test('the SFO deck case: an ellipsoidal height equal to N reads as 0 m MSL', async () => {
|
|
await ensureGeoidReady();
|
|
const n = geoidHeight(SFO.lat, SFO.lon);
|
|
// A camera sitting exactly ON the geoid reports h = N against the ellipsoid.
|
|
assert.equal(ellipsoidalToMslDisplayM(n, n), 0);
|
|
});
|
|
|
|
test('the reported SFO cockpit OSD height turns into a small positive MSL number', async () => {
|
|
await ensureGeoidReady();
|
|
const n = geoidHeight(SFO.lat, SFO.lon);
|
|
assert.ok(n < -25 && n > -40, `SFO undulation should be strongly negative, got ${n}`);
|
|
// The owner's screenshot: ALT: -15m ellipsoidal over the SFO deck.
|
|
const displayed = ellipsoidalToMslDisplayM(-15, n);
|
|
assert.ok(
|
|
displayed > 10 && displayed < 25,
|
|
`-15 m ellipsoidal at SFO should read ≈ +17 m MSL, got ${displayed}`
|
|
);
|
|
});
|
|
|
|
test('a positive undulation lowers the readout — the correction subtracts N', async () => {
|
|
await ensureGeoidReady();
|
|
const n = geoidHeight(LONDON.lat, LONDON.lon);
|
|
assert.ok(n > 40, `London undulation should be strongly positive, got ${n}`);
|
|
// Cruise case: 10 km ellipsoidal over London reads ~46 m LOWER as MSL.
|
|
const cruise = ellipsoidalToMslDisplayM(10000, n);
|
|
assert.equal(cruise, 10000 - n);
|
|
assert.ok(
|
|
cruise > 9950 && cruise < 9960,
|
|
`10 000 m ellipsoidal over London should read ≈ 9954 m MSL, got ${cruise}`
|
|
);
|
|
});
|
|
|
|
test('ellipsoidalToMslDisplayM round-trips orthometricToEllipsoidal', async () => {
|
|
await ensureGeoidReady();
|
|
for (const point of [LONDON, AUSTIN, SF, DENVER]) {
|
|
const n = geoidHeight(point.lat, point.lon);
|
|
const ellipsoidal = orthometricToEllipsoidal(120, point.lat, point.lon);
|
|
// Exact to FP epsilon: +N then -N is the same lookup, no extra fudge.
|
|
assert.ok(Math.abs(ellipsoidalToMslDisplayM(ellipsoidal, n) - 120) < 1e-9);
|
|
}
|
|
});
|
|
|
|
test('an unavailable geoid returns the uncorrected height, never NaN or blank', () => {
|
|
// Grid still loading, lazy import failed, or an out-of-range lookup: the
|
|
// readout degrades to the ellipsoidal number rather than printing NaN.
|
|
for (const missing of [null, undefined, Number.NaN, 'x']) {
|
|
assert.equal(ellipsoidalToMslDisplayM(-15, missing), -15);
|
|
}
|
|
});
|
|
|
|
test('a non-finite height is passed through untouched', async () => {
|
|
await ensureGeoidReady();
|
|
const n = geoidHeight(SFO.lat, SFO.lon);
|
|
assert.equal(ellipsoidalToMslDisplayM(null, n), null);
|
|
assert.equal(ellipsoidalToMslDisplayM(undefined, n), undefined);
|
|
assert.ok(Number.isNaN(ellipsoidalToMslDisplayM(Number.NaN, n)));
|
|
});
|
|
|
|
test('geoidHeight throws a clear error if called before the grid is ready', async () => {
|
|
// Exercise the not-ready path via a fresh dynamic import under Node's ESM
|
|
// cache-busting query trick so this module instance has never had
|
|
// ensureGeoidReady() called on it.
|
|
const fresh = await import('./geoid.js?fresh-not-ready-check');
|
|
assert.throws(() => fresh.geoidHeight(0, 0), /not ready|ensureGeoidReady/i);
|
|
});
|