From eb7f05975094ece3f7517af2a7a63a5eaf94ef4b Mon Sep 17 00:00:00 2001 From: Leo Pechnicki Date: Tue, 29 Sep 2026 19:03:50 +0200 Subject: [PATCH 1/5] feat(layers): highlight aircraft broadcasting a transponder emergency Flights and Military Flights tint aircraft red while the latest poll carries squawk 7500/7600/7700 or an adsb.lol ADS-B emergency status (general/unlawful/nordo/downed). Minimum-fuel and lifeguard priorities are reported but not tinted. The Cockpit HUD aircraft-meta line names the broadcast code in red. The status is not sticky: a cleared code stops highlighting on the next refresh. The analyst/voice query engine gains squawk, emergency and emergencyKind fields. No new source or request: OpenSky state vectors already carry the squawk and adsb.lol/readsb carries both fields. Built by the Hex project (Crew Leo Agile dev team). Co-Authored-By: Claude --- CHANGELOG.md | 11 + DATA_SOURCES.md | 1 + scripts/package-boundaries.json | 10 +- scripts/qa-emergency-squawk.mjs | 437 +++++++++++++++++++++++++++ src/cockpitMath.js | 14 + src/cockpitMath.test.mjs | 159 +++++++--- src/data/aircraftEmergency.js | 71 +++++ src/data/aircraftEmergency.test.mjs | 55 ++++ src/data/analystEngine.test.mjs | 334 +++++++++++++++++--- src/data/flights.test.mjs | 3 + src/data/militaryFlights.test.mjs | 20 ++ src/layers/flights/policy.js | 6 + src/layers/flights/queries.js | 13 +- src/layers/flights/records.js | 5 + src/layers/flights/records.test.mjs | 10 + src/layers/flights/rendering.js | 29 +- src/layers/military/policy.js | 6 + src/layers/military/queries.js | 12 +- src/layers/military/records.js | 4 + src/layers/military/records.test.mjs | 16 + src/layers/military/rendering.js | 20 +- src/sources/live/aircraft.js | 5 + src/sources/live/contract.test.mjs | 20 ++ src/ui/cockpitInstruments.js | 11 +- src/ui/styles/cockpit.css | 3 + src/ui/styles/cyber.css | 6 + 26 files changed, 1174 insertions(+), 107 deletions(-) create mode 100644 scripts/qa-emergency-squawk.mjs create mode 100644 src/data/aircraftEmergency.js create mode 100644 src/data/aircraftEmergency.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d15d897..50e731268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,17 @@ ## [Unreleased] +- Flights and Military Flights highlight aircraft broadcasting a transponder + emergency. Squawk 7500 (unlawful interference), 7600 (radio failure) and + 7700 (general emergency) — or adsb.lol's ADS-B `emergency` status, which + also reports downed aircraft — tint the aircraft red on the globe, in + Cockpit contacts and in snapshots, and name the code on the Cockpit HUD. + Minimum-fuel and lifeguard statuses are reported but not tinted. The status + follows the latest poll instead of sticking, so a cleared code stops + highlighting on the next refresh. The analyst and voice query engine can + filter by `squawk`, `emergency` and `emergencyKind`. No new source or + request: OpenSky already carries the squawk and adsb.lol carries both. + - Street Level: Google Street View is the second provider. On the Google 3D map (Google's terms keep it off other maps), light its chip (off by default: every panorama is billed to the Google Maps key, which needs the diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 040e53afc..ea1113642 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -78,6 +78,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **Google Maps Platform.** You supply your own API key and are bound by [Google's ToS](https://cloud.google.com/maps-platform/terms). Google Maps Content (tiles, geocodes, places) **may not be cached, stored, rehosted, or committed** — this app only ever uses it live, which is the compliant pattern. The "Google" attribution is displayed on the globe and must stay visible. Restrict your key (see [SECURITY.md](SECURITY.md)). - **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default. - **adsb.lol flight fallback.** When OpenSky is unavailable and no last-good OpenSky response exists, the server requests a cached, capped 250 nm adsb.lol point snapshot around the current camera subpoint. This is regional observed context, not worldwide completeness; provenance is exposed in the Flights stats/context row. Military ICAOs remain reconciled through the existing dedicated military registry rather than duplicated. +- **Transponder emergencies.** Emergency highlighting reads fields the flight feeds already return: OpenSky state vectors' `squawk` and adsb.lol/readsb's `squawk` and `emergency`. It makes no extra request. A broadcast code states what the transponder sends, not a verified incident; OpenSky has no ADS-B emergency field, so civil highlighting there relies on squawk 7500/7600/7700 alone. - **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it. - **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state. - **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover. diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index fc7308ab5..ad81ef5b6 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -794,6 +794,7 @@ "modules": [ "src/cyberSonar.js", "src/data/aircraftClass.js", + "src/data/aircraftEmergency.js", "src/data/aircraftIcons.js", "src/data/aircraftMeta.js", "src/data/aircraftNearbyPolicy.js", @@ -837,6 +838,7 @@ "modules": [ "src/cyberSonar.js", "src/data/aircraftClass.js", + "src/data/aircraftEmergency.js", "src/data/aircraftIcons.js", "src/data/aircraftMeta.js", "src/data/aircraftNearbyPolicy.js", @@ -1494,6 +1496,7 @@ "src/contextModePolicy.js", "src/creditKeyboard.js", "src/data/aircraftClass.js", + "src/data/aircraftEmergency.js", "src/data/aircraftIcons.js", "src/data/aircraftMeta.js", "src/data/aircraftNearbyPolicy.js", @@ -2276,6 +2279,7 @@ "src/celestialRing.js", "src/cockpitMath.js", "src/data/aircraftClass.js", + "src/data/aircraftEmergency.js", "src/data/aircraftIcons.js", "src/data/aircraftMeta.js", "src/data/aircraftNearbyPolicy.js", @@ -2897,7 +2901,8 @@ "src/layers/flights/recordPolicy.js", "src/data/aircraftMeta.js", "src/data/renderAltitude.js", - "src/data/aircraftClass.js" + "src/data/aircraftClass.js", + "src/data/aircraftEmergency.js" ], "external": [] }, @@ -2916,7 +2921,8 @@ "src/layers/military/recordPolicy.js", "src/data/aircraftMeta.js", "src/data/renderAltitude.js", - "src/data/aircraftClass.js" + "src/data/aircraftClass.js", + "src/data/aircraftEmergency.js" ], "external": [] }, diff --git a/scripts/qa-emergency-squawk.mjs b/scripts/qa-emergency-squawk.mjs new file mode 100644 index 000000000..bbcc98c22 --- /dev/null +++ b/scripts/qa-emergency-squawk.mjs @@ -0,0 +1,437 @@ +#!/usr/bin/env node +/** + * qa-emergency-squawk.mjs — headless proof for transponder emergency highlighting. + * + * Drives the REAL app in headless Chromium against a dev server (keyless). + * `/api/opensky` and `/api/adsblol/mil` are stubbed in the page so the codes + * are deterministic; everything downstream (normalizers, records, rendering, + * Cockpit HUD) is production code. + * + * (i) TINT — a civil 7600 and a military ADS-B `general` emergency are + * drawn red; a normal civil flight stays white, a normal military + * flight stays amber, and a military `minfuel` priority stays amber. + * (ii) HUD — tracking the military emergency and entering Cockpit names + * the broadcast code on the aircraft-meta line, in red. + * (iii) CLEAR — the next poll drops the code: the HUD line returns to its + * normal wording (the status is not sticky). + * + * Screenshots saved to qa-shots/emergency-*.png (gitignored). + * + * Run: node scripts/qa-emergency-squawk.mjs --url http://localhost:4173 + * Exits non-zero on any FAIL. Does not commit anything. + */ + +import puppeteer from 'puppeteer'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, '..'); +const SHOTS_DIR = path.join(REPO_ROOT, 'qa-shots'); + +const argv = process.argv.slice(2); +const getOpt = (name, dflt) => { + const i = argv.indexOf(name); + return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt; +}; +const APP_URL = getOpt('--url', 'http://localhost:4173'); +const HEADFUL = argv.includes('--headful'); +const CENTER = { lon: 4.0, lat: 53.5 }; + +const results = []; +function record(name, ok, detail) { + results.push({ name, ok, detail }); + const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'; + console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`); +} +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +/** OpenSky state vector: squawk sits at index 14. */ +const civilRow = (icao24, callsign, lon, lat, squawk) => [ + icao24, + callsign, + 'Netherlands', + null, + null, + lon, + lat, + 10000, + false, + 230, + 90, + 0, + null, + 10050, + squawk, + false, + 0, +]; + +/** readsb row as adsb.lol /v2/mil returns it. */ +const milRow = (hex, flight, lon, lat, squawk, emergency) => ({ + hex, + flight, + lon, + lat, + alt_baro: 24000, + alt_geom: 24200, + gs: 380, + track: 270, + t: 'C17', + r: `QA-${hex}`, + squawk, + emergency, + seen: 1, + seen_pos: 1, +}); + +/** Page-side route stubs; `__squawkQaPhase` switches the military emergency off. */ +function installStubs() { + const realFetch = window.fetch.bind(window); + window.__squawkQaPhase = 'emergency'; + const json = (body) => + Promise.resolve( + new Response(JSON.stringify(body), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + window.fetch = (input, init) => { + const url = typeof input === 'string' ? input : input?.url || ''; + const now = Date.now(); + const { civil, mil } = window.__squawkQaFixtures; + if (url.includes('/api/opensky-track')) return json({ path: [] }); + if (url.includes('/api/opensky')) { + const t = Math.floor(now / 1000); + return json({ + time: t, + states: civil.map((row) => { + const copy = [...row]; + copy[3] = t - 1; + copy[4] = t - 1; + return copy; + }), + }); + } + if (url.includes('/api/adsblol/mil')) { + const cleared = window.__squawkQaPhase === 'cleared'; + return json({ + msg: 'No error', + now, + ac: mil.map((row) => + cleared && row.hex === 'ae7700' + ? { ...row, squawk: '4521', emergency: 'none' } + : row, + ), + }); + } + return realFetch(input, init); + }; +} + +/** Colour of every shown fleet billboard whose id is one of `ids`. */ +function billboardColors(ids) { + const { viewer } = window.__godsEyeView; + const found = {}; + const seen = new Set(); + const visit = (primitive) => { + if (!primitive || seen.has(primitive)) return; + seen.add(primitive); + if ( + typeof primitive.length !== 'number' || + typeof primitive.get !== 'function' + ) + return; + for (let i = 0; i < primitive.length; i++) { + const item = primitive.get(i); + if (item?.color && ids.includes(item.id) && item.show !== false) { + found[item.id] = { + r: Math.round(item.color.red * 255), + g: Math.round(item.color.green * 255), + b: Math.round(item.color.blue * 255), + }; + } else visit(item); + } + }; + visit(viewer.scene.primitives); + return found; +} + +const isRed = (c) => Boolean(c) && c.r > 200 && c.g < 110 && c.b < 110; +const isWhite = (c) => Boolean(c) && c.r > 220 && c.g > 220 && c.b > 220; +const isAmber = (c) => + Boolean(c) && c.r > 180 && c.g > 110 && c.b < 140 && !isRed(c); + +async function shoot(page, name) { + await page + .waitForFunction( + () => window.__godsEyeView.viewer.scene.globe.tilesLoaded, + { timeout: 30000 }, + ) + .catch(() => {}); + await sleep(600); + await page.screenshot({ + path: path.join(SHOTS_DIR, `emergency-${name}.png`), + }); +} + +async function main() { + console.log('\nEmergency squawk proof (qa-emergency-squawk)'); + console.log(` App URL : ${APP_URL}\n`); + try { + const res = await fetch(APP_URL); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + } catch (e) { + console.error( + `\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`, + ); + process.exit(2); + } + fs.mkdirSync(SHOTS_DIR, { recursive: true }); + const executablePath = + process.env.PUPPETEER_EXECUTABLE_PATH || + (await puppeteer.executablePath().catch(() => null)) || + undefined; + const browser = await puppeteer.launch({ + headless: HEADFUL ? false : 'new', + protocolTimeout: 180000, + ...(executablePath && fs.existsSync(executablePath) + ? { executablePath } + : {}), + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--use-gl=angle', + '--use-angle=swiftshader', + '--disable-dev-shm-usage', + '--disable-background-timer-throttling', + '--disable-renderer-backgrounding', + '--window-size=1440,900', + ], + }); + + let exitCode = 0; + const errors = []; + try { + const page = await browser.newPage(); + await page.setViewport({ width: 1440, height: 900 }); + page.on('pageerror', (e) => errors.push(e.message)); + const { lon, lat } = CENTER; + await page.evaluateOnNewDocument( + (fixtures) => { + window.__squawkQaFixtures = fixtures; + }, + { + civil: [ + civilRow('4847a1', 'KLM7600 ', lon - 0.6, lat + 0.25, '7600'), + civilRow('4847a2', 'KLM1234 ', lon - 0.6, lat - 0.25, '2000'), + ], + mil: [ + milRow('ae7700', 'RCH7700', lon + 0.6, lat + 0.25, '7700', 'general'), + milRow('ae0f01', 'RCH0F01', lon + 0.6, lat - 0.25, '1234', 'minfuel'), + milRow('ae0001', 'RCH0001', lon, lat - 0.5, '4520', 'none'), + ], + }, + ); + // Stub in-page rather than with request interception: an intercepted page + // stalls Cesium's workers (see qa-gnss.mjs). + await page.evaluateOnNewDocument(installStubs); + await page.goto(`${APP_URL}/?welcome=0`, { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + await page.waitForFunction( + () => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager, + { timeout: 60000 }, + ); + await sleep(5000); + await page.keyboard.press('Escape'); + await page.evaluate( + (lo, la) => { + const gev = window.__godsEyeView; + const d2r = Math.PI / 180; + try { + gev.viewer.camera.cancelFlight(); + } catch { + /* no flight active */ + } + gev.viewer.camera.setView({ + destination: gev.viewer.scene.globe.ellipsoid.cartographicToCartesian( + { + longitude: lo * d2r, + latitude: la * d2r, + height: 260000, + }, + ), + orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 }, + }); + gev.viewer.scene.requestRender?.(); + }, + lon, + lat, + ); + + // ── (i) TINT ────────────────────────────────────────────────────────── + console.log('(i) TINT — emergency codes through the real layers...'); + await page.evaluate(async () => { + const dm = window.__godsEyeView.dataManager; + await dm.setEnabled('flights', true, { origin: 'user' }); + await dm.setEnabled('military', true, { origin: 'user' }); + }); + const ids = ['4847a1', '4847a2', 'ae7700', 'ae0f01', 'ae0001']; + const probe = `(${billboardColors})(${JSON.stringify(ids)})`; + await page + .waitForFunction(`Object.keys(${probe}).length === ${ids.length}`, { + timeout: 60000, + polling: 500, + }) + .catch(() => {}); + await sleep(1500); + const colors = await page.evaluate(probe); + const show = (id) => JSON.stringify(colors[id] ?? null); + record( + 'TINT: civil squawk 7600 is red', + isRed(colors['4847a1']), + show('4847a1'), + ); + record( + 'TINT: normal civil flight stays white', + isWhite(colors['4847a2']), + show('4847a2'), + ); + record( + 'TINT: military ADS-B general emergency is red', + isRed(colors.ae7700), + show('ae7700'), + ); + record( + 'TINT: military minimum-fuel priority stays amber', + isAmber(colors.ae0f01), + show('ae0f01'), + ); + record( + 'TINT: normal military flight stays amber', + isAmber(colors.ae0001), + show('ae0001'), + ); + await shoot(page, 'globe-tint'); + + // ── (ii) HUD ────────────────────────────────────────────────────────── + console.log('\n(ii) HUD — track the emergency and enter Cockpit...'); + const tracked = await page.evaluate(() => + Boolean( + window.__godsEyeView.dataManager.layers + .get('military') + .module.trackById('ae7700', { origin: 'user' }), + ), + ); + record( + 'HUD: emergency aircraft can be tracked', + tracked, + `tracked=${tracked}`, + ); + // Cockpit entry is gated on the Contacts (flights) context mode. + await page.evaluate(() => + window.__godsEyeView.styleManager._contextControls._selectContextMode( + 'flights', + ), + ); + await page + .waitForFunction( + () => { + const entry = document.getElementById('cockpit-entry'); + return entry && !entry.hidden && !entry.disabled; + }, + { timeout: 20000 }, + ) + .catch(() => {}); + await page + .$eval('#cockpit-entry', (entry) => entry.click()) + .catch(() => {}); + const hudReady = await page + .waitForFunction( + () => + document.body.classList.contains('cockpit-mode') && + /SQUAWK 7700/.test( + document.getElementById('cockpit-aircraft-meta')?.textContent || '', + ), + { timeout: 30000 }, + ) + .then( + () => true, + () => false, + ); + const hud = await page.evaluate(() => { + const meta = document.getElementById('cockpit-aircraft-meta'); + return { + cockpit: document.body.classList.contains('cockpit-mode'), + text: meta?.textContent || '', + emergencyClass: meta?.classList.contains('emergency') || false, + color: meta ? getComputedStyle(meta).color : null, + }; + }); + record( + 'HUD: meta line names the broadcast code', + hudReady && hud.text.includes('SQUAWK 7700 · GENERAL EMERGENCY'), + JSON.stringify(hud), + ); + record( + 'HUD: meta line is styled as an emergency', + hud.emergencyClass && /255,\s*68,\s*68/.test(hud.color || ''), + `class=${hud.emergencyClass} color=${hud.color}`, + ); + await shoot(page, 'cockpit-hud'); + + // ── (iii) CLEAR ─────────────────────────────────────────────────────── + console.log('\n(iii) CLEAR — the next poll drops the code...'); + await page.evaluate(async () => { + window.__squawkQaPhase = 'cleared'; + await window.__godsEyeView.dataManager.refreshLayer('military'); + }); + const cleared = await page + .waitForFunction( + () => + !document + .getElementById('cockpit-aircraft-meta') + ?.classList.contains('emergency'), + { timeout: 20000 }, + ) + .then( + () => true, + () => false, + ); + const after = await page.evaluate( + () => document.getElementById('cockpit-aircraft-meta')?.textContent || '', + ); + record( + 'CLEAR: HUD returns to its normal wording', + cleared && !after.includes('SQUAWK') && after.includes('COURSE ALIGNED'), + JSON.stringify(after), + ); + await shoot(page, 'cockpit-cleared'); + record( + 'no uncaught browser errors', + errors.length === 0, + errors.slice(0, 3).join(' | '), + ); + } catch (err) { + console.error('\x1b[31mHarness error:\x1b[0m', err); + exitCode = 3; + } finally { + await browser.close(); + } + + const pass = results.filter((r) => r.ok).length; + const fail = results.length - pass; + console.log('\n' + '─'.repeat(60)); + console.log(` RESULT: ${pass} passed, ${fail} failed`); + console.log(` Shots : ${SHOTS_DIR}/emergency-*.png`); + console.log('─'.repeat(60) + '\n'); + process.exit(exitCode || (fail > 0 ? 1 : 0)); +} + +main().catch((e) => { + console.error(e); + process.exit(3); +}); diff --git a/src/cockpitMath.js b/src/cockpitMath.js index a0bd83676..4c9666ee9 100644 --- a/src/cockpitMath.js +++ b/src/cockpitMath.js @@ -83,6 +83,20 @@ export function cockpitAltitudeDisplayFt(altitudeM, onGround) { return Number.isFinite(altitudeM) ? altitudeM * 3.28084 : null; } +/** + * HUD wording for a tracked aircraft's transponder emergency, or null. + * States the broadcast code, never a conclusion about the flight. + * @param {{label?: string, squawk?: string|null}|null|undefined} emergency + * @returns {string|null} + */ +export function formatCockpitEmergency(emergency) { + const label = String(emergency?.label || '') + .trim() + .toUpperCase(); + if (!label) return null; + return emergency.squawk ? `SQUAWK ${emergency.squawk} · ${label}` : label; +} + /** Format the cockpit Context scope without overstating installation coverage. */ export function formatCockpitContextScope( subjectLabel, diff --git a/src/cockpitMath.test.mjs b/src/cockpitMath.test.mjs index ef1e8382b..8abbf1c3e 100644 --- a/src/cockpitMath.test.mjs +++ b/src/cockpitMath.test.mjs @@ -10,6 +10,7 @@ import { cockpitGroundSafeHeight, cockpitSurfaceWaitExpired, cockpitUiUpdateDue, + formatCockpitEmergency, compassDivisions, formatAltitudeRulerTick, formatCockpitContextScope, @@ -41,7 +42,11 @@ test('cockpit anchor correction cannot turn a forward step into a reversal', () const speedMps = 200; const dtSec = 0.1; const forwardStepM = speedMps * dtSec; - const backwardCorrectionM = cockpitAnchorCorrectionStep(1000, speedMps, dtSec); + const backwardCorrectionM = cockpitAnchorCorrectionStep( + 1000, + speedMps, + dtSec, + ); assert.ok(backwardCorrectionM > 0); assert.ok(backwardCorrectionM < forwardStepM); }); @@ -77,10 +82,7 @@ test('cockpit Context scope distinguishes radius-complete feeds from viewport in }); test('cockpit Context scope preserves zero and replaces missing values intentionally', () => { - assert.equal( - formatCockpitContextScope('', 0), - '— · 0 KM AIR/SEA WINDOW', - ); + assert.equal(formatCockpitContextScope('', 0), '— · 0 KM AIR/SEA WINDOW'); assert.equal( formatCockpitContextScope(undefined, Number.NaN, {}), '— · — KM AIR/SEA WINDOW', @@ -168,26 +170,29 @@ test('relative bearing stays on the shortest signed arc', () => { }); test('HUD rail layout centers between intersecting upper and lower obstacles', () => { - assert.deepEqual(resolveHudRailLayout({ - viewportHeight: 1000, - panelHeight: 200, - laneLeft: 50, - laneRight: 410, - baseTop: 280, - baseBottom: 940, - gap: 10, - obstacles: [ - { left: 40, right: 240, top: 320, bottom: 460 }, - { left: 300, right: 1000, top: 780, bottom: 930 }, - { left: 900, right: 1100, top: 300, bottom: 700 }, - ], - }), { - top: 520, - maxHeight: 300, - safeTop: 470, - safeBottom: 770, - constrained: false, - }); + assert.deepEqual( + resolveHudRailLayout({ + viewportHeight: 1000, + panelHeight: 200, + laneLeft: 50, + laneRight: 410, + baseTop: 280, + baseBottom: 940, + gap: 10, + obstacles: [ + { left: 40, right: 240, top: 320, bottom: 460 }, + { left: 300, right: 1000, top: 780, bottom: 930 }, + { left: 900, right: 1100, top: 300, bottom: 700 }, + ], + }), + { + top: 520, + maxHeight: 300, + safeTop: 470, + safeBottom: 770, + constrained: false, + }, + ); }); test('HUD rail layout constrains an oversized panel to the safe corridor', () => { @@ -241,7 +246,10 @@ function contextSnapshot(subject, extra = {}) { } test('Contact panel hides only when there is no snapshot at all', () => { - const readout = resolveCockpitContextReadout({ snapshot: null, info: TRACKED_INFO }); + const readout = resolveCockpitContextReadout({ + snapshot: null, + info: TRACKED_INFO, + }); assert.equal(readout.visible, false); assert.equal(readout.mode, 'standby'); assert.equal( @@ -257,14 +265,22 @@ test('Contact panel survives NEXT onto a vessel or installation subject', () => // camera: "click next... whole left panel disappears"). for (const subject of [ { layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' }, - { layerId: 'military-installations', id: 'fort-hood', label: 'FORT CAVAZOS' }, + { + layerId: 'military-installations', + id: 'fort-hood', + label: 'FORT CAVAZOS', + }, { layerId: 'military', id: 'ae01ce', label: 'RCH451' }, ]) { const readout = resolveCockpitContextReadout({ snapshot: contextSnapshot(subject), info: TRACKED_INFO, }); - assert.equal(readout.visible, true, `${subject.layerId} subject must keep the panel up`); + assert.equal( + readout.visible, + true, + `${subject.layerId} subject must keep the panel up`, + ); assert.equal(readout.mode, 'foreign'); assert.equal(readout.subjectMatchesTracked, false); } @@ -272,7 +288,11 @@ test('Contact panel survives NEXT onto a vessel or installation subject', () => test('a foreign subject dashes the aircraft-relative fields and keeps the rest live', () => { const readout = resolveCockpitContextReadout({ - snapshot: contextSnapshot({ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' }), + snapshot: contextSnapshot({ + layerId: 'ais-live-vessels', + id: '353136000', + label: 'MAERSK DETROIT', + }), info: TRACKED_INFO, }); assert.equal(readout.visible, true); @@ -281,12 +301,20 @@ test('a foreign subject dashes the aircraft-relative fields and keeps the rest l // frame while every other row value is measured from the subject; rendering // both live would present one mixed-frame reading as a single measurement. assert.equal(readout.aircraftRelative, false); - assert.equal(readout.contactLost, false, 'subject-frame values still refresh'); + assert.equal( + readout.contactLost, + false, + 'subject-frame values still refresh', + ); }); test('the tracked aircraft as subject keeps every field in its own frame', () => { const readout = resolveCockpitContextReadout({ - snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077', label: 'SWA1234' }), + snapshot: contextSnapshot({ + layerId: 'flights', + id: 'aaa077', + label: 'SWA1234', + }), info: TRACKED_INFO, }); assert.equal(readout.visible, true); @@ -305,7 +333,11 @@ test('a fast-culled subject keeps the panel up as CONTACT LOST instead of collap snapshot: contextSnapshot(subject, { subjectPresent: false }), info: TRACKED_INFO, }); - assert.equal(readout.visible, true, 'a lost contact must not take the panel down'); + assert.equal( + readout.visible, + true, + 'a lost contact must not take the panel down', + ); assert.equal(readout.mode, 'lost'); assert.equal(readout.contactLost, true, 'last-known values stay on screen'); } @@ -313,7 +345,10 @@ test('a fast-culled subject keeps the panel up as CONTACT LOST instead of collap test('an explicitly present subject is never reported lost', () => { const present = resolveCockpitContextReadout({ - snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077' }, { subjectPresent: true }), + snapshot: contextSnapshot( + { layerId: 'flights', id: 'aaa077' }, + { subjectPresent: true }, + ), info: TRACKED_INFO, }); assert.equal(present.contactLost, false); @@ -332,20 +367,36 @@ test('the tracked flight layer is resolved by normalized tracked identity', () = // Both layers describe a tracked aircraft during a cross-layer handoff; // civilian-first precedence would hand the cockpit the wrong aircraft. assert.equal( - resolveTrackedAircraftInfo({ civilian, military, trackedId: 'military:ae01ce' }).layerId, + resolveTrackedAircraftInfo({ + civilian, + military, + trackedId: 'military:ae01ce', + }).layerId, 'military', ); assert.equal( - resolveTrackedAircraftInfo({ civilian, military, trackedId: 'military:ae01ce' }).icao24, + resolveTrackedAircraftInfo({ + civilian, + military, + trackedId: 'military:ae01ce', + }).icao24, 'ae01ce', ); assert.equal( - resolveTrackedAircraftInfo({ civilian, military, trackedId: 'flights:aaa077' }).layerId, + resolveTrackedAircraftInfo({ + civilian, + military, + trackedId: 'flights:aaa077', + }).layerId, 'flights', ); // Case-insensitive: layers stamp lowercase hex, callers may not. assert.equal( - resolveTrackedAircraftInfo({ civilian, military, trackedId: 'MILITARY:AE01CE' }).layerId, + resolveTrackedAircraftInfo({ + civilian, + military, + trackedId: 'MILITARY:AE01CE', + }).layerId, 'military', ); }); @@ -355,12 +406,38 @@ test('tracked-identity resolution falls back to layer precedence', () => { const military = { icao24: 'ae01ce' }; // No stamped identity (or one that matches neither layer) keeps the historic // civilian-first order so tracking paths without gevTrackedId still work. - assert.equal(resolveTrackedAircraftInfo({ civilian, military }).layerId, 'flights'); assert.equal( - resolveTrackedAircraftInfo({ civilian, military, trackedId: 'satellites:25544' }).layerId, + resolveTrackedAircraftInfo({ civilian, military }).layerId, 'flights', ); - assert.equal(resolveTrackedAircraftInfo({ military, trackedId: '' }).layerId, 'military'); - assert.equal(resolveTrackedAircraftInfo({ trackedId: 'flights:aaa077' }), null); + assert.equal( + resolveTrackedAircraftInfo({ + civilian, + military, + trackedId: 'satellites:25544', + }).layerId, + 'flights', + ); + assert.equal( + resolveTrackedAircraftInfo({ military, trackedId: '' }).layerId, + 'military', + ); + assert.equal( + resolveTrackedAircraftInfo({ trackedId: 'flights:aaa077' }), + null, + ); assert.equal(resolveTrackedAircraftInfo(), null); }); + +test('cockpit emergency wording states the broadcast code, not a conclusion', () => { + assert.equal( + formatCockpitEmergency({ label: 'General emergency', squawk: '7700' }), + 'SQUAWK 7700 · GENERAL EMERGENCY', + ); + assert.equal( + formatCockpitEmergency({ label: 'Minimum fuel', squawk: null }), + 'MINIMUM FUEL', + ); + assert.equal(formatCockpitEmergency(null), null); + assert.equal(formatCockpitEmergency({ label: '' }), null); +}); diff --git a/src/data/aircraftEmergency.js b/src/data/aircraftEmergency.js new file mode 100644 index 000000000..e7ee3aca5 --- /dev/null +++ b/src/data/aircraftEmergency.js @@ -0,0 +1,71 @@ +// src/data/aircraftEmergency.js +/** + * Transponder emergency status for one aircraft, from the Mode A squawk and + * the ADS-B emergency/priority field. + * + * Emergency squawks are ICAO-standard worldwide: 7500 unlawful interference, + * 7600 radio failure, 7700 general emergency. readsb (adsb.lol) also reports + * `emergency`, "a superset of the 7x00 squawks": none, general, lifeguard, + * minfuel, nordo, unlawful, downed, reserved. OpenSky state vectors carry the + * squawk only. + * + * The status is what the transponder broadcast in THIS poll, never a finding: + * crews pass through 7x00 codes while dialling, and a real emergency clears + * when the code does. Callers keep it per poll (not sticky). + */ + +/** @constant {string} Tint for aircraft broadcasting an emergency (not a priority). */ +export const EMERGENCY_TINT_CSS = '#FF4444'; + +const SQUAWK_KINDS = { 7500: 'unlawful', 7600: 'nordo', 7700: 'general' }; + +const KINDS = { + general: { label: 'General emergency', severity: 'emergency' }, + unlawful: { label: 'Unlawful interference', severity: 'emergency' }, + nordo: { label: 'Radio failure', severity: 'emergency' }, + downed: { label: 'Downed aircraft', severity: 'emergency' }, + minfuel: { label: 'Minimum fuel', severity: 'priority' }, + lifeguard: { label: 'Lifeguard (medical priority)', severity: 'priority' }, +}; + +/** + * A Mode A code as its four octal digits, else null. + * @param {unknown} value + * @returns {string|null} + */ +export function normalizeSquawk(value) { + const code = String(value ?? '').trim(); + return /^[0-7]{4}$/.test(code) ? code : null; +} + +/** + * Emergency status from one observation. An explicit ADS-B status wins; a + * 7x00 squawk counts even when the ADS-B field says `none`. + * @param {{squawk?: unknown, emergency?: unknown}} [observation] + * @returns {{kind: string, label: string, severity: 'emergency'|'priority', + * squawk: string|null, source: 'ads-b'|'squawk'}|null} + */ +export function aircraftEmergency({ squawk, emergency } = {}) { + const code = normalizeSquawk(squawk); + const reported = String(emergency ?? '') + .trim() + .toLowerCase(); + const fromAdsb = Object.hasOwn(KINDS, reported); + const kind = fromAdsb ? reported : (SQUAWK_KINDS[code] ?? null); + if (!kind) return null; + return { + kind, + ...KINDS[kind], + squawk: code, + source: fromAdsb ? 'ads-b' : 'squawk', + }; +} + +/** + * True when the aircraft is broadcasting an emergency (not a priority status). + * @param {{severity?: string}|null|undefined} status + * @returns {boolean} + */ +export function isEmergency(status) { + return status?.severity === 'emergency'; +} diff --git a/src/data/aircraftEmergency.test.mjs b/src/data/aircraftEmergency.test.mjs new file mode 100644 index 000000000..cae9f6c50 --- /dev/null +++ b/src/data/aircraftEmergency.test.mjs @@ -0,0 +1,55 @@ +// src/data/aircraftEmergency.test.mjs +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { + aircraftEmergency, + isEmergency, + normalizeSquawk, +} from './aircraftEmergency.js'; + +test('the three ICAO emergency squawks map to their kinds', () => { + assert.equal(aircraftEmergency({ squawk: '7500' }).kind, 'unlawful'); + assert.equal(aircraftEmergency({ squawk: '7600' }).kind, 'nordo'); + const general = aircraftEmergency({ squawk: ' 7700 ' }); + assert.deepEqual(general, { + kind: 'general', + label: 'General emergency', + severity: 'emergency', + squawk: '7700', + source: 'squawk', + }); +}); + +test('ordinary codes, blanks and non-octal values are not emergencies', () => { + for (const squawk of ['1200', '7000', '2000', '', null, undefined, '7800']) + assert.equal(aircraftEmergency({ squawk }), null, String(squawk)); + assert.equal(aircraftEmergency(), null); + assert.equal(normalizeSquawk('7800'), null); + assert.equal(normalizeSquawk(7700), '7700'); + assert.equal(normalizeSquawk('770'), null); +}); + +test('the ADS-B emergency field wins, and `none` defers to the squawk', () => { + const minfuel = aircraftEmergency({ squawk: '4521', emergency: 'MinFuel' }); + assert.equal(minfuel.kind, 'minfuel'); + assert.equal(minfuel.severity, 'priority'); + assert.equal(minfuel.source, 'ads-b'); + assert.equal(minfuel.squawk, '4521'); + assert.equal( + aircraftEmergency({ squawk: '7700', emergency: 'none' }).source, + 'squawk', + ); + assert.equal(aircraftEmergency({ emergency: 'downed' }).kind, 'downed'); + assert.equal(aircraftEmergency({ emergency: 'none' }), null); + assert.equal(aircraftEmergency({ emergency: 'reserved' }), null); +}); + +test('priority statuses are reported but are not emergencies', () => { + assert.equal(isEmergency(aircraftEmergency({ squawk: '7700' })), true); + assert.equal( + isEmergency(aircraftEmergency({ emergency: 'lifeguard' })), + false, + ); + assert.equal(isEmergency(aircraftEmergency({ emergency: 'minfuel' })), false); + assert.equal(isEmergency(null), false); +}); diff --git a/src/data/analystEngine.test.mjs b/src/data/analystEngine.test.mjs index 8da85a4d6..3476af7b4 100644 --- a/src/data/analystEngine.test.mjs +++ b/src/data/analystEngine.test.mjs @@ -1,18 +1,89 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { createAnalystEngine, applyScope, haversineKm } from './analystEngine.js'; +import { + createAnalystEngine, + applyScope, + haversineKm, +} from './analystEngine.js'; // Stub world: a square "Texland" region, flights + ships + fires around it. -const TEXLAND = { name: 'Texland', ring: [[-100, 28], [-94, 28], [-94, 33], [-100, 33]] }; +const TEXLAND = { + name: 'Texland', + ring: [ + [-100, 28], + [-94, 28], + [-94, 33], + [-100, 33], + ], +}; const FLIGHTS = [ - { id: 'SWA1', lat: 30.2, lon: -97.7, altitudeM: 11000, speedMps: 240, military: false, onGround: false, routeOrigin: 'AUS', routeDestination: 'LAX' }, - { id: 'RCH01', lat: 31.0, lon: -97.0, altitudeM: 13500, speedMps: 250, military: true, onGround: false, routeOrigin: null, routeDestination: null }, - { id: 'N123', lat: 45.0, lon: -122.0, altitudeM: 2000, speedMps: 80, military: false, onGround: false, routeOrigin: null, routeDestination: null }, - { id: 'GND1', lat: 30.19, lon: -97.66, altitudeM: 150, speedMps: 5, military: false, onGround: true, routeOrigin: null, routeDestination: null }, + { + id: 'SWA1', + lat: 30.2, + lon: -97.7, + altitudeM: 11000, + speedMps: 240, + military: false, + onGround: false, + routeOrigin: 'AUS', + routeDestination: 'LAX', + }, + { + id: 'RCH01', + lat: 31.0, + lon: -97.0, + altitudeM: 13500, + speedMps: 250, + military: true, + onGround: false, + routeOrigin: null, + routeDestination: null, + }, + { + id: 'N123', + lat: 45.0, + lon: -122.0, + altitudeM: 2000, + speedMps: 80, + military: false, + onGround: false, + squawk: '7700', + emergency: true, + emergencyKind: 'general', + routeOrigin: null, + routeDestination: null, + }, + { + id: 'GND1', + lat: 30.19, + lon: -97.66, + altitudeM: 150, + speedMps: 5, + military: false, + onGround: true, + routeOrigin: null, + routeDestination: null, + }, ]; const SHIPS = [ - { id: 'EVERGIVEN', lat: 29.5, lon: -94.9, speedKts: 12, shipType: 'Cargo', destination: 'OAKLAND', navStatus: 'under way' }, - { id: 'SLOWBOAT', lat: 29.6, lon: -95.0, speedKts: 0.2, shipType: 'Tanker', destination: 'HOUSTON', navStatus: 'anchored' }, + { + id: 'EVERGIVEN', + lat: 29.5, + lon: -94.9, + speedKts: 12, + shipType: 'Cargo', + destination: 'OAKLAND', + navStatus: 'under way', + }, + { + id: 'SLOWBOAT', + lat: 29.6, + lon: -95.0, + speedKts: 0.2, + shipType: 'Tanker', + destination: 'HOUSTON', + navStatus: 'anchored', + }, ]; const FIRES = [ { id: 'FIRE-1', lat: 30.5, lon: -98.2, frp: 1500 }, @@ -22,7 +93,10 @@ const FIRES = [ function makeEngine() { return createAnalystEngine({ - getRecords: (key) => ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES }[key] || []), + getRecords: (key) => + ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES })[ + key + ] || [], resolveRegionRing: async (name) => (/texland/i.test(name) ? TEXLAND : null), getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }), }); @@ -31,7 +105,10 @@ function makeEngine() { /** Same world, but Contacts is up with a subject far from the parked camera. */ function makeContactsEngine(subject) { return createAnalystEngine({ - getRecords: (key) => ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES }[key] || []), + getRecords: (key) => + ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES })[ + key + ] || [], resolveRegionRing: async (name) => (/texland/i.test(name) ? TEXLAND : null), // Parked far away, as a high-altitude camera often is. getViewContext: () => ({ lat: 45.0, lon: -122.0, viewRadiusKm: 150 }), @@ -52,7 +129,11 @@ test('analyst: a radius query centers on the active contact, not the parked came assert.equal(r.ok, true); // Austin-area flights, not the Oregon one the camera is parked over. assert.equal(r.count, 3); - assert.equal(r.centeredOn, 'SWA1', 'the answer names the centre it measured from'); + assert.equal( + r.centeredOn, + 'SWA1', + 'the answer names the centre it measured from', + ); assert.ok(r.coverage.scope.includes('@SWA1')); }); @@ -63,7 +144,11 @@ test('analyst: an explicit center still wins over the active contact', async () scope: { kind: 'radius', km: 250, center: { lat: 45.0, lon: -122.0 } }, limit: 50, }); - assert.equal(r.count, 1, 'only the Oregon flight is within 250 km of the given center'); + assert.equal( + r.count, + 1, + 'only the Oregon flight is within 250 km of the given center', + ); assert.equal(r.centeredOn, undefined, 'an explicit center is not relabelled'); }); @@ -73,7 +158,11 @@ test('analyst: with Contacts off, radius still centers on the view', async () => scope: { kind: 'radius', km: 250 }, limit: 50, }); - assert.equal(r.count, 1, 'view-centred behaviour is unchanged outside Contacts'); + assert.equal( + r.count, + 1, + 'view-centred behaviour is unchanged outside Contacts', + ); assert.equal(r.centeredOn, undefined); assert.equal(r.coverage.scope, 'radius:250km'); }); @@ -83,13 +172,25 @@ test('analyst: a subject without usable coordinates cannot lend its name to a ca // number. A subject present but position-less fell back to the camera and // kept the contact's name on the answer, so a camera-centred count read as // contact-centred with nothing in the payload to catch it. - const r = await makeContactsEngine({ lat: null, lon: null, label: 'SWA1' }).query({ + const r = await makeContactsEngine({ + lat: null, + lon: null, + label: 'SWA1', + }).query({ layers: ['flights'], scope: { kind: 'radius', km: 250 }, limit: 50, }); - assert.equal(r.count, 1, 'the count is the camera-centred one it actually measured'); - assert.equal(r.centeredOn, undefined, 'and it must not claim a centre it did not use'); + assert.equal( + r.count, + 1, + 'the count is the camera-centred one it actually measured', + ); + assert.equal( + r.centeredOn, + undefined, + 'and it must not claim a centre it did not use', + ); assert.equal(r.coverage.scope, 'radius:250km'); assert.equal(r.scopeLabel, 'within 250 km'); }); @@ -99,33 +200,47 @@ test('analyst: every scope names itself in words', async () => { // answers look like a contradiction, so each scope carries its own phrasing. const subject = { lat: 30.2, lon: -97.7, label: 'DYNO11' }; const centred = await makeContactsEngine(subject).query({ - layers: ['flights'], scope: { kind: 'radius', km: 250 }, limit: 1, + layers: ['flights'], + scope: { kind: 'radius', km: 250 }, + limit: 1, }); assert.equal(centred.scopeLabel, 'within 250 km of DYNO11'); const plainRadius = await makeContactsEngine(null).query({ - layers: ['flights'], scope: { kind: 'radius', km: 250 }, limit: 1, + layers: ['flights'], + scope: { kind: 'radius', km: 250 }, + limit: 1, }); assert.equal(plainRadius.scopeLabel, 'within 250 km'); const inView = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'view' }, limit: 1, + layers: ['flights'], + scope: { kind: 'view' }, + limit: 1, }); assert.equal(inView.scopeLabel, 'in view'); const region = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'region', name: 'Texland' }, limit: 1, + layers: ['flights'], + scope: { kind: 'region', name: 'Texland' }, + limit: 1, }); assert.equal(region.scopeLabel, 'over Texland'); const anywhere = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'anywhere' }, limit: 1, + layers: ['flights'], + scope: { kind: 'anywhere' }, + limit: 1, }); assert.equal(anywhere.scopeLabel, 'anywhere in the loaded data'); }); test('analyst: count flights over a region', async () => { - const r = await makeEngine().query({ layers: ['flights'], scope: { kind: 'region', name: 'Texland' }, limit: 50 }); + const r = await makeEngine().query({ + layers: ['flights'], + scope: { kind: 'region', name: 'Texland' }, + limit: 50, + }); assert.equal(r.ok, true); assert.equal(r.count, 3, 'Oregon flight excluded'); assert.ok(r.coverage.scope.includes('Texland')); @@ -133,32 +248,65 @@ test('analyst: count flights over a region', async () => { test('analyst: attribute filter — above 40,000 ft (~12,192 m)', async () => { const r = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'anywhere' }, + layers: ['flights'], + scope: { kind: 'anywhere' }, filters: [{ field: 'altitudeM', op: 'gt', value: 12192 }], }); - assert.deepEqual(r.items.map((i) => i.id), ['RCH01']); + assert.deepEqual( + r.items.map((i) => i.id), + ['RCH01'], + ); }); test('analyst: military flag + region compose', async () => { const r = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'region', name: 'Texland' }, + layers: ['flights'], + scope: { kind: 'region', name: 'Texland' }, filters: [{ field: 'military', op: 'eq', value: true }], }); assert.equal(r.count, 1); assert.equal(r.items[0].id, 'RCH01'); }); +test('analyst: aircraft broadcasting an emergency are filterable by flag and code', async () => { + const flagged = await makeEngine().query({ + layers: ['flights'], + scope: { kind: 'anywhere' }, + filters: [{ field: 'emergency', op: 'eq', value: true }], + }); + assert.deepEqual( + flagged.items.map((i) => i.id), + ['N123'], + ); + const coded = await makeEngine().query({ + layers: ['flights'], + scope: { kind: 'anywhere' }, + filters: [{ field: 'squawk', op: 'eq', value: '7700' }], + }); + assert.deepEqual( + coded.items.map((i) => i.id), + ['N123'], + ); +}); + test('analyst: ships headed to Oakland (destination contains)', async () => { const r = await makeEngine().query({ - layers: ['ais-live-vessels'], scope: { kind: 'anywhere' }, + layers: ['ais-live-vessels'], + scope: { kind: 'anywhere' }, filters: [{ field: 'destination', op: 'contains', value: 'oakland' }], }); - assert.deepEqual(r.items.map((i) => i.id), ['EVERGIVEN']); + assert.deepEqual( + r.items.map((i) => i.id), + ['EVERGIVEN'], + ); }); test('analyst: superlative — biggest fire in view radius', async () => { const r = await makeEngine().query({ - layers: ['local-firms'], scope: { kind: 'view' }, sortBy: 'frp', limit: 1, + layers: ['local-firms'], + scope: { kind: 'view' }, + sortBy: 'frp', + limit: 1, }); assert.equal(r.items[0].id, 'FIRE-1', 'BC monster is out of view scope'); assert.equal(r.summary.frpMax, 1500); @@ -166,7 +314,10 @@ test('analyst: superlative — biggest fire in view radius', async () => { test('analyst: nearest sorting attaches distanceKm ascending', async () => { const r = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'view' }, sortBy: 'distance', limit: 3, + layers: ['flights'], + scope: { kind: 'view' }, + sortBy: 'distance', + limit: 3, }); assert.ok(r.items[0].distanceKm <= r.items[1].distanceKm); assert.ok(Number.isFinite(r.items[0].distanceKm)); @@ -266,7 +417,10 @@ test('analyst: failed pre-commit work cannot replace follow-up memory', async () }); test('analyst: unresolved region is an honest failure, not empty success', async () => { - const r = await makeEngine().query({ layers: ['flights'], scope: { kind: 'region', name: 'Atlantis' } }); + const r = await makeEngine().query({ + layers: ['flights'], + scope: { kind: 'region', name: 'Atlantis' }, + }); assert.equal(r.ok, false); assert.match(r.error, /Atlantis/); }); @@ -274,10 +428,17 @@ test('analyst: unresolved region is an honest failure, not empty success', async test('analyst: a region lookup timeout is reported as region-timeout', async () => { const eng = createAnalystEngine({ getRecords: () => FLIGHTS, - resolveRegionRing: async (name) => ({ name, ring: null, error: 'region-timeout' }), + resolveRegionRing: async (name) => ({ + name, + ring: null, + error: 'region-timeout', + }), getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }), }); - const r = await eng.query({ layers: ['flights'], scope: { kind: 'region', name: 'Texas' } }); + const r = await eng.query({ + layers: ['flights'], + scope: { kind: 'region', name: 'Texas' }, + }); assert.equal(r.ok, false); assert.equal(r.code, 'region-timeout'); assert.match(r.error, /Texas/); @@ -319,32 +480,84 @@ test('analyst: region lookup receives and obeys the owning action signal', async test('analyst: route fields queryable from cached enrichment only', async () => { const r = await makeEngine().query({ - layers: ['flights'], scope: { kind: 'anywhere' }, + layers: ['flights'], + scope: { kind: 'anywhere' }, filters: [{ field: 'routeDestination', op: 'eq', value: 'LAX' }], }); - assert.deepEqual(r.items.map((i) => i.id), ['SWA1'], 'null route fields never match'); + assert.deepEqual( + r.items.map((i) => i.id), + ['SWA1'], + 'null route fields never match', + ); }); test('analyst: satellites and local infrastructure are queryable layers', async () => { const SATS = [ - { id: 'ISS (ZARYA)', noradId: '25544', name: 'ISS (ZARYA)', lat: 30.3, lon: -97.7, altitudeM: 410000, satelliteClass: 'STATION · ISS', group: 'stations' }, - { id: 'GPS BIIR-2', noradId: '24876', name: 'GPS BIIR-2', lat: 51.0, lon: 0.0, altitudeM: 20200000, satelliteClass: 'NAV · GPS', group: 'gps-ops' }, + { + id: 'ISS (ZARYA)', + noradId: '25544', + name: 'ISS (ZARYA)', + lat: 30.3, + lon: -97.7, + altitudeM: 410000, + satelliteClass: 'STATION · ISS', + group: 'stations', + }, + { + id: 'GPS BIIR-2', + noradId: '24876', + name: 'GPS BIIR-2', + lat: 51.0, + lon: 0.0, + altitudeM: 20200000, + satelliteClass: 'NAV · GPS', + group: 'gps-ops', + }, ]; const DAMS = [ - { id: 'Austin Dam', name: 'Austin Dam', lat: 30.27, lon: -97.74, operator: 'LCRA', river: 'Colorado', output: '2 MW' }, - { id: 'Far Dam', name: 'Far Dam', lat: 45.0, lon: -122.0, operator: 'USACE', river: 'Columbia', output: '1000 MW' }, + { + id: 'Austin Dam', + name: 'Austin Dam', + lat: 30.27, + lon: -97.74, + operator: 'LCRA', + river: 'Colorado', + output: '2 MW', + }, + { + id: 'Far Dam', + name: 'Far Dam', + lat: 45.0, + lon: -122.0, + operator: 'USACE', + river: 'Columbia', + output: '1000 MW', + }, ]; const DCS = [ - { id: 'AUS-1', name: 'AUS-1', lat: 30.28, lon: -97.75, operator: 'Example Cloud', capacity: '27 MW' }, + { + id: 'AUS-1', + name: 'AUS-1', + lat: 30.28, + lon: -97.75, + operator: 'Example Cloud', + capacity: '27 MW', + }, ]; const eng = createAnalystEngine({ - getRecords: (key) => ({ satellites: SATS, 'local-dams': DAMS, 'local-datacenters': DCS }[key] || []), + getRecords: (key) => + ({ satellites: SATS, 'local-dams': DAMS, 'local-datacenters': DCS })[ + key + ] || [], resolveRegionRing: async (name) => (/texland/i.test(name) ? TEXLAND : null), getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }), }); const sats = await eng.query({ - layers: ['satellites'], scope: { kind: 'view' }, sortBy: 'distance', limit: 2, + layers: ['satellites'], + scope: { kind: 'view' }, + sortBy: 'distance', + limit: 2, }); assert.equal(sats.ok, true); assert.equal(sats.count, 1, 'GPS sat is out of the Austin view radius'); @@ -352,20 +565,28 @@ test('analyst: satellites and local infrastructure are queryable layers', async assert.ok(Number.isFinite(sats.items[0].distanceKm)); const nav = await eng.query({ - layers: ['satellites'], scope: { kind: 'anywhere' }, + layers: ['satellites'], + scope: { kind: 'anywhere' }, filters: [{ field: 'satelliteClass', op: 'contains', value: 'NAV' }], }); - assert.deepEqual(nav.items.map((i) => i.id), ['GPS BIIR-2']); + assert.deepEqual( + nav.items.map((i) => i.id), + ['GPS BIIR-2'], + ); const dams = await eng.query({ - layers: ['local-dams'], scope: { kind: 'view' }, sortBy: 'distance', limit: 5, + layers: ['local-dams'], + scope: { kind: 'view' }, + sortBy: 'distance', + limit: 5, }); assert.equal(dams.count, 1); assert.equal(dams.items[0].id, 'Austin Dam'); assert.equal(dams.items[0].river, 'Colorado'); const dcs = await eng.query({ - layers: ['local-datacenters'], scope: { kind: 'region', name: 'Texland' }, + layers: ['local-datacenters'], + scope: { kind: 'region', name: 'Texland' }, filters: [{ field: 'operator', op: 'contains', value: 'cloud' }], }); assert.equal(dcs.count, 1); @@ -375,7 +596,11 @@ test('analyst: satellites and local infrastructure are queryable layers', async test('helpers: haversine sanity + scope radius', () => { const km = haversineKm(30.2672, -97.7431, 29.7604, -95.3698); // Austin→Houston assert.ok(km > 200 && km < 280, `Austin-Houston ~235km, got ${km}`); - const scoped = applyScope(FLIGHTS, { kind: 'radius' }, { center: { lat: 30.27, lon: -97.74 }, km: 50 }); + const scoped = applyScope( + FLIGHTS, + { kind: 'radius' }, + { center: { lat: 30.27, lon: -97.74 }, km: 50 }, + ); assert.deepEqual(scoped.map((f) => f.id).sort(), ['GND1', 'SWA1']); }); @@ -385,7 +610,11 @@ test('capped cohorts report returned/total/truncated and keep it on follow-up', getRecordCoverage: () => ({ total: 3000, truncated: true }), getViewContext: () => ({ lat: 0, lon: 0, viewRadiusKm: 25 }), }); - const result = await engine.query({ layers: ['local-dams'], scope: { kind: 'anywhere' }, sortBy: 'distance' }); + const result = await engine.query({ + layers: ['local-dams'], + scope: { kind: 'anywhere' }, + sortBy: 'distance', + }); assert.equal(result.count, 1); const layer = result.coverage.layersQueried[0]; assert.deepEqual({ returned: layer.returned, total: layer.total, truncated: layer.truncated }, { returned: 1, total: 3000, truncated: true }); @@ -510,7 +739,11 @@ test('follow-up provenance stays attached to old rows after a feed recovers', as let state = 'stale'; const engine = createAnalystEngine({ getRecords: () => [{ id: 'A', lat: 0, lon: 0 }], - getLayerSnapshot: () => ({ id: 'flights', enabled: true, feedState: state }), + getLayerSnapshot: () => ({ + id: 'flights', + enabled: true, + feedState: state, + }), getViewContext: () => ({ lat: 0, lon: 0, viewRadiusKm: 25 }), }); const first = await engine.query(); @@ -520,7 +753,10 @@ test('follow-up provenance stays attached to old rows after a feed recovers', as assert.equal(followUp.coverage.feedProvenance.overall, 'stale'); engine.reset(); assert.equal(engine.hasMemory(), false); - assert.equal((await engine.query()).coverage.feedProvenance.overall, 'nominal'); + assert.equal( + (await engine.query()).coverage.feedProvenance.overall, + 'nominal', + ); }); test('a large loaded set sorts and summarizes without overflowing the stack', async () => { diff --git a/src/data/flights.test.mjs b/src/data/flights.test.mjs index b09dc7014..740f1afcc 100644 --- a/src/data/flights.test.mjs +++ b/src/data/flights.test.mjs @@ -79,6 +79,9 @@ test('flights analyst record: full record maps every contract field', () => { aircraftClass: 'airliner', originCountry: 'United States', operator: 'Southwest Airlines', + squawk: null, + emergency: false, + emergencyKind: null, routeOrigin: 'AUS', routeDestination: 'LAX', }); diff --git a/src/data/militaryFlights.test.mjs b/src/data/militaryFlights.test.mjs index 597211bb7..cd29c1ebd 100644 --- a/src/data/militaryFlights.test.mjs +++ b/src/data/militaryFlights.test.mjs @@ -71,11 +71,31 @@ test('military analyst record: full record maps every contract field', () => { aircraftClass: 'widebody', originCountry: null, operator: 'United States Air Force', + squawk: null, + emergency: false, + emergencyKind: null, routeOrigin: null, routeDestination: null, }); }); +test('military analyst record: emergency flag is only raised for emergency severity', () => { + const hijack = mapAnalystRecord('ae01ce', { + ...FULL_INFO, + squawk: '7500', + emergency: { kind: 'unlawful', severity: 'emergency', squawk: '7500' }, + }); + assert.equal(hijack.squawk, '7500'); + assert.equal(hijack.emergency, true); + assert.equal(hijack.emergencyKind, 'unlawful'); + const lifeguard = mapAnalystRecord('ae01ce', { + ...FULL_INFO, + emergency: { kind: 'lifeguard', severity: 'priority', squawk: null }, + }); + assert.equal(lifeguard.emergency, false); + assert.equal(lifeguard.emergencyKind, 'lifeguard'); +}); + test('military analyst record: military is ALWAYS true, routes/country always null', () => { const r = mapAnalystRecord('ae01ce', undefined); assert.equal(r.military, true); diff --git a/src/layers/flights/policy.js b/src/layers/flights/policy.js index f8358c98f..80d77dddd 100644 --- a/src/layers/flights/policy.js +++ b/src/layers/flights/policy.js @@ -7,6 +7,7 @@ export { ERROR_BACKOFF_INTERVAL, } from './recordPolicy.js'; import * as Cesium from 'cesium'; +import { EMERGENCY_TINT_CSS } from '../../data/aircraftEmergency.js'; export const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true; @@ -14,6 +15,11 @@ export const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true; export const MIL_TINT = Cesium.Color.fromCssColorString('#FFB800'); +/** Red tint for aircraft broadcasting an emergency (7500/7600/7700 or ADS-B status). */ + +export const EMERGENCY_TINT = + Cesium.Color.fromCssColorString(EMERGENCY_TINT_CSS); + // --- Ground traffic (owner reversal 2026-07-03: "absolutely we should see planes // taxiing and landing") ----------------------------------------------------------- // Present-but-grounded planes are RENDERED instead of being skipped: same class diff --git a/src/layers/flights/queries.js b/src/layers/flights/queries.js index 5b64b2247..89ea44214 100644 --- a/src/layers/flights/queries.js +++ b/src/layers/flights/queries.js @@ -167,6 +167,9 @@ export function createQueries({ flightState.records.missingPolls.get(icao24) || flightState.feed._backoff, ), + // This poll's transponder broadcast (not sticky; see records.js). + squawk: info?.squawk ?? null, + emergency: info?.emergency ? { ...info.emergency } : null, airline: info?.airline ?? null, // CLASS label follows the TR-3B conversion so every downstream card // (cockpit, Contacts, analyst) agrees with the triangle on screen. @@ -219,7 +222,9 @@ export function createQueries({ * lon: number|null, altitudeM: number|null, speedMps: number|null, * heading: number|null, verticalRateMps: number|null, onGround: boolean, * military: boolean, aircraftClass: string|null, originCountry: string|null, - * operator: string|null, routeOrigin: string|null, routeDestination: string|null}} + * operator: string|null, squawk: string|null, emergency: boolean, + * emergencyKind: string|null, routeOrigin: string|null, + * routeDestination: string|null}} */ function mapAnalystRecord( @@ -253,6 +258,12 @@ export function createQueries({ aircraftClass: tr3bAircraftClass(icao24, text(info?.klass)), originCountry: text(info?.originCountry), operator: text(info?.airline), + squawk: text(info?.squawk), + // True while this poll broadcast an emergency (7500/7600/7700 or the + // ADS-B status); priority statuses (lifeguard, minfuel) report the kind + // without the flag. + emergency: info?.emergency?.severity === 'emergency', + emergencyKind: text(info?.emergency?.kind), routeOrigin: routeOk ? text(info?.route?.origin?.code) : null, routeDestination: routeOk ? text(info?.route?.destination?.code) : null, }; diff --git a/src/layers/flights/records.js b/src/layers/flights/records.js index 43809f516..4b665d773 100644 --- a/src/layers/flights/records.js +++ b/src/layers/flights/records.js @@ -4,6 +4,7 @@ import { pickRenderAltitudeM, } from '../../data/renderAltitude.js'; import { classifyAircraft } from '../../data/aircraftClass.js'; +import { aircraftEmergency } from '../../data/aircraftEmergency.js'; import { approxDistanceKm, GROUND_FLOOR_CLAMP_RADIUS_KM, @@ -226,6 +227,10 @@ export class FlightRecords { // like callsign so a transient blank row doesn't blank the field. originCountry: stickyText(origin_country, prevMeta?.originCountry) || null, + // Per poll, NOT sticky: an emergency code clears when the crew resets + // it, so a blank squawk must not hold a stale 7700 on screen. + squawk: observation.squawk ?? null, + emergency: aircraftEmergency(observation), // OpenSky distinguishes the last position epoch from the last // transponder message. The fleet coast horizon uses this actual // contact time so a temporarily old position does not hard-freeze diff --git a/src/layers/flights/records.test.mjs b/src/layers/flights/records.test.mjs index d001c7811..fff25b8b9 100644 --- a/src/layers/flights/records.test.mjs +++ b/src/layers/flights/records.test.mjs @@ -106,3 +106,13 @@ test('ground transitions preserve last render height while a surface cell is col assert.equal(next.meta.renderAltitudeM, 1040); assert.equal(next.meta.wasAirborne, true); }); + +test('civil emergency status follows the latest poll instead of sticking', () => { + const store = records(); + const first = store.receive(observation({ squawk: '7700' }), view()); + assert.equal(first.meta.squawk, '7700'); + assert.equal(first.meta.emergency.kind, 'general'); + const next = store.receive(observation({ squawk: '2000' }), view()); + assert.equal(next.meta.squawk, '2000'); + assert.equal(next.meta.emergency, null); +}); diff --git a/src/layers/flights/rendering.js b/src/layers/flights/rendering.js index 44e1b02c3..e84e05e01 100644 --- a/src/layers/flights/rendering.js +++ b/src/layers/flights/rendering.js @@ -3,6 +3,7 @@ import { cyberSonarBaseAlpha } from '../../cyberSonar.js'; import { selectModelEligible } from '../../data/modelEligibility.js'; import { civilAircraftModelSpec } from './modelSpec.js'; import { CLASS_SCALE_2D } from '../../data/aircraftClass.js'; +import { isEmergency } from '../../data/aircraftEmergency.js'; import { cockpitContactDotImage } from '../../data/cockpitContactDot.js'; import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js'; import { @@ -13,6 +14,7 @@ import { import { limitCourseStep, courseSlewCapDps } from '../../data/motionModel.js'; import { MIL_TINT, + EMERGENCY_TINT, GROUND_SCALE, COCKPIT_CONTACT_SIZE_PX, COCKPIT_CIVILIAN_COLOR, @@ -54,13 +56,28 @@ export function createRendering({ const { applyAircraftBillboardTreatment, applyAircraftModelTreatment } = services.recession; - /** Fleet (untracked) billboard tint: amber for known-military, white otherwise. + /** True while this contact's latest poll broadcast an emergency code. */ + + function _isEmergency(icao24) { + return isEmergency(flightState.records.data.get(icao24)?.emergency); + } + + /** Fleet (untracked) billboard tint: red while broadcasting an emergency, + * amber for known-military, white otherwise. * Ground traffic gets NO special tint (owner verdict 2026-07-03 field test). */ function _fleetBillboardColor(icao24) { + if (_isEmergency(icao24)) return EMERGENCY_TINT; return isMilitaryIcao(icao24) ? MIL_TINT : Cesium.Color.WHITE; } + /** Cockpit far-contact dot tint: the emergency red outranks the category tint. */ + + function _cockpitContactColor(icao24) { + if (_isEmergency(icao24)) return EMERGENCY_TINT; + return isMilitaryIcao(icao24) ? MIL_TINT : COCKPIT_CIVILIAN_COLOR; + } + /** Fleet billboard scale: per-class scale, ×GROUND_SCALE while grounded. */ function _fleetBillboardScale(icao24, klass) { @@ -129,9 +146,7 @@ export function createRendering({ bb.height = COCKPIT_CONTACT_SIZE_PX; bb.scale = limbScale; bb.scaleByDistance = _cockpitBillboardScaleByDistance(); - bb.color = ( - isMilitaryIcao(icao24) ? MIL_TINT : COCKPIT_CIVILIAN_COLOR - ).withAlpha(freshnessAlpha); + bb.color = _cockpitContactColor(icao24).withAlpha(freshnessAlpha); bb.rotation = 0; return; } @@ -162,7 +177,7 @@ export function createRendering({ function _modelColor(icao24) { if (icao24 === flightState._trackedIcao) return Cesium.Color.CYAN; - return isMilitaryIcao(icao24) ? MIL_TINT : Cesium.Color.WHITE; + return _fleetBillboardColor(icao24); } /** The FLEET's 3D-model regime: models3d enabled AND the camera zoomed in past the altitude @@ -1009,9 +1024,7 @@ export function createRendering({ flightState._cockpitNearContacts.has(icao24); const layerBaseColor = flightState._cockpitContactMode && !isCockpitNear - ? isMilitaryIcao(icao24) - ? MIL_TINT - : COCKPIT_CIVILIAN_COLOR + ? _cockpitContactColor(icao24) : _fleetBillboardColor(icao24); const baseColor = layerBaseColor; const treatment = applyAircraftBillboardTreatment({ diff --git a/src/layers/military/policy.js b/src/layers/military/policy.js index 36d5ebde4..f375e5419 100644 --- a/src/layers/military/policy.js +++ b/src/layers/military/policy.js @@ -6,6 +6,7 @@ export { ERROR_BACKOFF_INTERVAL, } from './recordPolicy.js'; import * as Cesium from 'cesium'; +import { EMERGENCY_TINT_CSS } from '../../data/aircraftEmergency.js'; /** * @module militaryFlights @@ -40,6 +41,11 @@ export const MIL_ICON_COLOR = Cesium.Color.fromCssColorString('#FFB800'); export const TRACKED_ICON_COLOR = Cesium.Color.fromCssColorString('#FFD166'); +/** @constant {Cesium.Color} Red tint for aircraft broadcasting an emergency (mirror of flights) */ + +export const EMERGENCY_ICON_COLOR = + Cesium.Color.fromCssColorString(EMERGENCY_TINT_CSS); + // --- Ground traffic (owner reversal 2026-07-03; mirror of flights.js) --------------- // adsb.lol/readsb flags ground traffic with alt_baro === "ground" (no separate // boolean). Such aircraft are RENDERED instead of floating at the 3 km altitude diff --git a/src/layers/military/queries.js b/src/layers/military/queries.js index 421e9804d..eab06458c 100644 --- a/src/layers/military/queries.js +++ b/src/layers/military/queries.js @@ -133,6 +133,9 @@ export function createQueries({ flightState.records.missingPolls.get(icao24) || flightState.feed._backoff, ), + // This poll's transponder broadcast (not sticky; see records.js). + squawk: info?.squawk ?? null, + emergency: info?.emergency ? { ...info.emergency } : null, }; } @@ -156,7 +159,8 @@ export function createQueries({ * lon: number|null, altitudeM: number|null, speedMps: number|null, * heading: number|null, verticalRateMps: number|null, onGround: boolean, * military: boolean, aircraftClass: string|null, originCountry: null, - * operator: string|null, routeOrigin: null, routeDestination: null}} + * operator: string|null, squawk: string|null, emergency: boolean, + * emergencyKind: string|null, routeOrigin: null, routeDestination: null}} */ function mapAnalystRecord(icao24, info) { @@ -187,6 +191,12 @@ export function createQueries({ aircraftClass: tr3bAircraftClass(icao24, text(info?.klass)), originCountry: null, operator: text(info?.operator), + squawk: text(info?.squawk), + // True while this poll broadcast an emergency (7500/7600/7700 or the + // ADS-B status); priority statuses (lifeguard, minfuel) report the kind + // without the flag. + emergency: info?.emergency?.severity === 'emergency', + emergencyKind: text(info?.emergency?.kind), routeOrigin: null, routeDestination: null, }; diff --git a/src/layers/military/records.js b/src/layers/military/records.js index cbc62be79..9cd6754e6 100644 --- a/src/layers/military/records.js +++ b/src/layers/military/records.js @@ -1,6 +1,7 @@ import { pickRenderAltitudeM } from '../../data/renderAltitude.js'; import { stickyText, stickyNumber } from '../../data/aircraftMeta.js'; import { classifyAircraft } from '../../data/aircraftClass.js'; +import { aircraftEmergency } from '../../data/aircraftEmergency.js'; import { GROUND_FLOOR_WARM_MAX_ALT_M, LANDED_MISSING_POLL_LIMIT, @@ -161,6 +162,9 @@ export class MilitaryFlightRecords { null, ), turnRateDps: prevMeta?.turnRateDps || 0, + // Per poll, NOT sticky (mirror of flights): a cleared code clears here. + squawk: aircraft.squawk ?? null, + emergency: aircraftEmergency(aircraft), onGround, // Round 7: sticky airborne history (see _likelyLanded). wasAirborne: prevMeta?.wasAirborne === true || !onGround, diff --git a/src/layers/military/records.test.mjs b/src/layers/military/records.test.mjs index df42760ce..5054fb28a 100644 --- a/src/layers/military/records.test.mjs +++ b/src/layers/military/records.test.mjs @@ -98,3 +98,19 @@ test('military records keep bounded partial retention and release metadata only 'remove', ); }); + +test('military emergency status follows the latest poll instead of sticking', () => { + const records = store(); + const first = records.receive( + observation({ squawk: '1234', emergency: 'minfuel' }), + context(), + ); + assert.equal(first.meta.squawk, '1234'); + assert.equal(first.meta.emergency.kind, 'minfuel'); + assert.equal(first.meta.emergency.severity, 'priority'); + const next = records.receive( + observation({ squawk: '1234', emergency: 'none' }), + context(), + ); + assert.equal(next.meta.emergency, null); +}); diff --git a/src/layers/military/rendering.js b/src/layers/military/rendering.js index 9ebd03180..48265a777 100644 --- a/src/layers/military/rendering.js +++ b/src/layers/military/rendering.js @@ -18,6 +18,7 @@ import { screenProjectedRotation, } from '../../data/iconOrientation.js'; import { limitCourseStep, courseSlewCapDps } from '../../data/motionModel.js'; +import { isEmergency } from '../../data/aircraftEmergency.js'; import { PLANE_MODEL_SCALE, PLANE_MODEL_URL, @@ -33,6 +34,7 @@ import { COCKPIT_CONTACT_SIZE_PX, MIL_ICON_COLOR, TRACKED_ICON_COLOR, + EMERGENCY_ICON_COLOR, MODEL_ALT_CEIL_M, MODEL_MAX_ALL, MODEL_MAX, @@ -159,7 +161,7 @@ export function createRendering({ bb.height = COCKPIT_CONTACT_SIZE_PX; bb.scale = limbScale; bb.scaleByDistance = _cockpitBillboardScaleByDistance(); - bb.color = MIL_ICON_COLOR.withAlpha(freshnessAlpha); + bb.color = _fleetBillboardColor(icao24).withAlpha(freshnessAlpha); bb.rotation = 0; return; } @@ -173,7 +175,7 @@ export function createRendering({ bb.height = icao24 === flightState._trackedIcao ? 24 : 20; bb.scale = _militaryBillboardScale(icao24) * limbScale; bb.scaleByDistance = _normalBillboardScaleByDistance(); - bb.color = MIL_ICON_COLOR.withAlpha(cyberSonarBaseAlpha(bb)); + bb.color = _fleetBillboardColor(icao24).withAlpha(cyberSonarBaseAlpha(bb)); } /** Sprite kind for one contact's billboard. Identity for every aircraft except @@ -189,7 +191,16 @@ export function createRendering({ function _modelColor(icao24) { if (icao24 === flightState._trackedIcao) return TRACKED_ICON_COLOR; - return MIL_ICON_COLOR; + return _fleetBillboardColor(icao24); + } + + /** Fleet (untracked) tint: red while the latest poll broadcast an emergency, + * amber otherwise. Cockpit far-contact dots use it too. */ + + function _fleetBillboardColor(icao24) { + return isEmergency(flightState.records.data.get(icao24)?.emergency) + ? EMERGENCY_ICON_COLOR + : MIL_ICON_COLOR; } /** The FLEET's 3D-model regime: models3d enabled AND the camera zoomed in past the altitude @@ -965,7 +976,7 @@ export function createRendering({ const isCockpitNear = flightState._cockpitContactMode && flightState._cockpitNearContacts.has(icao24); - const baseColor = MIL_ICON_COLOR; + const baseColor = _fleetBillboardColor(icao24); const treatment = applyAircraftBillboardTreatment({ billboard: bb, baseScale: @@ -1082,6 +1093,7 @@ export function createRendering({ _applyFleetBillboardPresentation, _iconKind, _modelColor, + _fleetBillboardColor, _modelRegimeActive, _modelCap, _modelAddDistM, diff --git a/src/sources/live/aircraft.js b/src/sources/live/aircraft.js index 9534a5c3c..977801aa7 100644 --- a/src/sources/live/aircraft.js +++ b/src/sources/live/aircraft.js @@ -30,6 +30,9 @@ export function normalizeOpenSkyAircraft(row) { typeCode: null, registration: null, operator: null, + squawk: cleanText(row[14]) || null, + // State vectors carry no ADS-B emergency field; the squawk stands alone. + emergency: null, }; } @@ -68,6 +71,8 @@ export function normalizeReadsbAircraft(row, snapshotTimeMs) { typeCode: cleanText(row.t), registration: cleanText(row.r), operator: cleanText(row.ownOp), + squawk: cleanText(row.squawk) || null, + emergency: cleanText(row.emergency).toLowerCase() || null, }; } diff --git a/src/sources/live/contract.test.mjs b/src/sources/live/contract.test.mjs index 391fe1e72..36fa9b95b 100644 --- a/src/sources/live/contract.test.mjs +++ b/src/sources/live/contract.test.mjs @@ -313,3 +313,23 @@ test('identity lookup validates its response and honors body-parse cancellation' name: 'AbortError', }); }); + +test('transponder squawk and ADS-B emergency status are carried, blanks stay null', () => { + const squawking = [...aircraft, '7700']; + const civil = openSkySnapshot({ states: [squawking] }, { now }).records[0]; + assert.equal(civil.squawk, '7700'); + assert.equal(civil.emergency, null); + assert.equal(openSkySnapshot({ states: [aircraft] }).records[0].squawk, null); + const mil = normalizeReadsbAircraft( + { hex: 'abc123', lat: 30, lon: -97, squawk: '7600', emergency: 'NORDO' }, + now, + ); + assert.equal(mil.squawk, '7600'); + assert.equal(mil.emergency, 'nordo'); + const quiet = normalizeReadsbAircraft( + { hex: 'abc123', lat: 30, lon: -97 }, + now, + ); + assert.equal(quiet.squawk, null); + assert.equal(quiet.emergency, null); +}); diff --git a/src/ui/cockpitInstruments.js b/src/ui/cockpitInstruments.js index e4814517d..437aa9b26 100644 --- a/src/ui/cockpitInstruments.js +++ b/src/ui/cockpitInstruments.js @@ -12,6 +12,7 @@ import { compassDivisions, formatAltitudeRulerTick, formatCompassDivision, + formatCockpitEmergency, formatSpeedRulerTick, normalizeHeading, relativeBearing, @@ -133,7 +134,15 @@ export function updateHud( : info.stale ? 'STALE FEED' : 'LIVE TRACK'; - this.aircraftMeta.textContent = `${info.layerId === 'military' ? 'MILITARY' : 'COMMERCIAL'} · ${feedState} · COURSE ALIGNED`; + const fleet = info.layerId === 'military' ? 'MILITARY' : 'COMMERCIAL'; + const emergency = formatCockpitEmergency(info.emergency); + this.aircraftMeta.textContent = emergency + ? `${fleet} · ${emergency} · ${feedState}` + : `${fleet} · ${feedState} · COURSE ALIGNED`; + this.aircraftMeta.classList.toggle( + 'emergency', + info.emergency?.severity === 'emergency', + ); } this.updateRoute(info); if ( diff --git a/src/ui/styles/cockpit.css b/src/ui/styles/cockpit.css index 8436c3a41..df4e599e4 100644 --- a/src/ui/styles/cockpit.css +++ b/src/ui/styles/cockpit.css @@ -496,6 +496,9 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) font-size: 7px; letter-spacing: 0.16em; } +.cockpit-aircraft-meta.emergency { + color: #ff4444; +} .cockpit-mode-chip { margin-top: 2px; padding: 7px 11px; diff --git a/src/ui/styles/cyber.css b/src/ui/styles/cyber.css index 5a25bfec8..79ce004e1 100644 --- a/src/ui/styles/cyber.css +++ b/src/ui/styles/cyber.css @@ -2270,6 +2270,12 @@ color: rgba(205, 225, 226, 0.48); } +:root[data-ui-theme='cyber'] + body.cockpit-mode + .cockpit-aircraft-meta.emergency { + color: #ff4444; +} + :root[data-ui-theme='cyber'] body.cockpit-mode .cockpit-vision-controls { margin-top: 0; border-color: rgba(105, 203, 209, 0.38); From 15b13e947bbb684ef3359e74fc1219aec44f5f35 Mon Sep 17 00:00:00 2001 From: Leo Pechnicki Date: Tue, 29 Sep 2026 19:47:19 +0200 Subject: [PATCH 2/5] =?UTF-8?q?fix(flights):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20quotable=20squawk=20only,=20all=20military=20tint?= =?UTF-8?q?=20sites,=20no=20test=20churn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - aircraftEmergency: `source` is 'squawk' only when the Mode A code itself names the status; the Cockpit HUD no longer quotes an unrelated squawk (e.g. SQUAWK 1200 on an ADS-B `downed` status). - military: new billboards and the untrack restore use the fleet tint, so an emergency stays red instead of snapping back to amber; tint helpers take the record already in hand instead of a second lookup. - cockpit/cyber CSS: emergency colour via a custom property / theme token, meta line bolded so the status does not rely on colour alone in the HUD. - tests: revert Prettier churn in unscoped test files; only semantic edits. - docs/CURRENT-STATE.md: transponder emergency highlighting section. - qa-emergency-squawk: (iv) RELEASE check; new files added to format scope. Co-Authored-By: Claude --- docs/CURRENT-STATE.md | 20 + scripts/format-scope.json | 3 + scripts/qa-emergency-squawk.mjs | 27 + src/cockpitMath.js | 12 +- src/cockpitMath.test.mjs | 158 +--- src/data/aircraftEmergency.js | 6 +- src/data/aircraftEmergency.test.mjs | 9 + src/data/analystEngine.test.mjs | 348 ++----- src/data/flights.test.mjs | 1115 +++++------------------ src/data/militaryFlights.test.mjs | 391 ++------ src/layers/flights/rendering.js | 16 +- src/layers/military/rendering.js | 11 +- src/layers/military/snapshotRenderer.js | 11 +- src/layers/military/tracking.js | 9 +- src/ui/styles/cockpit.css | 5 +- src/ui/styles/cyber.css | 2 +- 16 files changed, 543 insertions(+), 1600 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index a9aeeddbc..dea8a5bec 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -814,6 +814,26 @@ use the supplied asset resolver, including the preload path. Enrichment receives an abort signal and cannot update a later lifecycle after destruction. Existing camera, terrain floor, trail, selection and measured model-size policies remain. +## Transponder emergency highlighting + +`src/data/aircraftEmergency.js` derives one status per poll from the Mode A +squawk (7500 unlawful interference, 7600 radio failure, 7700 general emergency; +ICAO-standard worldwide) and, where the feed carries it, the readsb ADS-B +`emergency` field (adsb.lol; OpenSky state vectors carry the squawk only). An +explicit ADS-B status wins over the squawk. `general`, `unlawful`, `nordo` and +`downed` are emergencies; `minfuel` and `lifeguard` are priorities. + +Both aircraft layers keep `squawk` and `emergency` on each record and replace +them on every poll, so the status is never sticky. Civil and military fleet +billboards and Cockpit far-contact dots are tinted `EMERGENCY_TINT_CSS` red +only for emergencies; priorities keep the normal tint. The military layer uses +the same tint when a billboard is first created and when a tracked contact is +released. While a broadcasting aircraft is tracked, the Cockpit aircraft-meta +line reads `SQUAWK 7700 · GENERAL EMERGENCY` when the code names the status, +else `ADS-B EMERGENCY · …` or `ADS-B PRIORITY · …`, in red. Analyst records and +the voice query fields expose `squawk`, `emergency` (flag) and `emergencyKind`. +`scripts/qa-emergency-squawk.mjs` proves the tint, HUD line and clearing. + ## Browser live-source observations Flights, Military Flights and AIS Vessels obtain snapshots and optional history diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 4973bd484..ea1531793 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -18,6 +18,9 @@ "src/tooling/panelStorageDocs.test.mjs", "src/tooling/packageBoundaries.test.mjs", "src/data/infrastructure.js", + "src/data/aircraftEmergency.js", + "src/data/aircraftEmergency.test.mjs", + "scripts/qa-emergency-squawk.mjs", "src/data/infrastructure.test.mjs", "src/data/localGeojsonCore.js", "src/data/localGeojsonLod.js", diff --git a/scripts/qa-emergency-squawk.mjs b/scripts/qa-emergency-squawk.mjs index bbcc98c22..a1625ef6e 100644 --- a/scripts/qa-emergency-squawk.mjs +++ b/scripts/qa-emergency-squawk.mjs @@ -14,6 +14,8 @@ * the broadcast code on the aircraft-meta line, in red. * (iii) CLEAR — the next poll drops the code: the HUD line returns to its * normal wording (the status is not sticky). + * (iv) RELEASE — the code returns and tracking stops: the restored fleet + * billboard is red, not the plain military amber. * * Screenshots saved to qa-shots/emergency-*.png (gitignored). * @@ -410,6 +412,31 @@ async function main() { JSON.stringify(after), ); await shoot(page, 'cockpit-cleared'); + + // ── (iv) RELEASE ────────────────────────────────────────────────────── + console.log('\n(iv) RELEASE — the code returns, then tracking stops...'); + await page.evaluate(async () => { + window.__squawkQaPhase = 'emergency'; + const dm = window.__godsEyeView.dataManager; + await dm.refreshLayer('military'); + dm.layers.get('military').module.stopTracking({ origin: 'user' }); + }); + const releaseProbe = `(${billboardColors})(["ae7700"])`; + const released = await page + .waitForFunction(`Boolean((${releaseProbe}).ae7700)`, { + timeout: 20000, + polling: 250, + }) + .then( + () => true, + () => false, + ); + const releasedColor = (await page.evaluate(releaseProbe)).ae7700; + record( + 'RELEASE: untracked emergency billboard is red', + released && isRed(releasedColor), + JSON.stringify(releasedColor ?? null), + ); record( 'no uncaught browser errors', errors.length === 0, diff --git a/src/cockpitMath.js b/src/cockpitMath.js index 4c9666ee9..74e6d1451 100644 --- a/src/cockpitMath.js +++ b/src/cockpitMath.js @@ -85,8 +85,11 @@ export function cockpitAltitudeDisplayFt(altitudeM, onGround) { /** * HUD wording for a tracked aircraft's transponder emergency, or null. - * States the broadcast code, never a conclusion about the flight. - * @param {{label?: string, squawk?: string|null}|null|undefined} emergency + * States what was broadcast, never a conclusion about the flight: the Mode A + * code when it names the status, else the ADS-B field it came from (an + * unrelated squawk is dropped). + * @param {{label?: string, squawk?: string|null, source?: string, + * severity?: string}|null|undefined} emergency * @returns {string|null} */ export function formatCockpitEmergency(emergency) { @@ -94,7 +97,10 @@ export function formatCockpitEmergency(emergency) { .trim() .toUpperCase(); if (!label) return null; - return emergency.squawk ? `SQUAWK ${emergency.squawk} · ${label}` : label; + if (emergency.source === 'squawk' && emergency.squawk) + return `SQUAWK ${emergency.squawk} · ${label}`; + const field = emergency.severity === 'priority' ? 'PRIORITY' : 'EMERGENCY'; + return `ADS-B ${field} · ${label}`; } /** Format the cockpit Context scope without overstating installation coverage. */ diff --git a/src/cockpitMath.test.mjs b/src/cockpitMath.test.mjs index 8abbf1c3e..2189ca35e 100644 --- a/src/cockpitMath.test.mjs +++ b/src/cockpitMath.test.mjs @@ -10,10 +10,10 @@ import { cockpitGroundSafeHeight, cockpitSurfaceWaitExpired, cockpitUiUpdateDue, - formatCockpitEmergency, compassDivisions, formatAltitudeRulerTick, formatCockpitContextScope, + formatCockpitEmergency, formatCompassDivision, formatSpeedRulerTick, normalizeHeading, @@ -42,11 +42,7 @@ test('cockpit anchor correction cannot turn a forward step into a reversal', () const speedMps = 200; const dtSec = 0.1; const forwardStepM = speedMps * dtSec; - const backwardCorrectionM = cockpitAnchorCorrectionStep( - 1000, - speedMps, - dtSec, - ); + const backwardCorrectionM = cockpitAnchorCorrectionStep(1000, speedMps, dtSec); assert.ok(backwardCorrectionM > 0); assert.ok(backwardCorrectionM < forwardStepM); }); @@ -82,7 +78,10 @@ test('cockpit Context scope distinguishes radius-complete feeds from viewport in }); test('cockpit Context scope preserves zero and replaces missing values intentionally', () => { - assert.equal(formatCockpitContextScope('', 0), '— · 0 KM AIR/SEA WINDOW'); + assert.equal( + formatCockpitContextScope('', 0), + '— · 0 KM AIR/SEA WINDOW', + ); assert.equal( formatCockpitContextScope(undefined, Number.NaN, {}), '— · — KM AIR/SEA WINDOW', @@ -170,29 +169,26 @@ test('relative bearing stays on the shortest signed arc', () => { }); test('HUD rail layout centers between intersecting upper and lower obstacles', () => { - assert.deepEqual( - resolveHudRailLayout({ - viewportHeight: 1000, - panelHeight: 200, - laneLeft: 50, - laneRight: 410, - baseTop: 280, - baseBottom: 940, - gap: 10, - obstacles: [ - { left: 40, right: 240, top: 320, bottom: 460 }, - { left: 300, right: 1000, top: 780, bottom: 930 }, - { left: 900, right: 1100, top: 300, bottom: 700 }, - ], - }), - { - top: 520, - maxHeight: 300, - safeTop: 470, - safeBottom: 770, - constrained: false, - }, - ); + assert.deepEqual(resolveHudRailLayout({ + viewportHeight: 1000, + panelHeight: 200, + laneLeft: 50, + laneRight: 410, + baseTop: 280, + baseBottom: 940, + gap: 10, + obstacles: [ + { left: 40, right: 240, top: 320, bottom: 460 }, + { left: 300, right: 1000, top: 780, bottom: 930 }, + { left: 900, right: 1100, top: 300, bottom: 700 }, + ], + }), { + top: 520, + maxHeight: 300, + safeTop: 470, + safeBottom: 770, + constrained: false, + }); }); test('HUD rail layout constrains an oversized panel to the safe corridor', () => { @@ -246,10 +242,7 @@ function contextSnapshot(subject, extra = {}) { } test('Contact panel hides only when there is no snapshot at all', () => { - const readout = resolveCockpitContextReadout({ - snapshot: null, - info: TRACKED_INFO, - }); + const readout = resolveCockpitContextReadout({ snapshot: null, info: TRACKED_INFO }); assert.equal(readout.visible, false); assert.equal(readout.mode, 'standby'); assert.equal( @@ -265,22 +258,14 @@ test('Contact panel survives NEXT onto a vessel or installation subject', () => // camera: "click next... whole left panel disappears"). for (const subject of [ { layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' }, - { - layerId: 'military-installations', - id: 'fort-hood', - label: 'FORT CAVAZOS', - }, + { layerId: 'military-installations', id: 'fort-hood', label: 'FORT CAVAZOS' }, { layerId: 'military', id: 'ae01ce', label: 'RCH451' }, ]) { const readout = resolveCockpitContextReadout({ snapshot: contextSnapshot(subject), info: TRACKED_INFO, }); - assert.equal( - readout.visible, - true, - `${subject.layerId} subject must keep the panel up`, - ); + assert.equal(readout.visible, true, `${subject.layerId} subject must keep the panel up`); assert.equal(readout.mode, 'foreign'); assert.equal(readout.subjectMatchesTracked, false); } @@ -288,11 +273,7 @@ test('Contact panel survives NEXT onto a vessel or installation subject', () => test('a foreign subject dashes the aircraft-relative fields and keeps the rest live', () => { const readout = resolveCockpitContextReadout({ - snapshot: contextSnapshot({ - layerId: 'ais-live-vessels', - id: '353136000', - label: 'MAERSK DETROIT', - }), + snapshot: contextSnapshot({ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' }), info: TRACKED_INFO, }); assert.equal(readout.visible, true); @@ -301,20 +282,12 @@ test('a foreign subject dashes the aircraft-relative fields and keeps the rest l // frame while every other row value is measured from the subject; rendering // both live would present one mixed-frame reading as a single measurement. assert.equal(readout.aircraftRelative, false); - assert.equal( - readout.contactLost, - false, - 'subject-frame values still refresh', - ); + assert.equal(readout.contactLost, false, 'subject-frame values still refresh'); }); test('the tracked aircraft as subject keeps every field in its own frame', () => { const readout = resolveCockpitContextReadout({ - snapshot: contextSnapshot({ - layerId: 'flights', - id: 'aaa077', - label: 'SWA1234', - }), + snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077', label: 'SWA1234' }), info: TRACKED_INFO, }); assert.equal(readout.visible, true); @@ -333,11 +306,7 @@ test('a fast-culled subject keeps the panel up as CONTACT LOST instead of collap snapshot: contextSnapshot(subject, { subjectPresent: false }), info: TRACKED_INFO, }); - assert.equal( - readout.visible, - true, - 'a lost contact must not take the panel down', - ); + assert.equal(readout.visible, true, 'a lost contact must not take the panel down'); assert.equal(readout.mode, 'lost'); assert.equal(readout.contactLost, true, 'last-known values stay on screen'); } @@ -345,10 +314,7 @@ test('a fast-culled subject keeps the panel up as CONTACT LOST instead of collap test('an explicitly present subject is never reported lost', () => { const present = resolveCockpitContextReadout({ - snapshot: contextSnapshot( - { layerId: 'flights', id: 'aaa077' }, - { subjectPresent: true }, - ), + snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077' }, { subjectPresent: true }), info: TRACKED_INFO, }); assert.equal(present.contactLost, false); @@ -367,36 +333,20 @@ test('the tracked flight layer is resolved by normalized tracked identity', () = // Both layers describe a tracked aircraft during a cross-layer handoff; // civilian-first precedence would hand the cockpit the wrong aircraft. assert.equal( - resolveTrackedAircraftInfo({ - civilian, - military, - trackedId: 'military:ae01ce', - }).layerId, + resolveTrackedAircraftInfo({ civilian, military, trackedId: 'military:ae01ce' }).layerId, 'military', ); assert.equal( - resolveTrackedAircraftInfo({ - civilian, - military, - trackedId: 'military:ae01ce', - }).icao24, + resolveTrackedAircraftInfo({ civilian, military, trackedId: 'military:ae01ce' }).icao24, 'ae01ce', ); assert.equal( - resolveTrackedAircraftInfo({ - civilian, - military, - trackedId: 'flights:aaa077', - }).layerId, + resolveTrackedAircraftInfo({ civilian, military, trackedId: 'flights:aaa077' }).layerId, 'flights', ); // Case-insensitive: layers stamp lowercase hex, callers may not. assert.equal( - resolveTrackedAircraftInfo({ - civilian, - military, - trackedId: 'MILITARY:AE01CE', - }).layerId, + resolveTrackedAircraftInfo({ civilian, military, trackedId: 'MILITARY:AE01CE' }).layerId, 'military', ); }); @@ -406,37 +356,29 @@ test('tracked-identity resolution falls back to layer precedence', () => { const military = { icao24: 'ae01ce' }; // No stamped identity (or one that matches neither layer) keeps the historic // civilian-first order so tracking paths without gevTrackedId still work. + assert.equal(resolveTrackedAircraftInfo({ civilian, military }).layerId, 'flights'); assert.equal( - resolveTrackedAircraftInfo({ civilian, military }).layerId, + resolveTrackedAircraftInfo({ civilian, military, trackedId: 'satellites:25544' }).layerId, 'flights', ); - assert.equal( - resolveTrackedAircraftInfo({ - civilian, - military, - trackedId: 'satellites:25544', - }).layerId, - 'flights', - ); - assert.equal( - resolveTrackedAircraftInfo({ military, trackedId: '' }).layerId, - 'military', - ); - assert.equal( - resolveTrackedAircraftInfo({ trackedId: 'flights:aaa077' }), - null, - ); + assert.equal(resolveTrackedAircraftInfo({ military, trackedId: '' }).layerId, 'military'); + assert.equal(resolveTrackedAircraftInfo({ trackedId: 'flights:aaa077' }), null); assert.equal(resolveTrackedAircraftInfo(), null); }); test('cockpit emergency wording states the broadcast code, not a conclusion', () => { assert.equal( - formatCockpitEmergency({ label: 'General emergency', squawk: '7700' }), + formatCockpitEmergency({ label: 'General emergency', squawk: '7700', source: 'squawk', severity: 'emergency' }), 'SQUAWK 7700 · GENERAL EMERGENCY', ); assert.equal( - formatCockpitEmergency({ label: 'Minimum fuel', squawk: null }), - 'MINIMUM FUEL', + formatCockpitEmergency({ label: 'Downed aircraft', squawk: '1200', source: 'ads-b', severity: 'emergency' }), + 'ADS-B EMERGENCY · DOWNED AIRCRAFT', + 'an unrelated squawk is not quoted beside an ADS-B status', + ); + assert.equal( + formatCockpitEmergency({ label: 'Minimum fuel', squawk: null, source: 'ads-b', severity: 'priority' }), + 'ADS-B PRIORITY · MINIMUM FUEL', ); assert.equal(formatCockpitEmergency(null), null); assert.equal(formatCockpitEmergency({ label: '' }), null); diff --git a/src/data/aircraftEmergency.js b/src/data/aircraftEmergency.js index e7ee3aca5..3fd6ab688 100644 --- a/src/data/aircraftEmergency.js +++ b/src/data/aircraftEmergency.js @@ -43,7 +43,9 @@ export function normalizeSquawk(value) { * 7x00 squawk counts even when the ADS-B field says `none`. * @param {{squawk?: unknown, emergency?: unknown}} [observation] * @returns {{kind: string, label: string, severity: 'emergency'|'priority', - * squawk: string|null, source: 'ads-b'|'squawk'}|null} + * squawk: string|null, source: 'ads-b'|'squawk'}|null} `source` is + * 'squawk' when the Mode A code itself names the status (so it can be + * quoted), else 'ads-b'. */ export function aircraftEmergency({ squawk, emergency } = {}) { const code = normalizeSquawk(squawk); @@ -57,7 +59,7 @@ export function aircraftEmergency({ squawk, emergency } = {}) { kind, ...KINDS[kind], squawk: code, - source: fromAdsb ? 'ads-b' : 'squawk', + source: SQUAWK_KINDS[code] === kind ? 'squawk' : 'ads-b', }; } diff --git a/src/data/aircraftEmergency.test.mjs b/src/data/aircraftEmergency.test.mjs index cae9f6c50..2cff800b9 100644 --- a/src/data/aircraftEmergency.test.mjs +++ b/src/data/aircraftEmergency.test.mjs @@ -39,6 +39,15 @@ test('the ADS-B emergency field wins, and `none` defers to the squawk', () => { aircraftEmergency({ squawk: '7700', emergency: 'none' }).source, 'squawk', ); + assert.equal( + aircraftEmergency({ squawk: '7700', emergency: 'general' }).source, + 'squawk', + 'a code that names the status stays quotable', + ); + assert.equal( + aircraftEmergency({ squawk: '1200', emergency: 'general' }).source, + 'ads-b', + ); assert.equal(aircraftEmergency({ emergency: 'downed' }).kind, 'downed'); assert.equal(aircraftEmergency({ emergency: 'none' }), null); assert.equal(aircraftEmergency({ emergency: 'reserved' }), null); diff --git a/src/data/analystEngine.test.mjs b/src/data/analystEngine.test.mjs index 3476af7b4..3bb0dd06e 100644 --- a/src/data/analystEngine.test.mjs +++ b/src/data/analystEngine.test.mjs @@ -1,89 +1,18 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { - createAnalystEngine, - applyScope, - haversineKm, -} from './analystEngine.js'; +import { createAnalystEngine, applyScope, haversineKm } from './analystEngine.js'; // Stub world: a square "Texland" region, flights + ships + fires around it. -const TEXLAND = { - name: 'Texland', - ring: [ - [-100, 28], - [-94, 28], - [-94, 33], - [-100, 33], - ], -}; +const TEXLAND = { name: 'Texland', ring: [[-100, 28], [-94, 28], [-94, 33], [-100, 33]] }; const FLIGHTS = [ - { - id: 'SWA1', - lat: 30.2, - lon: -97.7, - altitudeM: 11000, - speedMps: 240, - military: false, - onGround: false, - routeOrigin: 'AUS', - routeDestination: 'LAX', - }, - { - id: 'RCH01', - lat: 31.0, - lon: -97.0, - altitudeM: 13500, - speedMps: 250, - military: true, - onGround: false, - routeOrigin: null, - routeDestination: null, - }, - { - id: 'N123', - lat: 45.0, - lon: -122.0, - altitudeM: 2000, - speedMps: 80, - military: false, - onGround: false, - squawk: '7700', - emergency: true, - emergencyKind: 'general', - routeOrigin: null, - routeDestination: null, - }, - { - id: 'GND1', - lat: 30.19, - lon: -97.66, - altitudeM: 150, - speedMps: 5, - military: false, - onGround: true, - routeOrigin: null, - routeDestination: null, - }, + { id: 'SWA1', lat: 30.2, lon: -97.7, altitudeM: 11000, speedMps: 240, military: false, onGround: false, routeOrigin: 'AUS', routeDestination: 'LAX' }, + { id: 'RCH01', lat: 31.0, lon: -97.0, altitudeM: 13500, speedMps: 250, military: true, onGround: false, routeOrigin: null, routeDestination: null }, + { id: 'N123', lat: 45.0, lon: -122.0, altitudeM: 2000, speedMps: 80, military: false, onGround: false, squawk: '7700', emergency: true, emergencyKind: 'general', routeOrigin: null, routeDestination: null }, + { id: 'GND1', lat: 30.19, lon: -97.66, altitudeM: 150, speedMps: 5, military: false, onGround: true, routeOrigin: null, routeDestination: null }, ]; const SHIPS = [ - { - id: 'EVERGIVEN', - lat: 29.5, - lon: -94.9, - speedKts: 12, - shipType: 'Cargo', - destination: 'OAKLAND', - navStatus: 'under way', - }, - { - id: 'SLOWBOAT', - lat: 29.6, - lon: -95.0, - speedKts: 0.2, - shipType: 'Tanker', - destination: 'HOUSTON', - navStatus: 'anchored', - }, + { id: 'EVERGIVEN', lat: 29.5, lon: -94.9, speedKts: 12, shipType: 'Cargo', destination: 'OAKLAND', navStatus: 'under way' }, + { id: 'SLOWBOAT', lat: 29.6, lon: -95.0, speedKts: 0.2, shipType: 'Tanker', destination: 'HOUSTON', navStatus: 'anchored' }, ]; const FIRES = [ { id: 'FIRE-1', lat: 30.5, lon: -98.2, frp: 1500 }, @@ -93,10 +22,7 @@ const FIRES = [ function makeEngine() { return createAnalystEngine({ - getRecords: (key) => - ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES })[ - key - ] || [], + getRecords: (key) => ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES }[key] || []), resolveRegionRing: async (name) => (/texland/i.test(name) ? TEXLAND : null), getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }), }); @@ -105,10 +31,7 @@ function makeEngine() { /** Same world, but Contacts is up with a subject far from the parked camera. */ function makeContactsEngine(subject) { return createAnalystEngine({ - getRecords: (key) => - ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES })[ - key - ] || [], + getRecords: (key) => ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES }[key] || []), resolveRegionRing: async (name) => (/texland/i.test(name) ? TEXLAND : null), // Parked far away, as a high-altitude camera often is. getViewContext: () => ({ lat: 45.0, lon: -122.0, viewRadiusKm: 150 }), @@ -129,11 +52,7 @@ test('analyst: a radius query centers on the active contact, not the parked came assert.equal(r.ok, true); // Austin-area flights, not the Oregon one the camera is parked over. assert.equal(r.count, 3); - assert.equal( - r.centeredOn, - 'SWA1', - 'the answer names the centre it measured from', - ); + assert.equal(r.centeredOn, 'SWA1', 'the answer names the centre it measured from'); assert.ok(r.coverage.scope.includes('@SWA1')); }); @@ -144,11 +63,7 @@ test('analyst: an explicit center still wins over the active contact', async () scope: { kind: 'radius', km: 250, center: { lat: 45.0, lon: -122.0 } }, limit: 50, }); - assert.equal( - r.count, - 1, - 'only the Oregon flight is within 250 km of the given center', - ); + assert.equal(r.count, 1, 'only the Oregon flight is within 250 km of the given center'); assert.equal(r.centeredOn, undefined, 'an explicit center is not relabelled'); }); @@ -158,11 +73,7 @@ test('analyst: with Contacts off, radius still centers on the view', async () => scope: { kind: 'radius', km: 250 }, limit: 50, }); - assert.equal( - r.count, - 1, - 'view-centred behaviour is unchanged outside Contacts', - ); + assert.equal(r.count, 1, 'view-centred behaviour is unchanged outside Contacts'); assert.equal(r.centeredOn, undefined); assert.equal(r.coverage.scope, 'radius:250km'); }); @@ -172,25 +83,13 @@ test('analyst: a subject without usable coordinates cannot lend its name to a ca // number. A subject present but position-less fell back to the camera and // kept the contact's name on the answer, so a camera-centred count read as // contact-centred with nothing in the payload to catch it. - const r = await makeContactsEngine({ - lat: null, - lon: null, - label: 'SWA1', - }).query({ + const r = await makeContactsEngine({ lat: null, lon: null, label: 'SWA1' }).query({ layers: ['flights'], scope: { kind: 'radius', km: 250 }, limit: 50, }); - assert.equal( - r.count, - 1, - 'the count is the camera-centred one it actually measured', - ); - assert.equal( - r.centeredOn, - undefined, - 'and it must not claim a centre it did not use', - ); + assert.equal(r.count, 1, 'the count is the camera-centred one it actually measured'); + assert.equal(r.centeredOn, undefined, 'and it must not claim a centre it did not use'); assert.equal(r.coverage.scope, 'radius:250km'); assert.equal(r.scopeLabel, 'within 250 km'); }); @@ -200,47 +99,33 @@ test('analyst: every scope names itself in words', async () => { // answers look like a contradiction, so each scope carries its own phrasing. const subject = { lat: 30.2, lon: -97.7, label: 'DYNO11' }; const centred = await makeContactsEngine(subject).query({ - layers: ['flights'], - scope: { kind: 'radius', km: 250 }, - limit: 1, + layers: ['flights'], scope: { kind: 'radius', km: 250 }, limit: 1, }); assert.equal(centred.scopeLabel, 'within 250 km of DYNO11'); const plainRadius = await makeContactsEngine(null).query({ - layers: ['flights'], - scope: { kind: 'radius', km: 250 }, - limit: 1, + layers: ['flights'], scope: { kind: 'radius', km: 250 }, limit: 1, }); assert.equal(plainRadius.scopeLabel, 'within 250 km'); const inView = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'view' }, - limit: 1, + layers: ['flights'], scope: { kind: 'view' }, limit: 1, }); assert.equal(inView.scopeLabel, 'in view'); const region = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'region', name: 'Texland' }, - limit: 1, + layers: ['flights'], scope: { kind: 'region', name: 'Texland' }, limit: 1, }); assert.equal(region.scopeLabel, 'over Texland'); const anywhere = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'anywhere' }, - limit: 1, + layers: ['flights'], scope: { kind: 'anywhere' }, limit: 1, }); assert.equal(anywhere.scopeLabel, 'anywhere in the loaded data'); }); test('analyst: count flights over a region', async () => { - const r = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'region', name: 'Texland' }, - limit: 50, - }); + const r = await makeEngine().query({ layers: ['flights'], scope: { kind: 'region', name: 'Texland' }, limit: 50 }); assert.equal(r.ok, true); assert.equal(r.count, 3, 'Oregon flight excluded'); assert.ok(r.coverage.scope.includes('Texland')); @@ -248,65 +133,32 @@ test('analyst: count flights over a region', async () => { test('analyst: attribute filter — above 40,000 ft (~12,192 m)', async () => { const r = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'anywhere' }, + layers: ['flights'], scope: { kind: 'anywhere' }, filters: [{ field: 'altitudeM', op: 'gt', value: 12192 }], }); - assert.deepEqual( - r.items.map((i) => i.id), - ['RCH01'], - ); + assert.deepEqual(r.items.map((i) => i.id), ['RCH01']); }); test('analyst: military flag + region compose', async () => { const r = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'region', name: 'Texland' }, + layers: ['flights'], scope: { kind: 'region', name: 'Texland' }, filters: [{ field: 'military', op: 'eq', value: true }], }); assert.equal(r.count, 1); assert.equal(r.items[0].id, 'RCH01'); }); -test('analyst: aircraft broadcasting an emergency are filterable by flag and code', async () => { - const flagged = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'anywhere' }, - filters: [{ field: 'emergency', op: 'eq', value: true }], - }); - assert.deepEqual( - flagged.items.map((i) => i.id), - ['N123'], - ); - const coded = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'anywhere' }, - filters: [{ field: 'squawk', op: 'eq', value: '7700' }], - }); - assert.deepEqual( - coded.items.map((i) => i.id), - ['N123'], - ); -}); - test('analyst: ships headed to Oakland (destination contains)', async () => { const r = await makeEngine().query({ - layers: ['ais-live-vessels'], - scope: { kind: 'anywhere' }, + layers: ['ais-live-vessels'], scope: { kind: 'anywhere' }, filters: [{ field: 'destination', op: 'contains', value: 'oakland' }], }); - assert.deepEqual( - r.items.map((i) => i.id), - ['EVERGIVEN'], - ); + assert.deepEqual(r.items.map((i) => i.id), ['EVERGIVEN']); }); test('analyst: superlative — biggest fire in view radius', async () => { const r = await makeEngine().query({ - layers: ['local-firms'], - scope: { kind: 'view' }, - sortBy: 'frp', - limit: 1, + layers: ['local-firms'], scope: { kind: 'view' }, sortBy: 'frp', limit: 1, }); assert.equal(r.items[0].id, 'FIRE-1', 'BC monster is out of view scope'); assert.equal(r.summary.frpMax, 1500); @@ -314,10 +166,7 @@ test('analyst: superlative — biggest fire in view radius', async () => { test('analyst: nearest sorting attaches distanceKm ascending', async () => { const r = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'view' }, - sortBy: 'distance', - limit: 3, + layers: ['flights'], scope: { kind: 'view' }, sortBy: 'distance', limit: 3, }); assert.ok(r.items[0].distanceKm <= r.items[1].distanceKm); assert.ok(Number.isFinite(r.items[0].distanceKm)); @@ -417,10 +266,7 @@ test('analyst: failed pre-commit work cannot replace follow-up memory', async () }); test('analyst: unresolved region is an honest failure, not empty success', async () => { - const r = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'region', name: 'Atlantis' }, - }); + const r = await makeEngine().query({ layers: ['flights'], scope: { kind: 'region', name: 'Atlantis' } }); assert.equal(r.ok, false); assert.match(r.error, /Atlantis/); }); @@ -428,17 +274,10 @@ test('analyst: unresolved region is an honest failure, not empty success', async test('analyst: a region lookup timeout is reported as region-timeout', async () => { const eng = createAnalystEngine({ getRecords: () => FLIGHTS, - resolveRegionRing: async (name) => ({ - name, - ring: null, - error: 'region-timeout', - }), + resolveRegionRing: async (name) => ({ name, ring: null, error: 'region-timeout' }), getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }), }); - const r = await eng.query({ - layers: ['flights'], - scope: { kind: 'region', name: 'Texas' }, - }); + const r = await eng.query({ layers: ['flights'], scope: { kind: 'region', name: 'Texas' } }); assert.equal(r.ok, false); assert.equal(r.code, 'region-timeout'); assert.match(r.error, /Texas/); @@ -480,84 +319,45 @@ test('analyst: region lookup receives and obeys the owning action signal', async test('analyst: route fields queryable from cached enrichment only', async () => { const r = await makeEngine().query({ - layers: ['flights'], - scope: { kind: 'anywhere' }, + layers: ['flights'], scope: { kind: 'anywhere' }, filters: [{ field: 'routeDestination', op: 'eq', value: 'LAX' }], }); - assert.deepEqual( - r.items.map((i) => i.id), - ['SWA1'], - 'null route fields never match', - ); + assert.deepEqual(r.items.map((i) => i.id), ['SWA1'], 'null route fields never match'); +}); + +test('analyst: aircraft broadcasting an emergency are filterable by flag and code', async () => { + const flagged = await makeEngine().query({ + layers: ['flights'], scope: { kind: 'anywhere' }, + filters: [{ field: 'emergency', op: 'eq', value: true }], + }); + assert.deepEqual(flagged.items.map((i) => i.id), ['N123']); + const coded = await makeEngine().query({ + layers: ['flights'], scope: { kind: 'anywhere' }, + filters: [{ field: 'squawk', op: 'eq', value: '7700' }], + }); + assert.deepEqual(coded.items.map((i) => i.id), ['N123']); }); test('analyst: satellites and local infrastructure are queryable layers', async () => { const SATS = [ - { - id: 'ISS (ZARYA)', - noradId: '25544', - name: 'ISS (ZARYA)', - lat: 30.3, - lon: -97.7, - altitudeM: 410000, - satelliteClass: 'STATION · ISS', - group: 'stations', - }, - { - id: 'GPS BIIR-2', - noradId: '24876', - name: 'GPS BIIR-2', - lat: 51.0, - lon: 0.0, - altitudeM: 20200000, - satelliteClass: 'NAV · GPS', - group: 'gps-ops', - }, + { id: 'ISS (ZARYA)', noradId: '25544', name: 'ISS (ZARYA)', lat: 30.3, lon: -97.7, altitudeM: 410000, satelliteClass: 'STATION · ISS', group: 'stations' }, + { id: 'GPS BIIR-2', noradId: '24876', name: 'GPS BIIR-2', lat: 51.0, lon: 0.0, altitudeM: 20200000, satelliteClass: 'NAV · GPS', group: 'gps-ops' }, ]; const DAMS = [ - { - id: 'Austin Dam', - name: 'Austin Dam', - lat: 30.27, - lon: -97.74, - operator: 'LCRA', - river: 'Colorado', - output: '2 MW', - }, - { - id: 'Far Dam', - name: 'Far Dam', - lat: 45.0, - lon: -122.0, - operator: 'USACE', - river: 'Columbia', - output: '1000 MW', - }, + { id: 'Austin Dam', name: 'Austin Dam', lat: 30.27, lon: -97.74, operator: 'LCRA', river: 'Colorado', output: '2 MW' }, + { id: 'Far Dam', name: 'Far Dam', lat: 45.0, lon: -122.0, operator: 'USACE', river: 'Columbia', output: '1000 MW' }, ]; const DCS = [ - { - id: 'AUS-1', - name: 'AUS-1', - lat: 30.28, - lon: -97.75, - operator: 'Example Cloud', - capacity: '27 MW', - }, + { id: 'AUS-1', name: 'AUS-1', lat: 30.28, lon: -97.75, operator: 'Example Cloud', capacity: '27 MW' }, ]; const eng = createAnalystEngine({ - getRecords: (key) => - ({ satellites: SATS, 'local-dams': DAMS, 'local-datacenters': DCS })[ - key - ] || [], + getRecords: (key) => ({ satellites: SATS, 'local-dams': DAMS, 'local-datacenters': DCS }[key] || []), resolveRegionRing: async (name) => (/texland/i.test(name) ? TEXLAND : null), getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }), }); const sats = await eng.query({ - layers: ['satellites'], - scope: { kind: 'view' }, - sortBy: 'distance', - limit: 2, + layers: ['satellites'], scope: { kind: 'view' }, sortBy: 'distance', limit: 2, }); assert.equal(sats.ok, true); assert.equal(sats.count, 1, 'GPS sat is out of the Austin view radius'); @@ -565,28 +365,20 @@ test('analyst: satellites and local infrastructure are queryable layers', async assert.ok(Number.isFinite(sats.items[0].distanceKm)); const nav = await eng.query({ - layers: ['satellites'], - scope: { kind: 'anywhere' }, + layers: ['satellites'], scope: { kind: 'anywhere' }, filters: [{ field: 'satelliteClass', op: 'contains', value: 'NAV' }], }); - assert.deepEqual( - nav.items.map((i) => i.id), - ['GPS BIIR-2'], - ); + assert.deepEqual(nav.items.map((i) => i.id), ['GPS BIIR-2']); const dams = await eng.query({ - layers: ['local-dams'], - scope: { kind: 'view' }, - sortBy: 'distance', - limit: 5, + layers: ['local-dams'], scope: { kind: 'view' }, sortBy: 'distance', limit: 5, }); assert.equal(dams.count, 1); assert.equal(dams.items[0].id, 'Austin Dam'); assert.equal(dams.items[0].river, 'Colorado'); const dcs = await eng.query({ - layers: ['local-datacenters'], - scope: { kind: 'region', name: 'Texland' }, + layers: ['local-datacenters'], scope: { kind: 'region', name: 'Texland' }, filters: [{ field: 'operator', op: 'contains', value: 'cloud' }], }); assert.equal(dcs.count, 1); @@ -596,11 +388,7 @@ test('analyst: satellites and local infrastructure are queryable layers', async test('helpers: haversine sanity + scope radius', () => { const km = haversineKm(30.2672, -97.7431, 29.7604, -95.3698); // Austin→Houston assert.ok(km > 200 && km < 280, `Austin-Houston ~235km, got ${km}`); - const scoped = applyScope( - FLIGHTS, - { kind: 'radius' }, - { center: { lat: 30.27, lon: -97.74 }, km: 50 }, - ); + const scoped = applyScope(FLIGHTS, { kind: 'radius' }, { center: { lat: 30.27, lon: -97.74 }, km: 50 }); assert.deepEqual(scoped.map((f) => f.id).sort(), ['GND1', 'SWA1']); }); @@ -610,11 +398,7 @@ test('capped cohorts report returned/total/truncated and keep it on follow-up', getRecordCoverage: () => ({ total: 3000, truncated: true }), getViewContext: () => ({ lat: 0, lon: 0, viewRadiusKm: 25 }), }); - const result = await engine.query({ - layers: ['local-dams'], - scope: { kind: 'anywhere' }, - sortBy: 'distance', - }); + const result = await engine.query({ layers: ['local-dams'], scope: { kind: 'anywhere' }, sortBy: 'distance' }); assert.equal(result.count, 1); const layer = result.coverage.layersQueried[0]; assert.deepEqual({ returned: layer.returned, total: layer.total, truncated: layer.truncated }, { returned: 1, total: 3000, truncated: true }); @@ -735,15 +519,12 @@ test('time fields filter by ISO and derive ageHours; the feed window is exposed' assert.equal(age.coverage.layersQueried[0].window, 'last 24 h'); }); + test('follow-up provenance stays attached to old rows after a feed recovers', async () => { let state = 'stale'; const engine = createAnalystEngine({ getRecords: () => [{ id: 'A', lat: 0, lon: 0 }], - getLayerSnapshot: () => ({ - id: 'flights', - enabled: true, - feedState: state, - }), + getLayerSnapshot: () => ({ id: 'flights', enabled: true, feedState: state }), getViewContext: () => ({ lat: 0, lon: 0, viewRadiusKm: 25 }), }); const first = await engine.query(); @@ -753,10 +534,7 @@ test('follow-up provenance stays attached to old rows after a feed recovers', as assert.equal(followUp.coverage.feedProvenance.overall, 'stale'); engine.reset(); assert.equal(engine.hasMemory(), false); - assert.equal( - (await engine.query()).coverage.feedProvenance.overall, - 'nominal', - ); + assert.equal((await engine.query()).coverage.feedProvenance.overall, 'nominal'); }); test('a large loaded set sorts and summarizes without overflowing the stack', async () => { diff --git a/src/data/flights.test.mjs b/src/data/flights.test.mjs index 740f1afcc..0e7021251 100644 --- a/src/data/flights.test.mjs +++ b/src/data/flights.test.mjs @@ -18,10 +18,8 @@ import flightsLayer, { } from './flights.js'; import { setMilitaryLayerActive } from './militaryRegistry.js'; import { - GROUND_FLOOR_LIFT_M, - reportMeshFloorCell, - setMeshFloorPreferred, - _clearMeshFloorCellsForTest, + GROUND_FLOOR_LIFT_M, reportMeshFloorCell, + setMeshFloorPreferred, _clearMeshFloorCellsForTest, } from './groundFloor.js'; import { _setTrackedOverlayHostForTest, @@ -35,10 +33,7 @@ test('share-Follow absence requires an accepted OpenSky snapshot', async () => { (await flightsLayer.resolveTrackingRestoreTarget('abc123')).status, 'source-unavailable', ); - _setFlightTrackingRefreshOutcomeForTest({ - status: 'accepted', - ids: ['different'], - }); + _setFlightTrackingRefreshOutcomeForTest({ status: 'accepted', ids: ['different'] }); const missing = await flightsLayer.resolveTrackingRestoreTarget('abc123'); assert.equal(missing.status, 'missing'); assert.equal(missing.reason, 'target-absent-from-snapshot'); @@ -60,10 +55,7 @@ const FULL_INFO = { }; test('flights analyst record: full record maps every contract field', () => { - const r = mapAnalystRecord('a1b2c3', FULL_INFO, { - military: false, - routeOk: true, - }); + const r = mapAnalystRecord('a1b2c3', FULL_INFO, { military: false, routeOk: true }); assert.deepEqual(r, { id: 'SWA696', icao24: 'a1b2c3', @@ -101,8 +93,7 @@ test('flights analyst record: empty info yields nulls, never NaN/undefined', () assert.equal(r.military, false); for (const [key, value] of Object.entries(r)) { assert.notEqual(value, undefined, `${key} must not be undefined`); - if (typeof value === 'number') - assert.ok(Number.isFinite(value), `${key} must not be NaN`); + if (typeof value === 'number') assert.ok(Number.isFinite(value), `${key} must not be NaN`); } }); @@ -110,31 +101,14 @@ test('flights analyst record: no callsign falls back to registration, then icao2 const r = mapAnalystRecord('abc123', { ...FULL_INFO, callsign: ' ' }); assert.equal(r.id, 'abc123'); // FULL_INFO carries no registration assert.equal(r.callsign, null); - assert.equal( - mapAnalystRecord('abc123', { - ...FULL_INFO, - callsign: '', - registration: 'N123AB', - }).id, - 'N123AB', - ); - assert.equal( - mapAnalystRecord('abc123', { callsign: '', registration: ' ' }).id, - 'abc123', - ); + assert.equal(mapAnalystRecord('abc123', { ...FULL_INFO, callsign: '', registration: 'N123AB' }).id, 'N123AB'); + assert.equal(mapAnalystRecord('abc123', { callsign: '', registration: ' ' }).id, 'abc123'); }); test('flights analyst record: NaN kinematics become null, military flag passes through', () => { - const r = mapAnalystRecord( - 'ae01ce', - { - ...FULL_INFO, - velocity: NaN, - true_track: undefined, - verticalRate: null, - }, - { military: true }, - ); + const r = mapAnalystRecord('ae01ce', { + ...FULL_INFO, velocity: NaN, true_track: undefined, verticalRate: null, + }, { military: true }); assert.equal(r.speedMps, null); assert.equal(r.heading, null); assert.equal(r.verticalRateMps, null); @@ -149,25 +123,17 @@ test('flights analyst record: output is JSON-safe (no Cesium types)', () => { test('flights first update forwards caller cancellation into the feed request', async () => { const realFetch = globalThis.fetch; let observedSignal = null; - globalThis.fetch = (_url, options = {}) => - new Promise((_resolve, reject) => { - observedSignal = options.signal; - options.signal?.addEventListener( - 'abort', - () => { - reject(new DOMException('aborted', 'AbortError')); - }, - { once: true }, - ); - }); + globalThis.fetch = (_url, options = {}) => new Promise((_resolve, reject) => { + observedSignal = options.signal; + options.signal?.addEventListener('abort', () => { + reject(new DOMException('aborted', 'AbortError')); + }, { once: true }); + }); try { const controller = new AbortController(); - const work = flightsLayer.update( - { camera: { positionCartographic: null }, scene: {} }, - { - signal: controller.signal, - }, - ); + const work = flightsLayer.update({ camera: { positionCartographic: null }, scene: {} }, { + signal: controller.signal, + }); await Promise.resolve(); assert.ok(observedSignal); controller.abort(); @@ -192,27 +158,14 @@ test('nonempty OpenSky payload with zero usable rows cannot prove share target a ok: true, status: 200, headers: { get: () => null }, - json: async () => ({ - states: [null, {}, ['abc123', null, null, null, null, null, null]], - }), + json: async () => ({ states: [null, {}, ['abc123', null, null, null, null, null, null]] }), }); try { - await flightsLayer.update({ - camera: { positionCartographic: null }, - scene: {}, - }); - const resolution = - await flightsLayer.resolveTrackingRestoreTarget('abc123'); + await flightsLayer.update({ camera: { positionCartographic: null }, scene: {} }); + const resolution = await flightsLayer.resolveTrackingRestoreTarget('abc123'); assert.equal(resolution.status, 'source-unavailable'); - assert.match( - flightsLayer.getStats().error, - /Malformed OpenSky aircraft rows/, - ); - assert.equal( - flightsLayer.getAnalystRecords().length, - 1, - 'warm aircraft data is preserved', - ); + assert.match(flightsLayer.getStats().error, /Malformed OpenSky aircraft rows/); + assert.equal(flightsLayer.getAnalystRecords().length, 1, 'warm aircraft data is preserved'); } finally { globalThis.fetch = realFetch; } @@ -350,31 +303,13 @@ test('flights poll refreshes tracked callsign/FL/kts and marks a missed poll STA if (!String(url).startsWith('/api/flights')) { return { ok: true, status: 200, json: async () => ({ ac: [] }) }; } - const states = - openskyPoll++ === 0 - ? [ - [ - icao24, - 'DAL123 ', - 'United States', - nowSec, - nowSec, - -97.6, - 30.3, - 10_668, - false, - 250, - 95, - 5, - null, - 10_700, - null, - null, - null, - 5, - ], - ] - : []; + const states = openskyPoll++ === 0 + ? [[ + icao24, 'DAL123 ', 'United States', nowSec, nowSec, + -97.6, 30.3, 10_668, false, 250, 95, 5, null, 10_700, + null, null, null, 5, + ]] + : []; return { ok: true, status: 200, @@ -413,18 +348,14 @@ test('real civil track path creates no native label and publishes every cached h trackedEntityChanged, clock: { currentTime: now }, camera: { - cancelFlight() { - cancelledFlights += 1; - }, + cancelFlight() { cancelledFlights += 1; }, position: new Cesium.Cartesian3(0, -10_000, 7_000), positionWC: new Cesium.Cartesian3(0, -10_000, 7_000), direction: Cesium.Cartesian3.UNIT_Y, transform: Cesium.Matrix4.IDENTITY, viewMatrix: Cesium.Matrix4.IDENTITY, frustum: { projectionMatrix: Cesium.Matrix4.IDENTITY }, - lookAtTransform() { - appliedFrames += 1; - }, + lookAtTransform() { appliedFrames += 1; }, }, scene: { canvas: { clientWidth: 1600, clientHeight: 900 }, @@ -463,12 +394,9 @@ test('real civil track path creates no native label and publishes every cached h const realFetch = globalThis.fetch; globalThis.window = new EventTarget(); const selectionEvents = []; - globalThis.window.addEventListener( - 'gev:awareness-subject-selected', - (event) => { - selectionEvents.push(event.detail); - }, - ); + globalThis.window.addEventListener('gev:awareness-subject-selected', (event) => { + selectionEvents.push(event.detail); + }); globalThis.fetch = async () => ({ ok: false }); _setTrackedOverlayHostForTest(host); try { @@ -480,15 +408,13 @@ test('real civil track path creates no native label and publishes every cached h billboardCollection: { show: true, remove() {} }, viewer, tracked: false, - history: [ - { - time: now, - epochMs: Date.now(), - position, - velocity: 250, - track: 95, - }, - ], + history: [{ + time: now, + epochMs: Date.now(), + position, + velocity: 250, + track: 95, + }], meta: { callsign: 'N12345', altitude: 10_668, @@ -511,19 +437,11 @@ test('real civil track path creates no native label and publishes every cached h }, }); - assert.equal( - flightsLayer.trackById(icao24, { origin: 'programmatic' }), - true, - ); + assert.equal(flightsLayer.trackById(icao24, { origin: 'programmatic' }), true); const entity = viewer.trackedEntity; - assert.ok( - entity instanceof Cesium.Entity, - 'trackById must create the real Cesium entity', - ); + assert.ok(entity instanceof Cesium.Entity, 'trackById must create the real Cesium entity'); assert.equal(entity.label, undefined); - assert.ok( - entities.values.every((candidate) => candidate.label === undefined), - ); + assert.ok(entities.values.every((candidate) => candidate.label === undefined)); assert.deepEqual(entity.gevLabelModel, { title: 'N12345 · FL350 · 486 kts', details: ['TEST AIR · A320', 'AUS → LAX'], @@ -533,44 +451,17 @@ test('real civil track path creates no native label and publishes every cached h const initialAppliedFrames = appliedFrames; const initialCancelledFlights = cancelledFlights; assert.equal(flightsLayer.trackById(icao24, { origin: 'user' }), true); - assert.equal( - cancelledFlights, - initialCancelledFlights, - 'ordinary repeated tracking stays camera-idempotent', - ); - assert.equal( - selectionEvents.at(-1)?.origin, - 'user', - 'same-target selection upgrades durable authority', - ); + assert.equal(cancelledFlights, initialCancelledFlights, 'ordinary repeated tracking stays camera-idempotent'); + assert.equal(selectionEvents.at(-1)?.origin, 'user', 'same-target selection upgrades durable authority'); assert.equal(entity.gevSelectionOrigin, 'user'); assert.equal(flightsLayer.refocusTrackedById('different-flight'), false); - assert.equal( - flightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), - true, - ); - assert.equal( - selectionEvents.at(-1)?.origin, - 'voice', - 'same-target refocus forwards explicit authority', - ); + assert.equal(flightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), true); + assert.equal(selectionEvents.at(-1)?.origin, 'voice', 'same-target refocus forwards explicit authority'); assert.equal(flightsLayer.refocusTrackedById(icao24), true); viewer.scene.preUpdate.raiseEvent(); - assert.equal( - viewer.trackedEntity, - entity, - 'refocus must retain the exact tracked entity', - ); - assert.equal( - appliedFrames, - initialAppliedFrames + 1, - 'repeated refocus keeps one camera-frame owner', - ); - assert.equal( - cancelledFlights, - initialCancelledFlights + 2, - 'only explicit refocus requests cancel camera flights', - ); + assert.equal(viewer.trackedEntity, entity, 'refocus must retain the exact tracked entity'); + assert.equal(appliedFrames, initialAppliedFrames + 1, 'repeated refocus keeps one camera-frame owner'); + assert.equal(cancelledFlights, initialCancelledFlights + 2, 'only explicit refocus requests cancel camera flights'); const publication = publications.at(-1); assert.equal(publication.sourceId, 'tracked'); const entry = publication.entries[0]; @@ -581,11 +472,7 @@ test('real civil track path creates no native label and publishes every cached h const display = entity.position.getValue(now); assert.ok(display, 'tracked position callback must seed its frame cache'); - assert.equal( - entry.position(), - display, - 'host must reuse the exact cached display Cartesian', - ); + assert.equal(entry.position(), display, 'host must reuse the exact cached display Cartesian'); flightsLayer.setParams( { selectedFlightsTrackingId: 'late001' }, @@ -593,17 +480,10 @@ test('real civil track path creates no native label and publishes every cached h ); assert.equal(_pendingFlightTrackingRestoreForTest(), 'late001'); assert.equal(flightsLayer.trackById(icao24, { origin: 'user' }), true); - assert.equal( - _pendingFlightTrackingRestoreForTest(), - null, - 'new user selection cancels stale restore', - ); + assert.equal(_pendingFlightTrackingRestoreForTest(), null, 'new user selection cancels stale restore'); _addFlightTrackingCandidateForTest({ icao24: 'late001', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-97.5, 30.4, 9_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.5, 30.4, 9_000) }, meta: { ...FULL_INFO, callsign: 'LATE1', rawLat: 30.4, rawLon: -97.5 }, history: [], }); @@ -617,17 +497,10 @@ test('real civil track path creates no native label and publishes every cached h ); assert.equal(_pendingFlightTrackingRestoreForTest(), 'late002'); flightsLayer.stopTracking({ origin: 'user' }); - assert.equal( - _pendingFlightTrackingRestoreForTest(), - null, - 'explicit clear cancels stale restore', - ); + assert.equal(_pendingFlightTrackingRestoreForTest(), null, 'explicit clear cancels stale restore'); _addFlightTrackingCandidateForTest({ icao24: 'late002', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-97.4, 30.5, 8_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.4, 30.5, 8_000) }, meta: { ...FULL_INFO, callsign: 'LATE2', rawLat: 30.5, rawLon: -97.4 }, history: [], }); @@ -647,10 +520,7 @@ test('real civil track path creates no native label and publishes every cached h ); _addFlightTrackingCandidateForTest({ icao24: 'late003', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000) }, meta: { ...FULL_INFO, callsign: 'LATE3', rawLat: 30.6, rawLon: -97.3 }, history: [], }); @@ -664,10 +534,7 @@ test('real civil track path creates no native label and publishes every cached h assert.equal(_pendingFlightTrackingRestoreForTest(), 'late004'); _addFlightTrackingCandidateForTest({ icao24: 'late004', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000) }, meta: { ...FULL_INFO, callsign: 'LATE4', rawLat: 30.7, rawLon: -97.2 }, history: [], }); @@ -678,10 +545,7 @@ test('real civil track path creates no native label and publishes every cached h { selectedFlightsTrackingId: 'late005' }, { origin: 'share-restore' }, ); - assert.equal( - flightsLayer.trackById(icao24, { origin: 'programmatic' }), - true, - ); + assert.equal(flightsLayer.trackById(icao24, { origin: 'programmatic' }), true); assert.equal( _pendingFlightTrackingRestoreForTest(), 'late005', @@ -689,10 +553,7 @@ test('real civil track path creates no native label and publishes every cached h ); _addFlightTrackingCandidateForTest({ icao24: 'late005', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-97.1, 30.8, 5_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.1, 30.8, 5_000) }, meta: { ...FULL_INFO, callsign: 'LATE5', rawLat: 30.8, rawLon: -97.1 }, history: [], }); @@ -727,11 +588,7 @@ function seedLabelContact({ callsign, registration, tracked = false }) { _setTrackedFlightRefreshStateForTest({ icao24: LABEL_ICAO, entity: null, - billboard: { - position: LABEL_POSITION, - color: Cesium.Color.WHITE, - show: true, - }, + billboard: { position: LABEL_POSITION, color: Cesium.Color.WHITE, show: true }, billboardCollection: { show: true, remove() {} }, viewer: { camera: { positionCartographic: null }, scene: {} }, tracked, @@ -789,11 +646,7 @@ test('civil label chain: a blank callsign falls back to the registration, not th const labels = labelsFor({ callsign: ' ', registration: 'N123AB ' }); for (const [surface, value] of Object.entries(labels)) { if (surface === '_identity') continue; - assert.equal( - value, - 'N123AB', - `${surface} must show the registration, not ${LABEL_ICAO}`, - ); + assert.equal(value, 'N123AB', `${surface} must show the registration, not ${LABEL_ICAO}`); } }); @@ -802,11 +655,7 @@ test('civil label chain: no callsign and no registration still reads as the ICAO const labels = labelsFor({ callsign: null, registration }); for (const [surface, value] of Object.entries(labels)) { if (surface === '_identity') continue; - assert.equal( - value, - LABEL_ICAO, - `${surface} must fall through to the hex`, - ); + assert.equal(value, LABEL_ICAO, `${surface} must fall through to the hex`); } } }); @@ -830,11 +679,7 @@ test('civil label chain: identity stays icao24 while the label moves', () => { ]) { const { _identity } = labelsFor(fixture); for (const [key, value] of Object.entries(_identity)) { - assert.equal( - value, - LABEL_ICAO, - `${key} identity must remain the ICAO hex`, - ); + assert.equal(value, LABEL_ICAO, `${key} identity must remain the ICAO hex`); } } }); @@ -881,11 +726,7 @@ test('civil label chain: the awareness selection EVENT uses the canonical chain' `selection event label for ${JSON.stringify(fixture)}`, ); // Identity must NOT move with the label. - assert.equal( - detail.id, - LABEL_ICAO, - 'selection event identity stays the ICAO hex', - ); + assert.equal(detail.id, LABEL_ICAO, 'selection event identity stays the ICAO hex'); assert.equal(detail.layerId, 'flights'); } }); @@ -896,11 +737,7 @@ test('civil tracked readout: a callsign-less enriched contact reads as its regis _setTrackedFlightRefreshStateForTest({ icao24: LABEL_ICAO, entity, - billboard: { - position: LABEL_POSITION, - color: Cesium.Color.WHITE, - show: false, - }, + billboard: { position: LABEL_POSITION, color: Cesium.Color.WHITE, show: false }, billboardCollection: { show: false, remove() {} }, viewer: { camera: { positionCartographic: null }, scene: {} }, meta: { @@ -921,16 +758,10 @@ test('civil tracked readout: a callsign-less enriched contact reads as its regis const realFetch = globalThis.fetch; globalThis.fetch = async () => ({ - ok: true, - status: 200, - headers: { get: () => null }, - json: async () => ({ time: 0, states: [] }), + ok: true, status: 200, headers: { get: () => null }, json: async () => ({ time: 0, states: [] }), }); try { - await flightsLayer.update({ - camera: { positionCartographic: null }, - scene: {}, - }); + await flightsLayer.update({ camera: { positionCartographic: null }, scene: {} }); assert.match(entity.gevLabelModel.title, /^N123AB\b/); assert.doesNotMatch( [entity.gevLabelModel.title, ...entity.gevLabelModel.details].join(' · '), @@ -1010,8 +841,7 @@ test('military suppression exempts the contact this layer is still restoring', ( // contacts only, never when a 3D model owns the visual (T7 one-shot). // --------------------------------------------------------------------------- -const _floorCarto = (pos) => - Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84); +const _floorCarto = (pos) => Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84); test('a newly seen taxiing aircraft starts on the floor under its delayed position', async () => { // A grounded contact is drawn at its delayed position lifted onto the floor @@ -1116,16 +946,10 @@ test('display floor: a grounded contact that drifted onto a higher cell is lifte // Fix cell floor was 124.7; the display has taxied into a 140.2 m cell. reportMeshFloorCell(30.2004, -97.6604, 140.2); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - ); + const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false); const c = _floorCarto(out); - assert.ok( - Math.abs(c.height - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, - `height ${c.height} should be 140.2 + lift`, - ); + assert.ok(Math.abs(c.height - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, + `height ${c.height} should be 140.2 + lift`); // Lifted straight up — the drifted lat/lon is preserved exactly. assert.ok(Math.abs(Cesium.Math.toDegrees(c.latitude) - 30.2004) < 1e-6); assert.ok(Math.abs(Cesium.Math.toDegrees(c.longitude) + 97.6604) < 1e-6); @@ -1137,11 +961,7 @@ test('display floor: an AIRBORNE contact over the same cell is never lifted', () setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140.2); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest( - { onGround: false }, - pos, - false, - ); + const out = _floorGroundedDisplayPositionForTest({ onGround: false }, pos, false); assert.equal(out, pos, 'airborne display positions pass through untouched'); }); @@ -1151,16 +971,8 @@ test('display floor: T7 — a model-owned grounded contact keeps its billboard d setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140.2); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - true, - ); - assert.equal( - out, - pos, - 'moving it would drag groundSnap past its 50 m re-sample threshold', - ); + const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, true); + assert.equal(out, pos, 'moving it would drag groundSnap past its 50 m re-sample threshold'); }); test('display floor: a contact already above the floor is passed through unchanged', () => { @@ -1169,11 +981,7 @@ test('display floor: a contact already above the floor is passed through unchang setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 124.7); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 140.2); - const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - ); + const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false); assert.equal(out, pos, 'no rebuild when nothing needs lifting'); }); @@ -1182,11 +990,7 @@ test('display floor: a cold display cell leaves the contact where it is', () => _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - ); + const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false); assert.equal(out, pos, 'no floor data → no clamp (never invent a surface)'); }); @@ -1197,11 +1001,7 @@ test('display floor: mesh cells are ignored outside the google-3d regime', () => reportMeshFloorCell(30.2004, -97.6604, 140.2); setMeshFloorPreferred(false); // globe stack: the DEM IS the rendered terrain const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - ); + const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false); assert.equal(out, pos); setMeshFloorPreferred(true); }); @@ -1217,23 +1017,14 @@ test('display floor: jitter across a cell edge does not alternate the floor', () reportMeshFloorCell(30.2014, -97.6604, 146); const at = (lat) => { const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, lat, 120), - false, - 'jitter', + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, lat, 120), false, 'jitter', ); return _floorCarto(out).height; }; const settled = at(30.2004); // squarely inside the low cell - assert.ok( - Math.abs(settled - (140 + GROUND_FLOOR_LIFT_M)) < 0.05, - `settled ${settled}`, - ); + assert.ok(Math.abs(settled - (140 + GROUND_FLOOR_LIFT_M)) < 0.05, `settled ${settled}`); // 30.2006 belongs to the HIGH cell but is only ~11 m past the boundary. - assert.ok( - Math.abs(at(30.2006) - settled) < 0.05, - 'held the low cell across the edge', - ); + assert.ok(Math.abs(at(30.2006) - settled) < 0.05, 'held the low cell across the edge'); assert.ok(Math.abs(at(30.2004) - settled) < 0.05, 'and back again — no pop'); }); @@ -1243,21 +1034,12 @@ test('display floor: a real crossing does adopt the next cell', () => { setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140); reportMeshFloorCell(30.2014, -97.6604, 146); - const at = (lat) => - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, lat, 120), - false, - 'crossing', - ), - ).height; + const at = (lat) => _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, lat, 120), false, 'crossing', + )).height; at(30.2004); const crossed = at(30.2014); // properly into the next cell - assert.ok( - Math.abs(crossed - (146 + GROUND_FLOOR_LIFT_M)) < 0.05, - `crossed ${crossed}`, - ); + assert.ok(Math.abs(crossed - (146 + GROUND_FLOOR_LIFT_M)) < 0.05, `crossed ${crossed}`); }); // --- F7: no rebuild when nothing changed ----------------------------------- @@ -1272,28 +1054,10 @@ test('display floor: a clamped stationary contact reuses its previous output', ( setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 120); - const first = _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'parked', - ); - const second = _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'parked', - ); - assert.notEqual( - first, - pos, - 'it really is clamped (not the pass-through case)', - ); - assert.equal( - second, - first, - 'same input + same floor ⇒ the very same object, no rebuild', - ); + const first = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'parked'); + const second = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'parked'); + assert.notEqual(first, pos, 'it really is clamped (not the pass-through case)'); + assert.equal(second, first, 'same input + same floor ⇒ the very same object, no rebuild'); }); test('display floor: the cached output is dropped when the floor changes under it', () => { @@ -1302,31 +1066,14 @@ test('display floor: the cached output is dropped when the floor changes under i setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 120); - const before = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'warming', - ), - ).height; + const before = _floorCarto(_floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'warming')).height; // A late mesh sample raises this cell (a fresh module state stands in for the // one-shot latch, which never overwrites). _clearMeshFloorCellsForTest(); reportMeshFloorCell(30.2004, -97.6604, 175); - const after = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'warming', - ), - ).height; + const after = _floorCarto(_floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'warming')).height; assert.ok(Math.abs(before - 141.5) < 0.05, `before ${before}`); - assert.ok( - Math.abs(after - 176.5) < 0.05, - `after ${after} — a stale cache would still read 141.5`, - ); + assert.ok(Math.abs(after - 176.5) < 0.05, `after ${after} — a stale cache would still read 141.5`); }); // --- F8: hold the last known floor through a floor-data gap ---------------- @@ -1346,28 +1093,18 @@ test('display floor: a cold cell HOLDS the last floor that resolved for this con reportMeshFloorCell(30.2004, -97.6604, 140.2); // On the warm cell: clamped onto the floor as usual. const warm = _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7), - false, - 'held', - ); - assert.ok( - Math.abs(_floorCarto(warm).height - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7), false, 'held', ); + assert.ok(Math.abs(_floorCarto(warm).height - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05); // Taxis ~550 m onto a cell nothing has resolved, still at the geoid-ish // height the poll path handed it. Pre-fix this passed straight through — // buried. const cold = _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, 30.2054, 124.7), - false, - 'held', + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2054, 124.7), false, 'held', ); const h = _floorCarto(cold).height; - assert.ok( - Math.abs(h - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, - `held ${h} — a cold cell must hold 140.2 + lift, never the 124.7 it arrived with`, - ); + assert.ok(Math.abs(h - (140.2 + GROUND_FLOOR_LIFT_M)) < 0.05, + `held ${h} — a cold cell must hold 140.2 + lift, never the 124.7 it arrived with`); }); test('display floor: with NO prior anywhere the contact is left exactly where it is', () => { @@ -1375,17 +1112,8 @@ test('display floor: with NO prior anywhere the contact is left exactly where it _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'fresh', - ); - assert.equal( - out, - pos, - 'nothing measured anywhere ⇒ nothing invented — same object, no clamp', - ); + const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'fresh'); + assert.equal(out, pos, 'nothing measured anywhere ⇒ nothing invented — same object, no clamp'); }); test('display floor: a fresh contact on a cold cell adopts a resolved NEIGHBOUR', () => { @@ -1398,16 +1126,11 @@ test('display floor: a fresh contact on a cold cell adopts a resolved NEIGHBOUR' reportMeshFloorCell(30.2014, -97.6604, 141.0); reportMeshFloorCell(30.1994, -97.6604, 152.0); const out = _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7), - false, - 'neighbour', + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7), false, 'neighbour', ); const h = _floorCarto(out).height; - assert.ok( - Math.abs(h - (141.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `neighbour hold ${h} — expected the lower of the two resolved neighbours`, - ); + assert.ok(Math.abs(h - (141.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `neighbour hold ${h} — expected the lower of the two resolved neighbours`); }); // Drives a stationary contact at the production 80 ms fleet cadence and reports @@ -1415,28 +1138,16 @@ test('display floor: a fresh contact on a cold cell adopts a resolved NEIGHBOUR' // `mutate` runs before the tick at that index, so a target can be moved // mid-approach. function _driveEase(id, pos, fromMs, ticks, mutate = () => {}) { - let previous = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - id, - fromMs, - ), - ).height; + let previous = _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, pos, false, id, fromMs, + )).height; const opened = previous; let maxStepM = 0; for (let i = 1; i <= ticks; i += 1) { mutate(i); - const h = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - id, - fromMs + i * 80, - ), - ).height; + const h = _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, pos, false, id, fromMs + i * 80, + )).height; maxStepM = Math.max(maxStepM, previous - h); previous = h; } @@ -1455,18 +1166,10 @@ test('display floor: a hold released onto a LOWER floor eases down, never snaps' if (i === 1) reportMeshFloorCell(30.2004, -97.6604, 160.0); // its own cell resolves lower }); const totalDropM = r.opened - (160.0 + GROUND_FLOOR_LIFT_M); - assert.ok( - Math.abs(r.opened - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `opened at ${r.opened}`, - ); - assert.ok( - r.maxStepM <= totalDropM * 0.25, - `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m — a snap is the whole drop at once`, - ); - assert.ok( - Math.abs(r.settled - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `settled at ${r.settled}`, - ); + assert.ok(Math.abs(r.opened - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, `opened at ${r.opened}`); + assert.ok(r.maxStepM <= totalDropM * 0.25, + `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m — a snap is the whole drop at once`); + assert.ok(Math.abs(r.settled - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05, `settled at ${r.settled}`); }); test('display floor: a hold released onto a HIGHER floor rises immediately', () => { @@ -1474,26 +1177,17 @@ test('display floor: a hold released onto a HIGHER floor rises immediately', () _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const at = (ms) => - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'rise', - ms, - ), - ).height; + const at = (ms) => _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, pos, false, 'rise', ms, + )).height; reportMeshFloorCell(30.2014, -97.6604, 160.0); reportMeshFloorCell(30.1994, -97.6604, 190.0); assert.ok(Math.abs(at(1000) - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05); reportMeshFloorCell(30.2004, -97.6604, 205.0); // No easing UP: an eased rise is time spent under the mesh, which is the // whole failure this path exists to prevent. - assert.ok( - Math.abs(at(1001) - (205.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - 'the next tick is already on the higher floor', - ); + assert.ok(Math.abs(at(1001) - (205.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + 'the next tick is already on the higher floor'); }); // --- F9: the hold is a live state machine, not a one-shot ------------------ @@ -1507,24 +1201,17 @@ test('display floor: a STATIONARY unresolved contact still adopts a floor that a _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); - const at = (ms) => - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'parked-cold', - ms, - ); + const at = (ms) => _floorGroundedDisplayPositionForTest( + { onGround: true }, pos, false, 'parked-cold', ms, + ); assert.equal(at(1000), pos, 'nothing anywhere yet — passes through'); // The poll's warm batch lands the cells NEXT DOOR (this contact's own cell // is still cold, so only the hold chain can see them). reportMeshFloorCell(30.2014, -97.6604, 138.0); reportMeshFloorCell(30.1994, -97.6604, 160.0); const adopted = _floorCarto(at(1600)).height; // past the probe throttle - assert.ok( - Math.abs(adopted - (138.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `stationary contact ${adopted} — a memoized unresolved answer would still read 124.7`, - ); + assert.ok(Math.abs(adopted - (138.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `stationary contact ${adopted} — a memoized unresolved answer would still read 124.7`); }); test('display floor: a stationary contact FOLLOWS the neighbourhood as it fills in', () => { @@ -1532,16 +1219,9 @@ test('display floor: a stationary contact FOLLOWS the neighbourhood as it fills _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); - const at = (ms) => - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'upgrade', - ms, - ), - ).height; + const at = (ms) => _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, pos, false, 'upgrade', ms, + )).height; reportMeshFloorCell(30.2014, -97.6604, 200.0); reportMeshFloorCell(30.1994, -97.6604, 240.0); assert.ok(Math.abs(at(1000) - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05); @@ -1551,10 +1231,8 @@ test('display floor: a stationary contact FOLLOWS the neighbourhood as it fills reportMeshFloorCell(30.2004, -97.6614, 120.0); let h = 0; for (let ms = 1600; ms <= 6000; ms += 80) h = at(ms); - assert.ok( - Math.abs(h - (120.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `followed to ${h} — a borrowed floor must not outlive a better one`, - ); + assert.ok(Math.abs(h - (120.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `followed to ${h} — a borrowed floor must not outlive a better one`); }); test('display floor: an on_ground FLAP mid-takeoff-roll never dips below the runway', () => { @@ -1568,35 +1246,23 @@ test('display floor: an on_ground FLAP mid-takeoff-roll never dips below the run // the geoid. Deleting the hold on the airborne poll made that switch visible: // the contact came back grounded with no prior, outrunning its own floor // cells at 23 m/s, and sat under the runway (owner sighting, VIR138M). - const GROUND = -28.5, - GEOID = -32.5, - LON = -73.78; + const GROUND = -28.5, GEOID = -32.5, LON = -73.78; reportMeshFloorCell(40.64, LON, GROUND); // only the cell it STARTED on is warm let lat = 40.64; const grounded = []; for (let i = 0; i <= 22; i += 1) { const onGround = i !== 10; // one airborne poll mid-roll const out = _floorGroundedDisplayPositionForTest( - { onGround }, - Cesium.Cartesian3.fromDegrees(LON, lat, GEOID), - false, - 'VIR138M', - 1000 + i * 80, + { onGround }, Cesium.Cartesian3.fromDegrees(LON, lat, GEOID), false, 'VIR138M', 1000 + i * 80, ); if (onGround) grounded.push(_floorCarto(out).height); lat += 0.00021; // ~23 m per tick at 45 kt } const dipped = grounded.filter((h) => h < GROUND - 0.05); - assert.deepEqual( - dipped, - [], - `${dipped.length} of ${grounded.length} grounded ticks rendered below the runway`, - ); - assert.ok( - Math.abs(grounded[grounded.length - 1] - (GROUND + GROUND_FLOOR_LIFT_M)) < - 0.05, - 'and it is still standing on the floor it held, not on nothing', - ); + assert.deepEqual(dipped, [], + `${dipped.length} of ${grounded.length} grounded ticks rendered below the runway`); + assert.ok(Math.abs(grounded[grounded.length - 1] - (GROUND + GROUND_FLOOR_LIFT_M)) < 0.05, + 'and it is still standing on the floor it held, not on nothing'); }); test('display floor: the rehydration seed EXPIRES on its own age, with NO calls in between', () => { @@ -1606,45 +1272,20 @@ test('display floor: the rehydration seed EXPIRES on its own age, with NO calls const field = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); reportMeshFloorCell(30.2014, -97.6604, 200.0); reportMeshFloorCell(30.1994, -97.6604, 240.0); - assert.ok( - Math.abs( - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - field, - false, - 'longhaul', - 1000, - ), - ).height - - (200.0 + GROUND_FLOOR_LIFT_M), - ) < 0.05, - 'holding 200 m before departure', - ); + assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, field, false, 'longhaul', 1000, + )).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, 'holding 200 m before departure'); // ONE airborne tick parks the seed, and then this contact is not seen again // for 198 s: off the poll on a cruise, outside the corridor radius, tab // hidden. Nothing runs in between, so the retire path never gets a second // chance to expire it — the age has to be judged where it comes BACK. // An earlier cut checked expiry only on the retire path and cleared // `retiredMs` before validating it, so this reused the 200 m floor. - _floorGroundedDisplayPositionForTest( - { onGround: false }, - field, - false, - 'longhaul', - 2000, - ); + _floorGroundedDisplayPositionForTest({ onGround: false }, field, false, 'longhaul', 2000); _clearMeshFloorCellsForTest(); // a different airport: nothing warm here const back = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); assert.equal( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - back, - false, - 'longhaul', - 200000, - ), - back, + _floorGroundedDisplayPositionForTest({ onGround: true }, back, false, 'longhaul', 200000), back, 'an expired seed is gone — no floor is invented from a 198-second-old measurement', ); }); @@ -1656,40 +1297,15 @@ test('display floor: the seed expires the same way when the contact keeps report const field = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); reportMeshFloorCell(30.2014, -97.6604, 200.0); reportMeshFloorCell(30.1994, -97.6604, 240.0); - _floorGroundedDisplayPositionForTest( - { onGround: true }, - field, - false, - 'cruise', - 1000, - ); + _floorGroundedDisplayPositionForTest({ onGround: true }, field, false, 'cruise', 1000); // The other half of the same clock: an airborne contact that IS still being // ticked. The window is the window either way. - _floorGroundedDisplayPositionForTest( - { onGround: false }, - field, - false, - 'cruise', - 2000, - ); - _floorGroundedDisplayPositionForTest( - { onGround: false }, - field, - false, - 'cruise', - 200000, - ); + _floorGroundedDisplayPositionForTest({ onGround: false }, field, false, 'cruise', 2000); + _floorGroundedDisplayPositionForTest({ onGround: false }, field, false, 'cruise', 200000); _clearMeshFloorCellsForTest(); const back = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); assert.equal( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - back, - false, - 'cruise', - 201000, - ), - back, + _floorGroundedDisplayPositionForTest({ onGround: true }, back, false, 'cruise', 201000), back, 'no memory of the departure field survives the grace window', ); }); @@ -1705,28 +1321,10 @@ test('display floor: a rehydrated seed ranks BELOW fresh neighbour evidence', () setMeshFloorPreferred(true); const departed = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 50); reportMeshFloorCell(30.2004, -97.6604, 200.0); - assert.ok( - Math.abs( - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - departed, - false, - 'short-hop', - 1000, - ), - ).height - - (200.0 + GROUND_FLOOR_LIFT_M), - ) < 0.05, - 'standing on its own 200 m cell', - ); - _floorGroundedDisplayPositionForTest( - { onGround: false }, - departed, - false, - 'short-hop', - 2000, - ); + assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, departed, false, 'short-hop', 1000, + )).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, 'standing on its own 200 m cell'); + _floorGroundedDisplayPositionForTest({ onGround: false }, departed, false, 'short-hop', 2000); // Re-grounds 0.56 km away — INSIDE HELD_FLOOR_MAX_DRIFT_KM, so the drift // bound does not save us here; the tier order has to. Its own cell is cold, // but two adjacent cells have since resolved, and they are 100 m of ground. @@ -1734,19 +1332,11 @@ test('display floor: a rehydrated seed ranks BELOW fresh neighbour evidence', () reportMeshFloorCell(30.2044, -97.6604, 100.0); reportMeshFloorCell(30.2064, -97.6604, 105.0); const moved = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2054, 50); - const rendered = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - moved, - false, - 'short-hop', - 3000, - ), - ).height; - assert.ok( - Math.abs(rendered - (100.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `rendered at ${rendered} — a seed that outranked the neighbourhood read 201.5, 100 m in the air`, - ); + const rendered = _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, moved, false, 'short-hop', 3000, + )).height; + assert.ok(Math.abs(rendered - (100.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `rendered at ${rendered} — a seed that outranked the neighbourhood read 201.5, 100 m in the air`); }); test('display floor: with nothing fresh to contradict it, the seed still answers', () => { @@ -1758,34 +1348,14 @@ test('display floor: with nothing fresh to contradict it, the seed still answers // warmed either. Demoting the seed must not mean discarding it. const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 50); reportMeshFloorCell(30.2004, -97.6604, 200.0); - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'flap', - 1000, - ); - _floorGroundedDisplayPositionForTest( - { onGround: false }, - pos, - false, - 'flap', - 2000, - ); + _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'flap', 1000); + _floorGroundedDisplayPositionForTest({ onGround: false }, pos, false, 'flap', 2000); _clearMeshFloorCellsForTest(); // the whole neighbourhood goes cold - const held = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, 30.2014, 50), - false, - 'flap', - 3000, - ), - ).height; - assert.ok( - Math.abs(held - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `held ${held} — with no measurement anywhere the seed is the best thing available`, - ); + const held = _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2014, 50), false, 'flap', 3000, + )).height; + assert.ok(Math.abs(held - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `held ${held} — with no measurement anywhere the seed is the best thing available`); }); test('display floor: re-measuring its own cell makes a seeded contact authoritative again', () => { @@ -1794,56 +1364,24 @@ test('display floor: re-measuring its own cell makes a seeded contact authoritat setMeshFloorPreferred(true); const pad = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); reportMeshFloorCell(30.2004, -97.6604, 300.0); - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pad, - false, - 'relatch', - 1000, - ); - _floorGroundedDisplayPositionForTest( - { onGround: false }, - pad, - false, - 'relatch', - 2000, - ); + _floorGroundedDisplayPositionForTest({ onGround: true }, pad, false, 'relatch', 1000); + _floorGroundedDisplayPositionForTest({ onGround: false }, pad, false, 'relatch', 2000); // Re-grounds on its own, still-warm 300 m cell: that is a live reading, so the // demotion is over and the ordinary tier order applies again. - assert.ok( - Math.abs( - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pad, - false, - 'relatch', - 3000, - ), - ).height - - (300.0 + GROUND_FLOOR_LIFT_M), - ) < 0.05, - 'back on its own measured cell', - ); + assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, pad, false, 'relatch', 3000, + )).height - (300.0 + GROUND_FLOOR_LIFT_M)) < 0.05, 'back on its own measured cell'); // It now taxis 220 m onto a cold cell whose two adjacent cells read 100 m. An // own floor it measured while standing here outranks a borrowed one — the // seed flag has to have been cleared by that reading, or the contact drops // 200 m onto someone else's ground. reportMeshFloorCell(30.2014, -97.6604, 100.0); reportMeshFloorCell(30.2034, -97.6604, 100.0); - const taxied = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, 30.2024, 100), - false, - 'relatch', - 4000, - ), - ).height; - assert.ok( - Math.abs(taxied - (300.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `taxied at ${taxied} — a still-seeded contact would have taken the 100 m neighbours`, - ); + const taxied = _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2024, 100), false, 'relatch', 4000, + )).height; + assert.ok(Math.abs(taxied - (300.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `taxied at ${taxied} — a still-seeded contact would have taken the 100 m neighbours`); }); test('display floor: taking off and landing elsewhere does NOT inherit the old field floor', () => { @@ -1853,62 +1391,25 @@ test('display floor: taking off and landing elsewhere does NOT inherit the old f const field = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); reportMeshFloorCell(30.2004, -97.6604, 200.0); // 1. grounded on a 200 m floor. - assert.ok( - Math.abs( - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - field, - false, - 'trip', - 1000, - ), - ).height - - (200.0 + GROUND_FLOOR_LIFT_M), - ) < 0.05, - ); + assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, field, false, 'trip', 1000, + )).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05); // 2. it taxis ~330 m onto ground nothing has resolved, so 200 m is now being // HELD — this is the state that must not survive the trip. const heldAt = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2034, 124.7); - assert.ok( - Math.abs( - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - heldAt, - false, - 'trip', - 1500, - ), - ).height - - (200.0 + GROUND_FLOOR_LIFT_M), - ) < 0.05, - 'the hold is active before takeoff', - ); + assert.ok(Math.abs(_floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, heldAt, false, 'trip', 1500, + )).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, 'the hold is active before takeoff'); // 3. airborne — the hold belongs to the grounded-billboard regime, not to the // contact, so leaving that regime must retire it. - _floorGroundedDisplayPositionForTest( - { onGround: false }, - heldAt, - false, - 'trip', - 2000, - ); + _floorGroundedDisplayPositionForTest({ onGround: false }, heldAt, false, 'trip', 2000); // 4. lands at another field on a real 100 m floor. reportMeshFloorCell(31.5004, -97.6604, 100.0); - const landed = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, 31.5004, 60), - false, - 'trip', - 3000, - ), - ).height; - assert.ok( - Math.abs(landed - (100.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `landed at ${landed} — a stale hold would arrive at 201.5 and ease down from it`, - ); + const landed = _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 31.5004, 60), false, 'trip', 3000, + )).height; + assert.ok(Math.abs(landed - (100.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `landed at ${landed} — a stale hold would arrive at 201.5 and ease down from it`); }); test('display floor: a model-owned interval retires the hold too', () => { @@ -1917,47 +1418,21 @@ test('display floor: a model-owned interval retires the hold too', () => { setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); reportMeshFloorCell(30.2004, -97.6604, 200.0); - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'handoff', - 1000, - ); + _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'handoff', 1000); // It moves onto unresolved ground, so 200 m is being HELD. const heldAt = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2034, 124.7); - _floorGroundedDisplayPositionForTest( - { onGround: true }, - heldAt, - false, - 'handoff', - 1500, - ); + _floorGroundedDisplayPositionForTest({ onGround: true }, heldAt, false, 'handoff', 1500); // T7: the 3D model takes the visual. Our state is retired (nothing visual is // touched — groundSnap's one-shot input is untouched either way). - _floorGroundedDisplayPositionForTest( - { onGround: true }, - heldAt, - true, - 'handoff', - 2000, - ); + _floorGroundedDisplayPositionForTest({ onGround: true }, heldAt, true, 'handoff', 2000); // The billboard resumes over ground that has since resolved LOWER. A stale // heldActive would read this as a hold release and ease; it is a fresh start. reportMeshFloorCell(30.2034, -97.6604, 150.0); - const back = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - heldAt, - false, - 'handoff', - 3000, - ), - ).height; - assert.ok( - Math.abs(back - (150.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `resumed at ${back} — no ease, because there was no hold to release`, - ); + const back = _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, heldAt, false, 'handoff', 3000, + )).height; + assert.ok(Math.abs(back - (150.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `resumed at ${back} — no ease, because there was no hold to release`); }); test('display floor: a model handoff back onto a COLD cell takes the neighbourhood, not the seed', () => { @@ -1970,44 +1445,18 @@ test('display floor: a model handoff back onto a COLD cell takes the neighbourho // a floor it measured 330 m ago. Two adjacent cells have resolved at 150 m. const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 124.7); reportMeshFloorCell(30.2004, -97.6604, 200.0); - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'cold-handoff', - 1000, - ); + _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, 'cold-handoff', 1000); const heldAt = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2034, 124.7); - _floorGroundedDisplayPositionForTest( - { onGround: true }, - heldAt, - false, - 'cold-handoff', - 1500, - ); - _floorGroundedDisplayPositionForTest( - { onGround: true }, - heldAt, - true, - 'cold-handoff', - 2000, - ); + _floorGroundedDisplayPositionForTest({ onGround: true }, heldAt, false, 'cold-handoff', 1500); + _floorGroundedDisplayPositionForTest({ onGround: true }, heldAt, true, 'cold-handoff', 2000); _clearMeshFloorCellsForTest(); reportMeshFloorCell(30.2024, -97.6604, 150.0); reportMeshFloorCell(30.2044, -97.6604, 155.0); - const back = _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - heldAt, - false, - 'cold-handoff', - 3000, - ), - ).height; - assert.ok( - Math.abs(back - (150.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `resumed at ${back} — a seed that outranked the neighbourhood would resume at 201.5`, - ); + const back = _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, heldAt, false, 'cold-handoff', 3000, + )).height; + assert.ok(Math.abs(back - (150.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `resumed at ${back} — a seed that outranked the neighbourhood would resume at 201.5`); }); // --- F10: no contact waits on another -------------------------------------- @@ -2030,28 +1479,15 @@ test('display floor: every grounded contact is floored on the tick it asks', () } const heights = []; for (let i = 0; i < N; i += 1) { - heights.push( - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.66, latOf(i), 100), - false, - `q${i}`, - 1000, - ), - ).height, - ); + heights.push(_floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.66, latOf(i), 100), false, `q${i}`, 1000, + )).height); } const unfloored = heights - .map((h, i) => - Math.abs(h - (300 + i + GROUND_FLOOR_LIFT_M)) < 0.05 ? null : i, - ) + .map((h, i) => (Math.abs(h - (300 + i + GROUND_FLOOR_LIFT_M)) < 0.05 ? null : i)) .filter((i) => i != null); - assert.deepEqual( - unfloored, - [], - 'a global budget would leave the tail of the fleet on the geoid for later ticks', - ); + assert.deepEqual(unfloored, [], + 'a global budget would leave the tail of the fleet on the geoid for later ticks'); }); test('display floor: a re-probe that finds a LOWER neighbour eases down, never snaps', () => { @@ -2068,14 +1504,9 @@ test('display floor: a re-probe that finds a LOWER neighbour eases down, never s if (i === 8) reportMeshFloorCell(30.2004, -97.6614, 120.0); // past the 500 ms probe throttle }); const totalDropM = r.opened - target; - assert.ok( - r.maxStepM <= totalDropM * 0.25, - `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m`, - ); - assert.ok( - Math.abs(r.settled - target) < 0.05, - `settled at ${r.settled}, expected ${target}`, - ); + assert.ok(r.maxStepM <= totalDropM * 0.25, + `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m`); + assert.ok(Math.abs(r.settled - target) < 0.05, `settled at ${r.settled}, expected ${target}`); }); test('display floor: a SECOND lower re-latch mid-approach retargets without a jump', () => { @@ -2095,14 +1526,9 @@ test('display floor: a SECOND lower re-latch mid-approach retargets without a ju if (i === 16) reportMeshFloorCell(30.2004, -97.6594, 100.0); // -> 100 m, mid-approach }); const totalDropM = r.opened - target; - assert.ok( - r.maxStepM <= totalDropM * 0.25, - `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m — the retarget must not be a seam`, - ); - assert.ok( - Math.abs(r.settled - target) < 0.05, - `settled at ${r.settled}, expected ${target}`, - ); + assert.ok(r.maxStepM <= totalDropM * 0.25, + `largest single-tick drop ${r.maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m — the retarget must not be a seam`); + assert.ok(Math.abs(r.settled - target) < 0.05, `settled at ${r.settled}, expected ${target}`); }); test('display floor: a DELAYED tick cannot close more of the gap than a prompt one', () => { @@ -2110,16 +1536,9 @@ test('display floor: a DELAYED tick cannot close more of the gap than a prompt o _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); - const at = (ms) => - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'stall', - ms, - ), - ).height; + const at = (ms) => _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, pos, false, 'stall', ms, + )).height; reportMeshFloorCell(30.2014, -97.6604, 400.0); reportMeshFloorCell(30.1994, -97.6604, 440.0); // borrowed floor = 400 m const opened = at(1000); @@ -2138,10 +1557,8 @@ test('display floor: a DELAYED tick cannot close more of the gap than a prompt o maxStepM = Math.max(maxStepM, previous - h); previous = h; } - assert.ok( - maxStepM <= totalDropM * 0.25, - `largest step ${maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m across a stalled schedule`, - ); + assert.ok(maxStepM <= totalDropM * 0.25, + `largest step ${maxStepM.toFixed(1)} m of ${totalDropM.toFixed(1)} m across a stalled schedule`); assert.ok(previous < opened, 'and it is still making progress downward'); }); @@ -2152,16 +1569,9 @@ test('display floor: a floor rising MID-APPROACH is taken whole, on that tick', const pos = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 100); reportMeshFloorCell(30.2014, -97.6604, 400.0); reportMeshFloorCell(30.1994, -97.6604, 440.0); // borrowed floor = 400 m - const at = (ms) => - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - pos, - false, - 'riseMid', - ms, - ), - ).height; + const at = (ms) => _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, pos, false, 'riseMid', ms, + )).height; at(1000); reportMeshFloorCell(30.2004, -97.6604, 150.0); // own cell resolves far below let h = 0; @@ -2172,10 +1582,8 @@ test('display floor: a floor rising MID-APPROACH is taken whole, on that tick', _clearMeshFloorCellsForTest(); reportMeshFloorCell(30.2004, -97.6604, 500.0); const risen = at(1000 + 5 * 80); - assert.ok( - Math.abs(risen - (500.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - `rose to ${risen} — an eased rise is time spent under the mesh`, - ); + assert.ok(Math.abs(risen - (500.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + `rose to ${risen} — an eased rise is time spent under the mesh`); }); test('display floor: a held floor is dropped once the contact leaves the ground it describes', () => { @@ -2183,38 +1591,18 @@ test('display floor: a held floor is dropped once the contact leaves the ground _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 200.0); - const at = (lat, ms) => - _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, lat, 124.7), - false, - 'drift', - ms, - ); - assert.ok( - Math.abs( - _floorCarto(at(30.2004, 1000)).height - (200.0 + GROUND_FLOOR_LIFT_M), - ) < 0.05, + const at = (lat, ms) => _floorGroundedDisplayPositionForTest( + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, lat, 124.7), false, 'drift', ms, ); + assert.ok(Math.abs(_floorCarto(at(30.2004, 1000)).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05); // ~670 m on: cold cell, but that is inside one poll's rollout. Hold stands. - assert.ok( - Math.abs( - _floorCarto(at(30.2064, 2000)).height - (200.0 + GROUND_FLOOR_LIFT_M), - ) < 0.05, - 'inside the drift bound the held floor still describes the ground', - ); + assert.ok(Math.abs(_floorCarto(at(30.2064, 2000)).height - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + 'inside the drift bound the held floor still describes the ground'); // ~3.3 km on: the held value is no longer a measurement of anywhere this // contact has been, so it is dropped rather than stretched. const far = Cesium.Cartesian3.fromDegrees(-97.6604, 30.2304, 124.7); assert.equal( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - far, - false, - 'drift', - 3000, - ), - far, + _floorGroundedDisplayPositionForTest({ onGround: true }, far, false, 'drift', 3000), far, 'past the drift bound: back to no clamp, not a stretched one', ); }); @@ -2228,21 +1616,12 @@ test('display floor: an ordinary cell-to-cell floor DROP keeps its existing timi // untouched by this round. reportMeshFloorCell(30.2004, -97.6604, 200.0); reportMeshFloorCell(30.2044, -97.6604, 160.0); - const at = (lat, ms) => - _floorCarto( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, lat, 124.7), - false, - 'plain', - ms, - ), - ).height; + const at = (lat, ms) => _floorCarto(_floorGroundedDisplayPositionForTest( + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, lat, 124.7), false, 'plain', ms, + )).height; assert.ok(Math.abs(at(30.2004, 1000) - (200.0 + GROUND_FLOOR_LIFT_M)) < 0.05); - assert.ok( - Math.abs(at(30.2044, 1050) - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05, - 'the very next tick is on the new cell — no ease was introduced here', - ); + assert.ok(Math.abs(at(30.2044, 1050) - (160.0 + GROUND_FLOOR_LIFT_M)) < 0.05, + 'the very next tick is on the new cell — no ease was introduced here'); }); test('display floor: the hold never applies to an airborne contact or a modelled one', () => { @@ -2254,23 +1633,11 @@ test('display floor: the hold never applies to an airborne contact or a modelled _floorGroundedDisplayPositionForTest({ onGround: true }, on, false, 'gates'); const cold = Cesium.Cartesian3.fromDegrees(-97.6704, 30.2104, 124.7); assert.equal( - _floorGroundedDisplayPositionForTest( - { onGround: false }, - cold, - false, - 'gates', - ), - cold, + _floorGroundedDisplayPositionForTest({ onGround: false }, cold, false, 'gates'), cold, 'airborne heights belong to the fix-time clamp, held floor or not', ); assert.equal( - _floorGroundedDisplayPositionForTest( - { onGround: true }, - cold, - true, - 'gates', - ), - cold, + _floorGroundedDisplayPositionForTest({ onGround: true }, cold, true, 'gates'), cold, 'T7: a model-owned contact keeps its billboard datum, held floor or not', ); }); @@ -2281,21 +1648,11 @@ test('display floor: two contacts on the same cell get their own outputs', () => setMeshFloorPreferred(true); reportMeshFloorCell(30.2004, -97.6604, 140); const a = _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 120), - false, - 'A', + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.6604, 30.2004, 120), false, 'A', ); const b = _floorGroundedDisplayPositionForTest( - { onGround: true }, - Cesium.Cartesian3.fromDegrees(-97.66041, 30.20041, 121), - false, - 'B', - ); - assert.notEqual( - a, - b, - 'a shared scratch would hand both contacts the same object', + { onGround: true }, Cesium.Cartesian3.fromDegrees(-97.66041, 30.20041, 121), false, 'B', ); + assert.notEqual(a, b, 'a shared scratch would hand both contacts the same object'); assert.ok(Math.abs(_floorCarto(a).height - _floorCarto(b).height) < 0.05); }); diff --git a/src/data/militaryFlights.test.mjs b/src/data/militaryFlights.test.mjs index cd29c1ebd..9e6e4aea8 100644 --- a/src/data/militaryFlights.test.mjs +++ b/src/data/militaryFlights.test.mjs @@ -24,12 +24,8 @@ test('share-Follow absence requires an accepted adsb.lol snapshot', async () => (await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234')).status, 'source-unavailable', ); - _setMilitaryTrackingRefreshOutcomeForTest({ - status: 'accepted', - ids: ['different'], - }); - const missing = - await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); + _setMilitaryTrackingRefreshOutcomeForTest({ status: 'accepted', ids: ['different'] }); + const missing = await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); assert.equal(missing.status, 'missing'); assert.equal(missing.reason, 'target-absent-from-snapshot'); }); @@ -105,14 +101,8 @@ test('military analyst record: military is ALWAYS true, routes/country always nu }); test('military analyst record: no callsign falls back to registration, then icao24', () => { - assert.equal( - mapAnalystRecord('ae01ce', { ...FULL_INFO, callsign: '' }).id, - '05-8152', - ); - assert.equal( - mapAnalystRecord('ae01ce', { callsign: '', registration: ' ' }).id, - 'ae01ce', - ); + assert.equal(mapAnalystRecord('ae01ce', { ...FULL_INFO, callsign: '' }).id, '05-8152'); + assert.equal(mapAnalystRecord('ae01ce', { callsign: '', registration: ' ' }).id, 'ae01ce'); }); test('military analyst record: empty info yields nulls, never NaN/undefined', () => { @@ -121,8 +111,7 @@ test('military analyst record: empty info yields nulls, never NaN/undefined', () assert.equal(r.onGround, false); for (const [key, value] of Object.entries(r)) { assert.notEqual(value, undefined, `${key} must not be undefined`); - if (typeof value === 'number') - assert.ok(Number.isFinite(value), `${key} must not be NaN`); + if (typeof value === 'number') assert.ok(Number.isFinite(value), `${key} must not be NaN`); } }); @@ -134,17 +123,12 @@ test('military analyst record: output is JSON-safe (no Cesium types)', () => { test('military first update forwards caller cancellation into the feed request', async () => { const realFetch = globalThis.fetch; let observedSignal = null; - globalThis.fetch = (_url, options = {}) => - new Promise((_resolve, reject) => { - observedSignal = options.signal; - options.signal?.addEventListener( - 'abort', - () => { - reject(new DOMException('aborted', 'AbortError')); - }, - { once: true }, - ); - }); + globalThis.fetch = (_url, options = {}) => new Promise((_resolve, reject) => { + observedSignal = options.signal; + options.signal?.addEventListener('abort', () => { + reject(new DOMException('aborted', 'AbortError')); + }, { once: true }); + }); try { const controller = new AbortController(); const work = militaryFlightsLayer.update({}, { signal: controller.signal }); @@ -174,22 +158,11 @@ test('nonempty adsb.lol payload with zero usable rows cannot prove share target json: async () => ({ ac: [null, {}, { hex: 'ae1234' }] }), }); try { - await militaryFlightsLayer.update({ - camera: { positionCartographic: null }, - scene: {}, - }); - const resolution = - await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); + await militaryFlightsLayer.update({ camera: { positionCartographic: null }, scene: {} }); + const resolution = await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); assert.equal(resolution.status, 'source-unavailable'); - assert.match( - militaryFlightsLayer.getStats().error, - /Malformed adsb\.lol aircraft rows/, - ); - assert.equal( - militaryFlightsLayer.getAnalystRecords().length, - 1, - 'warm aircraft data is preserved', - ); + assert.match(militaryFlightsLayer.getStats().error, /Malformed adsb\.lol aircraft rows/); + assert.equal(militaryFlightsLayer.getAnalystRecords().length, 1, 'warm aircraft data is preserved'); } finally { globalThis.fetch = realFetch; } @@ -235,26 +208,21 @@ test('military poll refreshes tracked callsign/altitude/kts and marks a missed p ok: true, status: 200, json: async () => ({ - ac: - poll++ === 0 - ? [ - { - hex: icao24, - lon: -96.9, - lat: 31.1, - alt_baro: 28_000, - alt_geom: 28_100, - track: 95, - gs: 400, - seen: 0, - seen_pos: 0, - flight: 'RCH451 ', - t: 'C17', - r: '05-8152', - ownOp: 'United States Air Force', - }, - ] - : [], + ac: poll++ === 0 ? [{ + hex: icao24, + lon: -96.9, + lat: 31.1, + alt_baro: 28_000, + alt_geom: 28_100, + track: 95, + gs: 400, + seen: 0, + seen_pos: 0, + flight: 'RCH451 ', + t: 'C17', + r: '05-8152', + ownOp: 'United States Air Force', + }] : [], }), }); @@ -279,10 +247,7 @@ test('cached military snapshots retain their observation time and expose stale f _setTrackedMilitaryRefreshStateForTest({ icao24: 'ae01ce', entity: { gevLabelModel: { title: 'RCH451', details: [] } }, - billboard: { - position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000), - show: false, - }, + billboard: { position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000), show: false }, billboardCollection: { show: false, remove() {} }, viewer, history, @@ -320,18 +285,11 @@ test('cached military snapshots retain their observation time and expose stale f await militaryFlightsLayer.update(viewer); assert.equal(history.length, 1); assert.equal(history[0].epochMs, receivedAt - 2000); - for (const [status, age] of [ - ['HIT', 5000], - ['STALE', 45000], - ]) { + for (const [status, age] of [['HIT', 5000], ['STALE', 45000]]) { cache = status; now = receivedAt + age; await militaryFlightsLayer.update(viewer); - assert.equal( - history.length, - 1, - 'replayed positions cannot create a new fix', - ); + assert.equal(history.length, 1, 'replayed positions cannot create a new fix'); assert.equal(militaryFlightsLayer.getStats().lastUpdate, receivedAt); assert.equal(militaryFlightsLayer.getStats().stale, status === 'STALE'); assert.equal(militaryFlightsLayer.getStats().error, null); @@ -358,18 +316,14 @@ test('real military track path creates no native label and publishes every cache trackedEntityChanged, clock: { currentTime: now }, camera: { - cancelFlight() { - cancelledFlights += 1; - }, + cancelFlight() { cancelledFlights += 1; }, position: new Cesium.Cartesian3(0, -10_000, 7_000), positionWC: new Cesium.Cartesian3(0, -10_000, 7_000), direction: Cesium.Cartesian3.UNIT_Y, transform: Cesium.Matrix4.IDENTITY, viewMatrix: Cesium.Matrix4.IDENTITY, frustum: { projectionMatrix: Cesium.Matrix4.IDENTITY }, - lookAtTransform() { - appliedFrames += 1; - }, + lookAtTransform() { appliedFrames += 1; }, }, scene: { canvas: { clientWidth: 1600, clientHeight: 900 }, @@ -408,12 +362,9 @@ test('real military track path creates no native label and publishes every cache const realFetch = globalThis.fetch; globalThis.window = new EventTarget(); const selectionEvents = []; - globalThis.window.addEventListener( - 'gev:awareness-subject-selected', - (event) => { - selectionEvents.push(event.detail); - }, - ); + globalThis.window.addEventListener('gev:awareness-subject-selected', (event) => { + selectionEvents.push(event.detail); + }); globalThis.fetch = async () => ({ ok: false }); _setTrackedOverlayHostForTest(host); try { @@ -425,15 +376,13 @@ test('real military track path creates no native label and publishes every cache billboardCollection: { show: true, remove() {} }, viewer, tracked: false, - history: [ - { - time: now, - epochMs: Date.now(), - position, - velocity: 231.5, - track: 92.1, - }, - ], + history: [{ + time: now, + epochMs: Date.now(), + position, + velocity: 231.5, + track: 92.1, + }], meta: { ...FULL_INFO, type: 'C17', @@ -443,72 +392,34 @@ test('real military track path creates no native label and publishes every cache }, }); - assert.equal( - militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), - true, - ); + assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), true); const entity = viewer.trackedEntity; - assert.ok( - entity instanceof Cesium.Entity, - 'trackById must create the real Cesium entity', - ); + assert.ok(entity instanceof Cesium.Entity, 'trackById must create the real Cesium entity'); assert.equal(entity.label, undefined); - assert.ok( - entities.values.every((candidate) => candidate.label === undefined), - ); + assert.ok(entities.values.every((candidate) => candidate.label === undefined)); assert.deepEqual(entity.gevLabelModel, { title: 'RCH451', - details: ['C17 · 05-8152', 'United States Air Force · 28000 ft · 450 kt'], + details: [ + 'C17 · 05-8152', + 'United States Air Force · 28000 ft · 450 kt', + ], accent: '#ffd166', }); viewer.scene.preUpdate.raiseEvent(); const initialAppliedFrames = appliedFrames; const initialCancelledFlights = cancelledFlights; - assert.equal( - militaryFlightsLayer.trackById(icao24, { origin: 'user' }), - true, - ); - assert.equal( - cancelledFlights, - initialCancelledFlights, - 'ordinary repeated tracking stays camera-idempotent', - ); - assert.equal( - selectionEvents.at(-1)?.origin, - 'user', - 'same-target selection upgrades durable authority', - ); + assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'user' }), true); + assert.equal(cancelledFlights, initialCancelledFlights, 'ordinary repeated tracking stays camera-idempotent'); + assert.equal(selectionEvents.at(-1)?.origin, 'user', 'same-target selection upgrades durable authority'); assert.equal(entity.gevSelectionOrigin, 'user'); - assert.equal( - militaryFlightsLayer.refocusTrackedById('different-flight'), - false, - ); - assert.equal( - militaryFlightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), - true, - ); - assert.equal( - selectionEvents.at(-1)?.origin, - 'voice', - 'same-target refocus forwards explicit authority', - ); + assert.equal(militaryFlightsLayer.refocusTrackedById('different-flight'), false); + assert.equal(militaryFlightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), true); + assert.equal(selectionEvents.at(-1)?.origin, 'voice', 'same-target refocus forwards explicit authority'); assert.equal(militaryFlightsLayer.refocusTrackedById(icao24), true); viewer.scene.preUpdate.raiseEvent(); - assert.equal( - viewer.trackedEntity, - entity, - 'refocus must retain the exact tracked entity', - ); - assert.equal( - appliedFrames, - initialAppliedFrames + 1, - 'repeated refocus keeps one camera-frame owner', - ); - assert.equal( - cancelledFlights, - initialCancelledFlights + 2, - 'only explicit refocus requests cancel camera flights', - ); + assert.equal(viewer.trackedEntity, entity, 'refocus must retain the exact tracked entity'); + assert.equal(appliedFrames, initialAppliedFrames + 1, 'repeated refocus keeps one camera-frame owner'); + assert.equal(cancelledFlights, initialCancelledFlights + 2, 'only explicit refocus requests cancel camera flights'); const publication = publications.at(-1); assert.equal(publication.sourceId, 'tracked'); const entry = publication.entries[0]; @@ -519,40 +430,23 @@ test('real military track path creates no native label and publishes every cache const display = entity.position.getValue(now); assert.ok(display, 'tracked position callback must seed its frame cache'); - assert.equal( - entry.position(), - display, - 'host must reuse the exact cached display Cartesian', - ); + assert.equal(entry.position(), display, 'host must reuse the exact cached display Cartesian'); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late001' }, { origin: 'share-restore' }, ); assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late001'); - assert.equal( - militaryFlightsLayer.trackById(icao24, { origin: 'user' }), - true, - ); - assert.equal( - _pendingMilitaryTrackingRestoreForTest(), - null, - 'new user selection cancels stale restore', - ); + assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'user' }), true); + assert.equal(_pendingMilitaryTrackingRestoreForTest(), null, 'new user selection cancels stale restore'); _addMilitaryTrackingCandidateForTest({ icao24: 'late001', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000) }, meta: { ...FULL_INFO, callsign: 'LATE1', rawLat: 31.1, rawLon: -96.9 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal( - militaryFlightsLayer.getParams().selectedMilitaryTrackingId, - icao24, - ); + assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, icao24); militaryFlightsLayer.stopTracking(); militaryFlightsLayer.setParams( @@ -561,25 +455,15 @@ test('real military track path creates no native label and publishes every cache ); assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late002'); militaryFlightsLayer.stopTracking({ origin: 'user' }); - assert.equal( - _pendingMilitaryTrackingRestoreForTest(), - null, - 'explicit clear cancels stale restore', - ); + assert.equal(_pendingMilitaryTrackingRestoreForTest(), null, 'explicit clear cancels stale restore'); _addMilitaryTrackingCandidateForTest({ icao24: 'late002', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000) }, meta: { ...FULL_INFO, callsign: 'LATE2', rawLat: 31.2, rawLon: -96.8 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal( - militaryFlightsLayer.getParams().selectedMilitaryTrackingId, - null, - ); + assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, null); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late003' }, @@ -594,18 +478,12 @@ test('real military track path creates no native label and publishes every cache ); _addMilitaryTrackingCandidateForTest({ icao24: 'late003', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000) }, meta: { ...FULL_INFO, callsign: 'LATE3', rawLat: 30.6, rawLon: -97.3 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal( - militaryFlightsLayer.getParams().selectedMilitaryTrackingId, - null, - ); + assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, null); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late004' }, @@ -614,27 +492,18 @@ test('real military track path creates no native label and publishes every cache assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late004'); _addMilitaryTrackingCandidateForTest({ icao24: 'late004', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000) }, meta: { ...FULL_INFO, callsign: 'LATE4', rawLat: 30.7, rawLon: -97.2 }, history: [], }); assert.equal(_applyPendingMilitaryTrackingRestoreForTest(), true); - assert.equal( - militaryFlightsLayer.getParams().selectedMilitaryTrackingId, - 'late004', - ); + assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, 'late004'); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late005' }, { origin: 'local-restore' }, ); - assert.equal( - militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), - true, - ); + assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), true); assert.equal( _pendingMilitaryTrackingRestoreForTest(), 'late005', @@ -642,18 +511,12 @@ test('real military track path creates no native label and publishes every cache ); _addMilitaryTrackingCandidateForTest({ icao24: 'late005', - billboard: { - ...billboard, - position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000), - }, + billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000) }, meta: { ...FULL_INFO, callsign: 'LATE5', rawLat: 31.4, rawLon: -96.6 }, history: [], }); assert.equal(_applyPendingMilitaryTrackingRestoreForTest(), true); - assert.equal( - militaryFlightsLayer.getParams().selectedMilitaryTrackingId, - 'late005', - ); + assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, 'late005'); } finally { militaryFlightsLayer.stopTracking(); viewer.scene.preUpdate.raiseEvent(); @@ -698,47 +561,21 @@ test('a callsign-less contact is findable by the tail number the app displays', // The identity analyst_query hands back for this contact. const analystId = mapAnalystRecord(icao24, { - callsign: null, - registration: '6606', - rawLat: 40.71, - rawLon: -74.01, + callsign: null, registration: '6606', rawLat: 40.71, rawLon: -74.01, }).id; - assert.equal( - analystId, - '6606', - 'the analyst names a callsign-less contact by its tail', - ); + assert.equal(analystId, '6606', 'the analyst names a callsign-less contact by its tail'); const found = militaryFlightsLayer.findByQuery(analystId); - assert.equal( - found?.icao24, - icao24, - 'and the tracker must resolve that same identity', - ); - assert.equal( - militaryFlightsLayer.findByQuery('660')?.icao24, - icao24, - 'prefix match too', - ); - assert.equal( - militaryFlightsLayer.findByQuery(icao24)?.icao24, - icao24, - 'hex still wins outright', - ); + assert.equal(found?.icao24, icao24, 'and the tracker must resolve that same identity'); + assert.equal(militaryFlightsLayer.findByQuery('660')?.icao24, icao24, 'prefix match too'); + assert.equal(militaryFlightsLayer.findByQuery(icao24)?.icao24, icao24, 'hex still wins outright'); }); test('the analyst record carries the hex key the tracker keys on', () => { const record = mapAnalystRecord('ae7f01', { - callsign: null, - registration: '6606', - rawLat: 40.71, - rawLon: -74.01, + callsign: null, registration: '6606', rawLat: 40.71, rawLon: -74.01, }); - assert.equal( - record.icao24, - 'ae7f01', - 'a resolvable key must ride along with the display label', - ); + assert.equal(record.icao24, 'ae7f01', 'a resolvable key must ride along with the display label'); }); test('a registration never outranks another contact’s exact callsign', () => { @@ -751,35 +588,18 @@ test('a registration never outranks another contact’s exact callsign', () => { show: true, }); const base = { - type: 'C17', - operator: 'USAF', - altitudeFt: 25000, - speedMps: 180, - track: 90, - klass: 'widebody', - onGround: false, + type: 'C17', operator: 'USAF', altitudeFt: 25000, speedMps: 180, + track: 90, klass: 'widebody', onGround: false, }; // Registered FIRST, so insertion order favours it. _addMilitaryTrackingCandidateForTest({ icao24: 'ae0aa1', - meta: { - ...base, - callsign: null, - registration: 'ZULU777', - rawLat: 31.0, - rawLon: -97.0, - }, + meta: { ...base, callsign: null, registration: 'ZULU777', rawLat: 31.0, rawLon: -97.0 }, billboard: billboardAt(-97.0, 31.0), }); _addMilitaryTrackingCandidateForTest({ icao24: 'ae0bb2', - meta: { - ...base, - callsign: 'ZULU777', - registration: '05-9999', - rawLat: 31.2, - rawLon: -97.2, - }, + meta: { ...base, callsign: 'ZULU777', registration: '05-9999', rawLat: 31.2, rawLon: -97.2 }, billboard: billboardAt(-97.2, 31.2), }); @@ -802,49 +622,24 @@ test('two contacts matching at the same strength resolve deterministically', () show: true, }); const base = { - type: 'C130', - operator: 'USAF', - altitudeFt: 21000, - speedMps: 150, - track: 45, - klass: 'turboprop', - onGround: false, + type: 'C130', operator: 'USAF', altitudeFt: 21000, speedMps: 150, + track: 45, klass: 'turboprop', onGround: false, }; // Both registrations begin with QQ7 — a same-tier prefix tie. Registered // highest-hex first so insertion order would pick the wrong one. _addMilitaryTrackingCandidateForTest({ icao24: 'aef002', - meta: { - ...base, - callsign: null, - registration: 'QQ7-222', - rawLat: 32.0, - rawLon: -98.0, - }, + meta: { ...base, callsign: null, registration: 'QQ7-222', rawLat: 32.0, rawLon: -98.0 }, billboard: billboardAt(-98.0, 32.0), }); _addMilitaryTrackingCandidateForTest({ icao24: 'aef001', - meta: { - ...base, - callsign: null, - registration: 'QQ7-111', - rawLat: 32.1, - rawLon: -98.1, - }, + meta: { ...base, callsign: null, registration: 'QQ7-111', rawLat: 32.1, rawLon: -98.1 }, billboard: billboardAt(-98.1, 32.1), }); const first = militaryFlightsLayer.findByQuery('QQ7')?.icao24; const second = militaryFlightsLayer.findByQuery('qq7')?.icao24; - assert.equal( - first, - 'aef001', - 'the stable key (lowest hex) wins, not the feed order', - ); - assert.equal( - second, - first, - 'and the same query resolves the same way every time', - ); + assert.equal(first, 'aef001', 'the stable key (lowest hex) wins, not the feed order'); + assert.equal(second, first, 'and the same query resolves the same way every time'); }); diff --git a/src/layers/flights/rendering.js b/src/layers/flights/rendering.js index e84e05e01..a58587100 100644 --- a/src/layers/flights/rendering.js +++ b/src/layers/flights/rendering.js @@ -58,23 +58,23 @@ export function createRendering({ /** True while this contact's latest poll broadcast an emergency code. */ - function _isEmergency(icao24) { - return isEmergency(flightState.records.data.get(icao24)?.emergency); + function _isEmergency(icao24, info = flightState.records.data.get(icao24)) { + return isEmergency(info?.emergency); } /** Fleet (untracked) billboard tint: red while broadcasting an emergency, * amber for known-military, white otherwise. * Ground traffic gets NO special tint (owner verdict 2026-07-03 field test). */ - function _fleetBillboardColor(icao24) { - if (_isEmergency(icao24)) return EMERGENCY_TINT; + function _fleetBillboardColor(icao24, info) { + if (_isEmergency(icao24, info)) return EMERGENCY_TINT; return isMilitaryIcao(icao24) ? MIL_TINT : Cesium.Color.WHITE; } /** Cockpit far-contact dot tint: the emergency red outranks the category tint. */ - function _cockpitContactColor(icao24) { - if (_isEmergency(icao24)) return EMERGENCY_TINT; + function _cockpitContactColor(icao24, info) { + if (_isEmergency(icao24, info)) return EMERGENCY_TINT; return isMilitaryIcao(icao24) ? MIL_TINT : COCKPIT_CIVILIAN_COLOR; } @@ -1024,8 +1024,8 @@ export function createRendering({ flightState._cockpitNearContacts.has(icao24); const layerBaseColor = flightState._cockpitContactMode && !isCockpitNear - ? _cockpitContactColor(icao24) - : _fleetBillboardColor(icao24); + ? _cockpitContactColor(icao24, info) + : _fleetBillboardColor(icao24, info); const baseColor = layerBaseColor; const treatment = applyAircraftBillboardTreatment({ billboard: bb, diff --git a/src/layers/military/rendering.js b/src/layers/military/rendering.js index 48265a777..2beb81bc5 100644 --- a/src/layers/military/rendering.js +++ b/src/layers/military/rendering.js @@ -197,10 +197,11 @@ export function createRendering({ /** Fleet (untracked) tint: red while the latest poll broadcast an emergency, * amber otherwise. Cockpit far-contact dots use it too. */ - function _fleetBillboardColor(icao24) { - return isEmergency(flightState.records.data.get(icao24)?.emergency) - ? EMERGENCY_ICON_COLOR - : MIL_ICON_COLOR; + function _fleetBillboardColor( + icao24, + info = flightState.records.data.get(icao24), + ) { + return isEmergency(info?.emergency) ? EMERGENCY_ICON_COLOR : MIL_ICON_COLOR; } /** The FLEET's 3D-model regime: models3d enabled AND the camera zoomed in past the altitude @@ -976,7 +977,7 @@ export function createRendering({ const isCockpitNear = flightState._cockpitContactMode && flightState._cockpitNearContacts.has(icao24); - const baseColor = _fleetBillboardColor(icao24); + const baseColor = _fleetBillboardColor(icao24, info); const treatment = applyAircraftBillboardTreatment({ billboard: bb, baseScale: diff --git a/src/layers/military/snapshotRenderer.js b/src/layers/military/snapshotRenderer.js index 58eb57972..b9e54c173 100644 --- a/src/layers/military/snapshotRenderer.js +++ b/src/layers/military/snapshotRenderer.js @@ -7,12 +7,7 @@ import { } from '../../data/motionModel.js'; import { aircraftIcon } from '../../data/aircraftIcons.js'; import { POSITION_HISTORY_LIMIT } from './recordPolicy.js'; -import { - BILLBOARD_SCALE, - GROUND_SCALE, - TRACKED_ICON_COLOR, - MIL_ICON_COLOR, -} from './policy.js'; +import { BILLBOARD_SCALE, GROUND_SCALE, TRACKED_ICON_COLOR } from './policy.js'; /** Apply military record changes to Cesium resources and existing follow operations. */ export function createMilitarySnapshotRenderer({ @@ -161,7 +156,9 @@ export function createMilitarySnapshotRenderer({ // Screen-projected rotation lands on the next fleet tick. rotation: 0, alignedAxis: Cesium.Cartesian3.ZERO, - color: isTracked ? TRACKED_ICON_COLOR : MIL_ICON_COLOR, + color: isTracked + ? TRACKED_ICON_COLOR + : rendering._fleetBillboardColor(icao24, meta), sizeInMeters: false, scaleByDistance: rendering._normalBillboardScaleByDistance(), // Grounded/near-surface planes sit at/below the photoreal tile diff --git a/src/layers/military/tracking.js b/src/layers/military/tracking.js index b0cfb0368..5723ac39e 100644 --- a/src/layers/military/tracking.js +++ b/src/layers/military/tracking.js @@ -21,7 +21,6 @@ import { TRAIL_COLOR, BILLBOARD_SCALE, GROUND_SCALE, - MIL_ICON_COLOR, AMBER_TRANSPARENT, TRACKED_ICON_COLOR, } from './policy.js'; @@ -780,12 +779,16 @@ export function createTracking({ bb.width = 20; bb.height = 20; // Ground-aware restore (a plane untracked while taxiing comes back at - // ground scale; tint is full amber on the ground and in the air). + // ground scale; the fleet tint is amber, or red while broadcasting an + // emergency, on the ground and in the air). bb.scale = BILLBOARD_SCALE * (CLASS_SCALE_2D[meta?.klass] || 1) * (meta?.onGround ? GROUND_SCALE : 1); - bb.color = MIL_ICON_COLOR; + bb.color = parts.rendering._fleetBillboardColor( + flightState._trackedIcao, + meta, + ); bb.rotation = flightState._lastTrackedRotation; } flightState._lastCamPoseSig = ''; // force a fleet rotation pass on the next tick diff --git a/src/ui/styles/cockpit.css b/src/ui/styles/cockpit.css index df4e599e4..bed0c1772 100644 --- a/src/ui/styles/cockpit.css +++ b/src/ui/styles/cockpit.css @@ -43,6 +43,8 @@ #cockpit-hud { --cockpit-accent: #00dcff; + /* Matches EMERGENCY_TINT_CSS in src/data/aircraftEmergency.js. */ + --cockpit-emergency: #ff4444; --cockpit-side-panel-width: min(340px, 28vw); /* HUD bottom offset + tallest scaled telemetry block + 20px clear border. */ --cockpit-bottom-rail-safe: calc(clamp(84px, 11vh, 118px) + 100px); @@ -497,7 +499,8 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) letter-spacing: 0.16em; } .cockpit-aircraft-meta.emergency { - color: #ff4444; + color: var(--cockpit-emergency); + font-weight: 600; } .cockpit-mode-chip { margin-top: 2px; diff --git a/src/ui/styles/cyber.css b/src/ui/styles/cyber.css index 79ce004e1..c8f5b139f 100644 --- a/src/ui/styles/cyber.css +++ b/src/ui/styles/cyber.css @@ -2273,7 +2273,7 @@ :root[data-ui-theme='cyber'] body.cockpit-mode .cockpit-aircraft-meta.emergency { - color: #ff4444; + color: var(--cyber-red-bright); } :root[data-ui-theme='cyber'] body.cockpit-mode .cockpit-vision-controls { From 9af1ee0e09a8eea936c85e3c67866d12fc01933b Mon Sep 17 00:00:00 2001 From: Leo Pechnicki Date: Tue, 29 Sep 2026 19:57:31 +0200 Subject: [PATCH 3/5] refactor(flights): pass the record already in hand to the military fleet tint; default info on both civil tint helpers Co-Authored-By: Claude --- src/layers/flights/rendering.js | 10 ++++++++-- src/layers/military/rendering.js | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/layers/flights/rendering.js b/src/layers/flights/rendering.js index a58587100..ae4b7c7ca 100644 --- a/src/layers/flights/rendering.js +++ b/src/layers/flights/rendering.js @@ -66,14 +66,20 @@ export function createRendering({ * amber for known-military, white otherwise. * Ground traffic gets NO special tint (owner verdict 2026-07-03 field test). */ - function _fleetBillboardColor(icao24, info) { + function _fleetBillboardColor( + icao24, + info = flightState.records.data.get(icao24), + ) { if (_isEmergency(icao24, info)) return EMERGENCY_TINT; return isMilitaryIcao(icao24) ? MIL_TINT : Cesium.Color.WHITE; } /** Cockpit far-contact dot tint: the emergency red outranks the category tint. */ - function _cockpitContactColor(icao24, info) { + function _cockpitContactColor( + icao24, + info = flightState.records.data.get(icao24), + ) { if (_isEmergency(icao24, info)) return EMERGENCY_TINT; return isMilitaryIcao(icao24) ? MIL_TINT : COCKPIT_CIVILIAN_COLOR; } diff --git a/src/layers/military/rendering.js b/src/layers/military/rendering.js index 2beb81bc5..089ae9ec8 100644 --- a/src/layers/military/rendering.js +++ b/src/layers/military/rendering.js @@ -175,7 +175,9 @@ export function createRendering({ bb.height = icao24 === flightState._trackedIcao ? 24 : 20; bb.scale = _militaryBillboardScale(icao24) * limbScale; bb.scaleByDistance = _normalBillboardScaleByDistance(); - bb.color = _fleetBillboardColor(icao24).withAlpha(cyberSonarBaseAlpha(bb)); + bb.color = _fleetBillboardColor(icao24, meta).withAlpha( + cyberSonarBaseAlpha(bb), + ); } /** Sprite kind for one contact's billboard. Identity for every aircraft except From 6c643c8113bb0a22d0faee0885705fd6eaf39094 Mon Sep 17 00:00:00 2001 From: Leo Pechnicki Date: Thu, 1 Oct 2026 11:00:18 +0200 Subject: [PATCH 4/5] chore(squawk): scaffold review fixes for #856 From 7d6f31e54d397690dc8cfa3d3dfa3415a9a5602a Mon Sep 17 00:00:00 2001 From: Leo Pechnicki Date: Thu, 1 Oct 2026 11:53:33 +0200 Subject: [PATCH 5/5] feat(squawk): carry emergencySource + observation time into analyst records Review on #856 (kvnloo): the analyst projection flattened a 7700 squawk and the readsb ADS-B emergency field into the same emergency/emergencyKind pair. - mapAnalystRecord (flights, military) adds emergencySource ('squawk' | 'ads-b' | null, from aircraftEmergency().source), emergencyBroadcast (new pure emergencyBroadcastPhrase: "broadcasting squawk 7700 (general emergency)" vs "ADS-B reports general emergency") and observedAtMs (the record's lastContactEpochMs: last transponder message, epoch ms). - emergencySource is a queryable analyst text field; the voice payload passes it and emergencyBroadcast through, and observedAtMs only for aircraft with a status. Tool description asks the model to narrate with the phrase. - Source-to-analyst fixture test: OpenSky 7700 vs adsb.lol emergency=general on squawk 1200 differ in provenance and timestamp; filterable by source. - Voice tool-payload pins re-derived for the additive description change. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 5 +- docs/CURRENT-STATE.md | 7 +- scripts/format-scope.json | 1 + src/data/aircraftEmergency.js | 16 +++ src/data/aircraftEmergency.test.mjs | 20 +++ src/data/aircraftEmergencyAnalyst.test.mjs | 152 +++++++++++++++++++++ src/data/flights.test.mjs | 3 + src/data/militaryFlights.test.mjs | 3 + src/layers/flights/queries.js | 12 +- src/layers/military/queries.js | 12 +- 10 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/data/aircraftEmergencyAnalyst.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e731268..cab1f1f72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,7 +103,10 @@ Minimum-fuel and lifeguard statuses are reported but not tinted. The status follows the latest poll instead of sticking, so a cleared code stops highlighting on the next refresh. The analyst and voice query engine can - filter by `squawk`, `emergency` and `emergencyKind`. No new source or + filter by `squawk`, `emergency`, `emergencyKind` and `emergencySource` + (whether the squawk or the ADS-B field named the status), and each result + says when the transponder was last heard, so answers read "broadcasting + squawk 7700" or "ADS-B reports general emergency". No new source or request: OpenSky already carries the squawk and adsb.lol carries both. - Street Level: Google Street View is the second provider. On the Google 3D diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index dea8a5bec..6244311a5 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -831,7 +831,12 @@ the same tint when a billboard is first created and when a tracked contact is released. While a broadcasting aircraft is tracked, the Cockpit aircraft-meta line reads `SQUAWK 7700 · GENERAL EMERGENCY` when the code names the status, else `ADS-B EMERGENCY · …` or `ADS-B PRIORITY · …`, in red. Analyst records and -the voice query fields expose `squawk`, `emergency` (flag) and `emergencyKind`. +the voice query fields expose `squawk`, `emergency` (flag), `emergencyKind` and +`emergencySource` (`squawk` when the Mode A code names the status, `ads-b` for +the readsb field), plus `emergencyBroadcast` (`broadcasting squawk 7700 (general +emergency)` vs `ADS-B reports general emergency`) and `observedAtMs`, the epoch +ms of the last transponder message. Voice results carry `observedAtMs` only for +aircraft with a status. `scripts/qa-emergency-squawk.mjs` proves the tint, HUD line and clearing. ## Browser live-source observations diff --git a/scripts/format-scope.json b/scripts/format-scope.json index ea1531793..a2ec00096 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -20,6 +20,7 @@ "src/data/infrastructure.js", "src/data/aircraftEmergency.js", "src/data/aircraftEmergency.test.mjs", + "src/data/aircraftEmergencyAnalyst.test.mjs", "scripts/qa-emergency-squawk.mjs", "src/data/infrastructure.test.mjs", "src/data/localGeojsonCore.js", diff --git a/src/data/aircraftEmergency.js b/src/data/aircraftEmergency.js index 3fd6ab688..3a555399e 100644 --- a/src/data/aircraftEmergency.js +++ b/src/data/aircraftEmergency.js @@ -63,6 +63,22 @@ export function aircraftEmergency({ squawk, emergency } = {}) { }; } +/** + * How a summary may phrase the status without flattening it into an incident: + * quote the code when the Mode A squawk itself names the status, otherwise + * attribute it to the ADS-B emergency field. + * @param {{kind?: string, squawk?: string|null, source?: string}|null|undefined} status + * @returns {string|null} e.g. 'broadcasting squawk 7700 (general emergency)' + * or 'ADS-B reports general emergency'; null when there is no status. + */ +export function emergencyBroadcastPhrase(status) { + if (!status || !Object.hasOwn(KINDS, status.kind)) return null; + const label = KINDS[status.kind].label.toLowerCase(); + return status.source === 'squawk' && status.squawk + ? `broadcasting squawk ${status.squawk} (${label})` + : `ADS-B reports ${label}`; +} + /** * True when the aircraft is broadcasting an emergency (not a priority status). * @param {{severity?: string}|null|undefined} status diff --git a/src/data/aircraftEmergency.test.mjs b/src/data/aircraftEmergency.test.mjs index 2cff800b9..4304dcb14 100644 --- a/src/data/aircraftEmergency.test.mjs +++ b/src/data/aircraftEmergency.test.mjs @@ -3,6 +3,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { aircraftEmergency, + emergencyBroadcastPhrase, isEmergency, normalizeSquawk, } from './aircraftEmergency.js'; @@ -62,3 +63,22 @@ test('priority statuses are reported but are not emergencies', () => { assert.equal(isEmergency(aircraftEmergency({ emergency: 'minfuel' })), false); assert.equal(isEmergency(null), false); }); + +test('the broadcast phrase quotes a naming code, else attributes ADS-B', () => { + assert.equal( + emergencyBroadcastPhrase(aircraftEmergency({ squawk: '7700' })), + 'broadcasting squawk 7700 (general emergency)', + ); + assert.equal( + emergencyBroadcastPhrase( + aircraftEmergency({ squawk: '1200', emergency: 'general' }), + ), + 'ADS-B reports general emergency', + ); + assert.equal( + emergencyBroadcastPhrase(aircraftEmergency({ emergency: 'lifeguard' })), + 'ADS-B reports lifeguard (medical priority)', + ); + assert.equal(emergencyBroadcastPhrase(null), null); + assert.equal(emergencyBroadcastPhrase({ kind: 'reserved' }), null); +}); diff --git a/src/data/aircraftEmergencyAnalyst.test.mjs b/src/data/aircraftEmergencyAnalyst.test.mjs new file mode 100644 index 000000000..8df428162 --- /dev/null +++ b/src/data/aircraftEmergencyAnalyst.test.mjs @@ -0,0 +1,152 @@ +// src/data/aircraftEmergencyAnalyst.test.mjs +// Source-to-analyst fixture: an OpenSky state vector squawking 7700 and an +// adsb.lol (readsb) aircraft whose ADS-B `emergency` field says `general` +// on an ordinary code must reach the analyst with the same kind but distinct +// provenance, so a summary never flattens one into the other. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { openSkySnapshot, readsbSnapshot } from '../sources/live/index.js'; +import { FlightRecords } from '../layers/flights/records.js'; +import { MilitaryFlightRecords } from '../layers/military/records.js'; +import { mapAnalystRecord as mapFlight } from './flights.js'; +import { mapAnalystRecord as mapMilitary } from './militaryFlights.js'; +import { createAnalystEngine } from './analystEngine.js'; + +const NOW_MS = 1800000000000; + +// OpenSky /states/all: [icao24, callsign, origin_country, time_position, +// last_contact, lon, lat, baro_alt, on_ground, velocity, true_track, +// vertical_rate, sensors, geo_alt, squawk, spi, position_source, category]. +const OPENSKY = { + time: NOW_MS / 1000, + states: [ + [ + 'a1b2c3', + 'SWA696 ', + 'United States', + NOW_MS / 1000 - 6, + NOW_MS / 1000 - 3, + -97.67, + 30.19, + 3000, + false, + 150, + 180, + -5, + null, + 3040, + '7700', + false, + 0, + 4, + ], + ], +}; + +// adsb.lol /v2/mil: readsb aircraft with `seen`/`seen_pos` ages in seconds. +const ADSB_LOL = { + ac: [ + { + hex: 'ae01ce', + flight: 'RCH123 ', + t: 'C17', + lat: 31.1, + lon: -97.4, + alt_baro: 18000, + gs: 300, + track: 90, + squawk: '1200', + emergency: 'general', + seen: 1.5, + seen_pos: 2, + }, + ], +}; + +function civilAnalystRecord() { + const [row] = openSkySnapshot(OPENSKY, { now: NOW_MS }).records; + const store = new FlightRecords({ + geoidHeight: () => 0, + cachedGroundFloor: () => null, + floorAltitudeM: (altitude) => altitude, + approxDistanceKm: () => 0, + }); + const { meta } = store.receive(row, { + viewerLatDeg: null, + viewerLonDeg: null, + trackedId: null, + floorWarmPoints: [], + }); + return mapFlight(row.id, meta); +} + +function militaryAnalystRecord() { + const [row] = readsbSnapshot(ADSB_LOL, { + observedAtMs: NOW_MS, + now: NOW_MS, + }).records; + const store = new MilitaryFlightRecords({ + geoidHeight: () => 0, + cachedGroundFloor: () => null, + floorAltitudeM: (altitude) => altitude, + }); + const { meta } = store.receive(row, { + observedAtMs: NOW_MS, + floorWarmPoints: [], + modelOwnsVisual: false, + }); + return mapMilitary(row.id, meta); +} + +test('OpenSky 7700 and adsb.lol emergency=general differ only in provenance', () => { + const civil = civilAnalystRecord(); + const military = militaryAnalystRecord(); + + assert.equal(civil.emergency, true); + assert.equal(civil.emergencyKind, 'general'); + assert.equal(civil.squawk, '7700'); + assert.equal(civil.emergencySource, 'squawk'); + assert.equal( + civil.emergencyBroadcast, + 'broadcasting squawk 7700 (general emergency)', + ); + assert.equal(civil.observedAtMs, NOW_MS - 3000, 'OpenSky last_contact'); + + assert.equal(military.emergency, true); + assert.equal(military.emergencyKind, 'general'); + assert.equal(military.squawk, '1200', 'the code is not an emergency code'); + assert.equal(military.emergencySource, 'ads-b'); + assert.equal(military.emergencyBroadcast, 'ADS-B reports general emergency'); + assert.equal(military.observedAtMs, NOW_MS - 1500, 'snapshot time - seen'); +}); + +test('the analyst can filter emergencies by source', async () => { + const records = { + flights: [civilAnalystRecord()], + military: [militaryAnalystRecord()], + }; + const engine = createAnalystEngine({ + getRecords: (key) => records[key] || [], + resolveRegionRing: async () => null, + getViewContext: () => ({ lat: 30.5, lon: -97.5, viewRadiusKm: 500 }), + }); + const query = (value) => + engine.query({ + layers: ['flights', 'military'], + scope: { kind: 'anywhere' }, + filters: [ + { field: 'emergency', op: 'eq', value: true }, + { field: 'emergencySource', op: 'eq', value }, + ], + }); + const bySquawk = await query('squawk'); + assert.deepEqual( + bySquawk.items.map((item) => [item.icao24, item.emergencyBroadcast]), + [['a1b2c3', 'broadcasting squawk 7700 (general emergency)']], + ); + const byAdsb = await query('ads-b'); + assert.deepEqual( + byAdsb.items.map((item) => [item.icao24, item.emergencyBroadcast]), + [['ae01ce', 'ADS-B reports general emergency']], + ); +}); diff --git a/src/data/flights.test.mjs b/src/data/flights.test.mjs index 0e7021251..6c6a586d6 100644 --- a/src/data/flights.test.mjs +++ b/src/data/flights.test.mjs @@ -74,6 +74,9 @@ test('flights analyst record: full record maps every contract field', () => { squawk: null, emergency: false, emergencyKind: null, + emergencySource: null, + emergencyBroadcast: null, + observedAtMs: null, routeOrigin: 'AUS', routeDestination: 'LAX', }); diff --git a/src/data/militaryFlights.test.mjs b/src/data/militaryFlights.test.mjs index 9e6e4aea8..b6441aa52 100644 --- a/src/data/militaryFlights.test.mjs +++ b/src/data/militaryFlights.test.mjs @@ -70,6 +70,9 @@ test('military analyst record: full record maps every contract field', () => { squawk: null, emergency: false, emergencyKind: null, + emergencySource: null, + emergencyBroadcast: null, + observedAtMs: null, routeOrigin: null, routeDestination: null, }); diff --git a/src/layers/flights/queries.js b/src/layers/flights/queries.js index 89ea44214..0714acfc5 100644 --- a/src/layers/flights/queries.js +++ b/src/layers/flights/queries.js @@ -15,6 +15,7 @@ import { FOCUS_EVIDENCE_DEV, FLEET_DR_INTERVAL_MS, } from './policy.js'; +import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js'; export function createQueries({ flightState, @@ -223,7 +224,8 @@ export function createQueries({ * heading: number|null, verticalRateMps: number|null, onGround: boolean, * military: boolean, aircraftClass: string|null, originCountry: string|null, * operator: string|null, squawk: string|null, emergency: boolean, - * emergencyKind: string|null, routeOrigin: string|null, + * emergencyKind: string|null, emergencySource: 'squawk'|'ads-b'|null, + * emergencyBroadcast: string|null, observedAtMs: number|null, routeOrigin: string|null, * routeDestination: string|null}} */ @@ -264,6 +266,14 @@ export function createQueries({ // without the flag. emergency: info?.emergency?.severity === 'emergency', emergencyKind: text(info?.emergency?.kind), + // Provenance, so a summary can say "broadcasting squawk 7700" rather + // than "ADS-B reports general emergency" (or flatten both): 'squawk' + // when the Mode A code names the status, 'ads-b' for the readsb field. + emergencySource: text(info?.emergency?.source), + emergencyBroadcast: emergencyBroadcastPhrase(info?.emergency), + // Epoch ms of the last transponder message — when this squawk/status + // was heard (OpenSky time_contact; readsb snapshot time minus `seen`). + observedAtMs: num(info?.lastContactEpochMs), routeOrigin: routeOk ? text(info?.route?.origin?.code) : null, routeDestination: routeOk ? text(info?.route?.destination?.code) : null, }; diff --git a/src/layers/military/queries.js b/src/layers/military/queries.js index eab06458c..0d418ddc1 100644 --- a/src/layers/military/queries.js +++ b/src/layers/military/queries.js @@ -9,6 +9,7 @@ import { CONTACT_MATCH_TIER, } from '../../data/contactMatch.js'; import { LANDED_ALT_MAX_FT, LANDED_SPEED_MAX_MPS } from './policy.js'; +import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js'; export function createQueries({ flightState, @@ -160,7 +161,8 @@ export function createQueries({ * heading: number|null, verticalRateMps: number|null, onGround: boolean, * military: boolean, aircraftClass: string|null, originCountry: null, * operator: string|null, squawk: string|null, emergency: boolean, - * emergencyKind: string|null, routeOrigin: null, routeDestination: null}} + * emergencyKind: string|null, emergencySource: 'squawk'|'ads-b'|null, + * emergencyBroadcast: string|null, observedAtMs: number|null, routeOrigin: null, routeDestination: null}} */ function mapAnalystRecord(icao24, info) { @@ -197,6 +199,14 @@ export function createQueries({ // without the flag. emergency: info?.emergency?.severity === 'emergency', emergencyKind: text(info?.emergency?.kind), + // Provenance, so a summary can say "broadcasting squawk 7700" rather + // than "ADS-B reports general emergency" (or flatten both): 'squawk' + // when the Mode A code names the status, 'ads-b' for the readsb field. + emergencySource: text(info?.emergency?.source), + emergencyBroadcast: emergencyBroadcastPhrase(info?.emergency), + // Epoch ms of the last transponder message — when this squawk/status + // was heard (OpenSky time_contact; readsb snapshot time minus `seen`). + observedAtMs: num(info?.lastContactEpochMs), routeOrigin: null, routeDestination: null, };