Extract instance-owned civil-flight components

This commit is contained in:
Sameh Khamis committed 2026-09-13 09:50:14 -07:00
1 parent cf149ecfc0
commit 7c7e83c9e3
33 files changed
+6190 -5284

No files matched your search

+2
View File
@@ -13,6 +13,8 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
- Split civil flights into instance-owned state, ingestion, motion, rendering, tracking and query components. Cancel enrichment on teardown and resolve model assets through the application.
- Separate aircraft/vessel transport and normalization from layer rendering, preserving observation timestamps, altitude datums and optional history.
- Retain absent aircraft during partially admitted snapshots and bound source error messages.
+16
View File
@@ -1,5 +1,21 @@
# God's Eye View Current State
## Civil-flight components
`gods-eye-view/layers/flights` exports `createCivilFlightLayer`. Each instance
owns its contacts, histories, model collections, scratch objects and lifecycle.
State, ingestion, enrichment, motion/floor interpolation, rendering, tracking and
queries live in separate files under `src/layers/flights`. The standalone
`src/data/flights.js` assembles the existing OpenSky source and scene services.
Applications supply the existing floor/snap, geoid, picking, sprite, camera,
trail, focus, readout, context, aircraft presentation and render services.
The factory never constructs a second application registry. Configure a source
before initialization; replacing it while initialized is rejected. Model loads
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.
## Browser live-source observations
Flights, Military Flights and AIS Vessels obtain snapshots and optional history
+2 -1
View File
@@ -142,6 +142,7 @@
"./ui/shell": "./src/ui/applicationShell.js",
"./ui/styles": "./src/ui/styles.js",
"./scenes": "./src/scenes/director.js",
"./sources/live": "./src/sources/live/index.js"
"./sources/live": "./src/sources/live/index.js",
"./layers/flights": "./src/layers/flights/index.js"
}
}
+17 -1
View File
@@ -254,5 +254,21 @@
"src/sources/live/contract.test.mjs",
"src/sources/live/index.js",
"src/sources/live/standalone.js",
"src/sources/live/vessels.js"
"src/sources/live/vessels.js",
"src/data/flights.js",
"src/testSupport/readLayerSource.mjs",
"src/layers/flights/lifecycle.js",
"src/layers/flights/motion.js",
"src/layers/flights/controller.js",
"src/layers/flights/state.js",
"src/layers/flights/policy.js",
"src/layers/flights/tracking.js",
"src/layers/flights/rendering.js",
"src/layers/flights/testing.js",
"src/layers/flights/evidence.js",
"src/layers/flights/ingestion.js",
"src/layers/flights/queries.js",
"src/layers/flights/index.js",
"src/layers/flights/enrichment.js",
"src/layers/flights/ownership.test.mjs"
]
+34
View File
@@ -576,5 +576,39 @@
"src/sources/live/vessels.js"
],
"external": []
},
"civil-flights": {
"exports": ["./layers/flights"],
"modules": [
"src/data/aircraftClass.js",
"src/data/aircraftIcons.js",
"src/data/aircraftMeta.js",
"src/data/aircraftNearbyPolicy.js",
"src/data/cockpitAirLod.js",
"src/data/cockpitContactDot.js",
"src/data/contactMatch.js",
"src/data/iconOrientation.js",
"src/data/layerState.js",
"src/data/modelVisualAnchor.js",
"src/data/motionModel.js",
"src/data/renderAltitude.js",
"src/data/routePlausible.js",
"src/data/trackedModelRegime.js",
"src/data/trackingClickGesture.js",
"src/layers/flights/controller.js",
"src/layers/flights/enrichment.js",
"src/layers/flights/evidence.js",
"src/layers/flights/index.js",
"src/layers/flights/ingestion.js",
"src/layers/flights/lifecycle.js",
"src/layers/flights/motion.js",
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js"
],
"external": ["cesium"]
}
}
+5 -4
View File
@@ -25,7 +25,7 @@ const TESTS = [
'src/data/renderAltitude.test.mjs',
];
const FLIGHTS = 'src/data/flights.js';
const FLIGHTS = 'src/layers/flights/motion.js';
const FLOOR = 'src/data/groundFloor.js';
const ALT = 'src/data/renderAltitude.js';
@@ -204,7 +204,7 @@ const MUTATIONS = [
{
file: FLIGHTS,
from: ' if (state.retiredMs == null) {',
to: ' _displayFloorState.delete(icao24);\n if (false) {',
to: ' flightState._displayFloorState.delete(icao24);\n if (false) {',
},
],
},
@@ -291,12 +291,13 @@ for (const mut of MUTATIONS) {
for (const e of mut.edits) {
if (!backups.has(e.file)) backups.set(e.file, read(e.file));
const cur = read(e.file);
if (!cur.includes(e.from)) {
const anchor = new RegExp(e.from.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\s*'));
if (!anchor.test(cur)) {
console.log(` [STALE] ${mut.defect} — anchor not found in ${e.file}`);
applied = false;
break;
}
write(e.file, cur.replace(e.from, e.to));
write(e.file, cur.replace(anchor, () => e.to.trim()));
}
if (applied) {
const r = runTests();
+4 -3
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import { dispatchCockpitModeChanged, enter, exit, _adoptTrackedEntity } from '../ui/cockpitTrackingController.js';
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -15,8 +16,8 @@ import militaryFlightsLayer, {
const SUBJECT = 'abc123';
const NEXT_SUBJECT = 'def456';
const FLIGHTS_SOURCE = readFileSync(new URL('./flights.js', import.meta.url), 'utf8');
const MILITARY_SOURCE = readFileSync(new URL('./militaryFlights.js', import.meta.url), 'utf8');
const FLIGHTS_SOURCE = readLayerSource(new URL('./flights.js', import.meta.url));
const MILITARY_SOURCE = readLayerSource(new URL('./militaryFlights.js', import.meta.url));
const LAYERS = [
{
@@ -108,7 +109,7 @@ test('Cockpit lifecycle publishes one normalized aircraft identity to both detec
['commercial', FLIGHTS_SOURCE],
['military', MILITARY_SOURCE],
]) {
const consumer = /function _applyCockpitState\(detail = \{\}\) \{[\s\S]*?\n\}/
const consumer = /function (?:parts\.\w+\.)?_applyCockpitState\(detail = \{\}\) \{[\s\S]*?\n\}/
.exec(source)?.[0];
assert.ok(consumer, `${name} Cockpit consumer is defined`);
assert.match(consumer, /detail\?\.subjectId/);
+18 -17
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
@@ -25,7 +26,7 @@ function numericConstant(source, name) {
}
for (const layer of LAYERS) {
const source = readFileSync(layer.path, 'utf8');
const source = readLayerSource(layer.path);
test(`${layer.name}: every GLB creation bypasses the tile-contended frame-spread queue`, () => {
const calls = [...source.matchAll(/Cesium\.Model\.fromGltfAsync\(\{([\s\S]*?)\}\)/g)];
@@ -37,11 +38,11 @@ for (const layer of LAYERS) {
});
test(`${layer.name}: Cockpit 3D obeys the shared Display toggle`, () => {
const regime = /function _modelRegimeActive\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
const regime = /function (?:parts\.\w+\.)?_modelRegimeActive\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
assert.ok(regime, '_modelRegimeActive is defined');
assert.match(regime, /if \(!_models3dEnabled\) return false;/,
assert.match(regime, /if \(!(?:flightState\.)?_models3dEnabled\) return false;/,
'OFF must keep Cockpit AIR contacts in 2D');
assert.doesNotMatch(regime, /!_models3dEnabled\s*&&\s*!_cockpitContactMode/,
assert.doesNotMatch(regime, /!(?:flightState\.)?_models3dEnabled\s*&&\s*!(?:flightState\.)?_cockpitContactMode/,
'Cockpit must not bypass the owner-visible Display toggle');
});
@@ -51,13 +52,13 @@ for (const layer of LAYERS) {
// owns. The tracked regime is DEFAULT-ON by camera distance (2026-08-19), so
// it no longer routes through the toggle-gated `_modelRegimeActive` — the
// suppression is now an explicit early return.
const regime = /function _trackedModelRegimeActive\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
const regime = /function (?:parts\.\w+\.)?_trackedModelRegimeActive\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
assert.ok(regime, '_trackedModelRegimeActive is defined');
assert.match(regime, /if \(!_trackedIcao \|\| _cockpitContactMode \|\|[\s\S]*?return false;/,
assert.match(regime, /if \(!(?:flightState\.)?_trackedIcao \|\| (?:flightState\.)?_cockpitContactMode \|\|[\s\S]*?return false;/,
'_trackedModelRegimeActive excludes cockpit');
const tracked = /function _updateTrackedModel\(\)[\s\S]*?\n if \(!active\)/.exec(source)?.[0];
const tracked = /function (?:parts\.\w+\.)?_updateTrackedModel\(\)[\s\S]*?\n if \(!active\)/.exec(source)?.[0];
assert.ok(tracked, '_updateTrackedModel is defined');
assert.match(tracked, /_trackedModelRegimeActive\(\)/,
assert.match(tracked, /(?:parts\.\w+\.)?_trackedModelRegimeActive\(\)/,
'the tracked-model driver uses the cockpit-aware predicate');
});
@@ -68,14 +69,14 @@ for (const layer of LAYERS) {
assert.equal(numericConstant(source, 'MODEL_ALL_KEEP_M'), 450_000);
assert.equal(numericConstant(source, 'COCKPIT_MODEL_MAX'), 60);
const add = /function _modelAddDistM\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
const keep = /function _modelKeepDistM\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
assert.match(add, /_models3dMode === 'all' \? MODEL_ALL_ADD_M : MODEL_PROX_ADD_M/);
assert.match(keep, /_models3dMode === 'all' \? MODEL_ALL_KEEP_M : MODEL_PROX_KEEP_M/);
const add = /function (?:parts\.\w+\.)?_modelAddDistM\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
const keep = /function (?:parts\.\w+\.)?_modelKeepDistM\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
assert.match(add, /(?:flightState\.)?_models3dMode === 'all' \? MODEL_ALL_ADD_M : MODEL_PROX_ADD_M/);
assert.match(keep, /(?:flightState\.)?_models3dMode === 'all' \? MODEL_ALL_KEEP_M : MODEL_PROX_KEEP_M/);
assert.doesNotMatch(add, /COCKPIT_MODEL_ADD_M/);
assert.doesNotMatch(keep, /COCKPIT_MODEL_KEEP_M/);
const cap = /function _modelCap\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
const cap = /function (?:parts\.\w+\.)?_modelCap\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
assert.match(cap, /Math\.min\(COCKPIT_MODEL_MAX/,
'Cockpit keeps its 60-model performance ceiling');
});
@@ -92,15 +93,15 @@ for (const layer of LAYERS) {
'near contacts and model fallbacks retain the class-derived aircraft silhouette');
assert.match(source, /bb\.rotation = 0;/,
'far dots are reset to a rotation-free presentation');
assert.match(source, /\(!_cockpitContactMode \|\| isCockpitNear\) && \(doRotations \|\| revealed\)/,
assert.match(source, /\(!(?:flightState\.)?_cockpitContactMode \|\| isCockpitNear\) && \(doRotations \|\| revealed\)/,
'near 2D silhouettes continue to receive projected course');
assert.match(source, /if \(bb\.show\) bb\.show = false; \/\/ hand off ONLY once the model renders/,
'the gap-proof billboard-to-model handoff remains intact');
});
test(`${layer.name}: Cockpit exit clears near state before restoring map presentation`, () => {
const setMode = /function _setCockpitContactMode\([\s\S]*?\n\}/.exec(source)?.[0];
assert.match(setMode, /else _cockpitNearContacts = new Set\(\);/);
assert.match(setMode, /for \(const \[icao24, bb\] of _billboards\) _applyFleetBillboardPresentation\(icao24, bb\);/);
const setMode = /function (?:parts\.\w+\.)?_setCockpitContactMode\([\s\S]*?\n\}/.exec(source)?.[0];
assert.match(setMode, /else (?:flightState\.)?_cockpitNearContacts = new Set\(\);/);
assert.match(setMode, /for \(const \[icao24, bb\] of (?:flightState\.)?_billboards\) (?:parts\.\w+\.)?_applyFleetBillboardPresentation\(icao24, bb\);/);
});
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
// src/data/detectionRenderDemand.test.mjs
//
// Detection must not hold the render loop open — and must not drop work on the
@@ -427,7 +428,7 @@ test('the render-governor gate covers the parked case, with teeth on the painter
// promptness goes with them, silently. This test is where that shows up.
test('aircraft brackets stay prompt because the aircraft layers hold the render loop', async () => {
for (const file of ['./flights.js', './militaryFlights.js']) {
const source = await readFile(new URL(file, import.meta.url), 'utf8');
const source = readLayerSource(new URL(file, import.meta.url));
const enable = /\n enable\([\s\S]*?\n \},/.exec(source)?.[0];
assert.ok(enable, `${file}: enable() is still identifiable`);
assert.match(
+36 -5210
View File
File diff suppressed because it is too large. Load diff
+7 -6
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -365,21 +366,21 @@ test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, an
// first-run session actually boots from.
const { readFile } = await import('node:fs/promises');
for (const name of ['flights.js', 'militaryFlights.js']) {
const source = await readFile(new URL(`./${name}`, import.meta.url), 'utf8');
assert.match(source, /^let _models3dEnabled = true;$/m,
const source = readLayerSource(new URL(`./${name}`, import.meta.url));
assert.match(source, /^\s*(?:let |flightState\.)_models3dEnabled = true;$/m,
`${name}: the fleet starts armed, matching the codec default`);
assert.match(source, /^let _models3dMode = 'proximity';/m,
assert.match(source, /^\s*(?:let |flightState\.)_models3dMode = 'proximity';/m,
`${name}: and starts in proximity, matching the codec default`);
}
const ui = await readFile(new URL('../ui/applicationShell.js', import.meta.url), 'utf8');
assert.match(ui, /^\s*this\._models3dEnabled = true;$/m,
assert.match(ui, /^\s*this\.(?:flightState\.)?_models3dEnabled = true;$/m,
'ui.js: the DISPLAY rail believes 3D is on before any layer-state sync arrives');
assert.match(ui, /this\._models3dMode = 'proximity';/,
assert.match(ui, /this\.(?:flightState\.)?_models3dMode = 'proximity';/,
'ui.js: and believes the mode is proximity');
const html = await readFile(new URL('../../index.html', import.meta.url), 'utf8');
assert.match(html, /class="pp-toggle-btn active" id="models3d-toggle" aria-pressed="true"/,
'index.html: the 3D button paints lit on first paint, before ui.js runs — and says so');
assert.match(ui, /this\._models3dBtn\?\.setAttribute\(\s*'aria-pressed',\s*String\(this\._models3dEnabled\),?\s*\)/,
assert.match(ui, /this\._models3dBtn\?\.setAttribute\(\s*'aria-pressed',\s*String\(this\.(?:flightState\.)?_models3dEnabled\),?\s*\)/,
'ui.js: and keeps aria-pressed synchronized, so the lit state is not colour-only');
assert.match(html, /class="pp-slider-row visible" id="models3d-mode-row"/,
'index.html: and the Proximity/All row paints open with it');
+12 -11
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -297,7 +298,7 @@ test('Contacts live repaint restores only stable identity and never retargets an
});
test('Contacts continuation target is the visible explanatory note', () => {
const source = fs.readFileSync(new URL('./militaryAwareness.js', import.meta.url), 'utf8');
const source = readLayerSource(new URL('./militaryAwareness.js', import.meta.url));
assert.match(
source,
/<p class="military-awareness-note" tabindex="-1" data-awareness-focus-continuation>Open-source mapped\/observed context\./,
@@ -1352,11 +1353,11 @@ test('hasContact declines while a layer is disabled, whatever its maps still hol
// disable() hides the collection but keeps the records, so a map lookup
// alone would report a preserved subject as FRESH from hidden stale data.
for (const [name, source, guard] of [
['flights', fs.readFileSync(new URL('./flights.js', import.meta.url), 'utf8'),
/hasContact\(icao24\) \{\s*\n\s*if \(!_billboardCollection \|\| !_billboardCollection\.show \|\| _billboards\.size === 0\) return null;/],
['militaryFlights', fs.readFileSync(new URL('./militaryFlights.js', import.meta.url), 'utf8'),
/hasContact\(icao24\) \{\s*\n\s*if \(!_billboardCollection \|\| !_billboardCollection\.show \|\| _billboards\.size === 0\) return null;/],
['aisLiveVessels', fs.readFileSync(new URL('./aisLiveVessels.js', import.meta.url), 'utf8'),
['flights', readLayerSource(new URL('./flights.js', import.meta.url)),
/hasContact\(icao24\) \{\s*\n\s*if \(!(?:flightState\.)?_billboardCollection \|\| !(?:flightState\.)?_billboardCollection\.show \|\| (?:flightState\.)?_billboards\.size === 0\) return null;/],
['militaryFlights', readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
/hasContact\(icao24\) \{\s*\n\s*if \(!(?:flightState\.)?_billboardCollection \|\| !(?:flightState\.)?_billboardCollection\.show \|\| (?:flightState\.)?_billboards\.size === 0\) return null;/],
['aisLiveVessels', readLayerSource(new URL('./aisLiveVessels.js', import.meta.url)),
/hasContact\(mmsi\) \{\s*\n\s*if \(!state\.enabled \|\| !state\.vesselMap \|\| state\.vesselMap\.size === 0\) return null;/],
]) {
assert.match(source, guard, `${name}.hasContact must decline while the layer is disabled`);
@@ -1527,14 +1528,14 @@ test('a deliberate source clear still fully clears the subject', () => {
test('production eviction sites actually tag their clears', () => {
// The event contract above is worthless if the real cull paths never set the
// origin, so pin the three production call sites.
const flightsSource = fs.readFileSync(new URL('./flights.js', import.meta.url), 'utf8');
const militarySource = fs.readFileSync(new URL('./militaryFlights.js', import.meta.url), 'utf8');
const vesselsSource = fs.readFileSync(new URL('./aisLiveVessels.js', import.meta.url), 'utf8');
const flightsSource = readLayerSource(new URL('./flights.js', import.meta.url));
const militarySource = readLayerSource(new URL('./militaryFlights.js', import.meta.url));
const vesselsSource = readLayerSource(new URL('./aisLiveVessels.js', import.meta.url));
for (const [name, source] of [['flights', flightsSource], ['militaryFlights', militarySource]]) {
assert.match(
source,
/if \(icao24 === _trackedIcao\) \{\s*\n\s*_clearTracking\(false, \{ evicted: true \}\);/,
/if \(icao24 === (?:flightState\.)?_trackedIcao\) \{\s*\n\s*(?:parts\.\w+\.)?_clearTracking\(false, \{ evicted: true \}\);/,
`${name} must mark its aged-out cull as an eviction`,
);
assert.match(
@@ -1553,7 +1554,7 @@ test('production eviction sites actually tag their clears', () => {
// The tag alone is not enough — see the behavioral test in
// firmsInteraction.test.mjs. The clear must also run BEFORE renderCurrentLod,
// whose registration sweep deletes the record the clear needs to see.
const firmsSource = fs.readFileSync(new URL('./firmsHeatmap.js', import.meta.url), 'utf8');
const firmsSource = readLayerSource(new URL('./firmsHeatmap.js', import.meta.url));
const evictedClear = firmsSource.indexOf('clearSelectedEntityContextForLayer(id, { evicted: true });');
const lodRebuild = firmsSource.indexOf('renderCurrentLod(true);\n if (reselected) selectFire(reselected);');
assert.ok(evictedClear > 0, 'FIRMS must mark a refresh-vanished selection as an eviction');
+4 -3
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
// Regression test for the giant-military-jet bug: MODEL_SCALE must put every
// rendered aircraft at REAL-WORLD size, whatever the GLB's native scale is.
//
@@ -358,7 +359,7 @@ function nativeVisualCenter(file) {
// --- per-layer constants, read from source (see header) --------------------
function layerConstants(sourceFile) {
const src = fs.readFileSync(path.join(ROOT, sourceFile), 'utf8');
const src = readLayerSource(path.join(ROOT, sourceFile));
const scale = src.match(/\bconst MODEL_SCALE = ([\d.]+);/);
assert.ok(scale, `${sourceFile}: MODEL_SCALE not found`);
const belly = src.match(/\bconst MODEL_BELLY_OFFSET_NATIVE = ([\d.]+);/);
@@ -367,8 +368,8 @@ function layerConstants(sourceFile) {
}
function normalBillboardScaleByDistance(sourceFile) {
const src = fs.readFileSync(path.join(ROOT, sourceFile), 'utf8');
const fn = src.match(/function _normalBillboardScaleByDistance\(\) \{([\s\S]*?)\n\}/);
const src = readLayerSource(path.join(ROOT, sourceFile));
const fn = src.match(/function (?:parts\.\w+\.)?_normalBillboardScaleByDistance\(\) \{([\s\S]*?)\n\}/);
assert.ok(fn, `${sourceFile}: _normalBillboardScaleByDistance not found`);
const scalar = fn[1].match(/NearFarScalar\((\d+), ([\d.]+), (\d+), ([\d.]+)\)/);
assert.ok(scalar, `${sourceFile}: billboard NearFarScalar not found`);
+8 -7
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
// src/data/tr3bRegistry.test.mjs
// TR-3B conversion Easter egg: registry state, sprite-variant selection,
// class-label override, and the render-path invariants that keep a converted
@@ -227,7 +228,7 @@ test('a conversion survives a poll refresh, in both the billboard and the tracke
test('both flight layers keep a converted contact 2D and visible (render invariants)', async () => {
for (const name of ['flights.js', 'militaryFlights.js']) {
const source = await readFile(new URL(`./${name}`, import.meta.url), 'utf8');
const source = readLayerSource(new URL(`./${name}`, import.meta.url));
// 1. The 3D model handoff is SUPPRESSED for a converted contact — there is
// no TR-3B GLB, so the triangle billboard stays the visual.
@@ -237,13 +238,13 @@ test('both flight layers keep a converted contact 2D and visible (render invaria
// (2026-08-19), so the suppression moved from a conjunct on
// `_modelRegimeActive()` to an explicit early return. The invariant is
// unchanged: a converted contact never reaches the model handoff.
assert.match(source, /if \(!_trackedIcao \|\| _cockpitContactMode \|\| isTr3b\(_trackedIcao\)\) \{/,
assert.match(source, /if \(!(?:flightState\.)?_trackedIcao \|\| (?:flightState\.)?_cockpitContactMode \|\| isTr3b\((?:flightState\.)?_trackedIcao\)\) \{/,
`${name}: the standalone tracked model is suppressed for a converted contact`);
// 2. The billboard is never hidden by that suppression — it must keep
// satisfying the getNearby/getDetectableObjects visibility guards, so a
// converted contact still works in Contacts and Cockpit.
assert.match(source, /if \(bb && id !== _trackedIcao\) bb\.show = true;|if \(modelled && id !== _trackedIcao\) modelled\.show = true;/,
assert.match(source, /if \(bb && id !== (?:flightState\.)?_trackedIcao\) bb\.show = true;|if \(modelled && id !== (?:flightState\.)?_trackedIcao\) modelled\.show = true;/,
`${name}: converting restores the billboard the model handoff had hidden`);
// 3. Every aircraftIcon() CALL SITE routes through the kind resolver, so no
@@ -253,7 +254,7 @@ test('both flight layers keep a converted contact 2D and visible (render invaria
const callSites = code.match(/aircraftIcon\(\s*[^;]*?\)/g) || [];
assert.equal(callSites.length >= 4, true, `${name}: expected the known aircraftIcon call sites`);
for (const call of callSites) {
assert.match(call, /aircraftIcon\(\s*_iconKind\(/,
assert.match(call, /aircraftIcon\(\s*(?:parts\.rendering\.)?_iconKind\(/,
`${name}: ${call.replace(/\s+/g, ' ')} must resolve its sprite kind through _iconKind`);
}
@@ -271,7 +272,7 @@ test('conversions are session-scoped and no lifecycle path clears them', async (
// should still show the triangle. Only a page reload resets it — so no
// production code may clear the registry.
for (const name of ['flights.js', 'militaryFlights.js', '../ui/applicationShell.js']) {
const source = await readFile(new URL(`./${name}`, import.meta.url), 'utf8');
const source = readLayerSource(new URL(`./${name}`, import.meta.url));
assert.doesNotMatch(source, /clearTr3bRegistry/,
`${name}: teardown must not clear session conversions`);
}
@@ -406,10 +407,10 @@ test('a converted contact never consumes a 3D model CAP SLOT', async () => {
// in the fleet tick (no seam to drive headlessly), so this asserts the guard's
// POSITION rather than replaying the four-pass selection.
for (const name of ['flights.js', 'militaryFlights.js']) {
const source = await readFile(new URL(`./${name}`, import.meta.url), 'utf8');
const source = readLayerSource(new URL(`./${name}`, import.meta.url));
// Anchor on the MODEL-eligibility loop (keepDistSq), not the unrelated
// ambient-enrichment candidate loop that also builds a `cand`.
const loop = /const cand = \[\];\s*\n\s*for \(const \[icao, bb\] of _billboards\)[\s\S]*?cand\.push\(/.exec(source)?.[0];
const loop = /const cand = \[\];\s*\n\s*for \(const \[icao, bb\] of (?:flightState\.)?_billboards\)[\s\S]*?cand\.push\(/.exec(source)?.[0];
assert.ok(loop, `${name}: the model-eligibility candidate loop is present`);
assert.match(loop, /keepDistSq/, `${name}: matched the model-eligibility loop`);
assert.match(loop, /if \(isTr3b\(icao\)\) continue;/,
+11 -10
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
// src/data/trackedModelRegime.test.mjs
//
// Zoom-driven 2D↔3D for the TRACKED contact (owner directive 2026-08-19).
@@ -931,15 +932,15 @@ for (const fixture of LAYERS) {
test('both layers gate the tracked-model load and record its failures', async () => {
const { readFile } = await import('node:fs/promises');
for (const name of ['flights.js', 'militaryFlights.js']) {
const source = await readFile(new URL(`./${name}`, import.meta.url), 'utf8');
assert.match(source, /!_trackedModel && !_trackedModelLoading && _trackedModelLoadAllowed\(\)/,
const source = readLayerSource(new URL(`./${name}`, import.meta.url));
assert.match(source, /!(?:flightState\.)?_trackedModel && !(?:flightState\.)?_trackedModelLoading && (?:parts\.\w+\.)?_trackedModelLoadAllowed\(\)/,
`${name}: the driver asks permission before starting another tracked-model load`);
assert.match(source, /_noteTrackedModelLoadFailure\(trackedSpec\.url, err\)/,
assert.match(source, /(?:parts\.\w+\.)?_noteTrackedModelLoadFailure\(trackedSpec\.url, err\)/,
`${name}: a rejected load is recorded against this selection, not silently retried`);
// The reset must be reachable from the real teardown, not only a test seam.
assert.match(source, /_releaseTrackedModel\(\);\n _resetTrackedSelectionState\(\);/,
assert.match(source, /(?:parts\.\w+\.)?_releaseTrackedModel\(\);\n (?:parts\.\w+\.)?_resetTrackedSelectionState\(\);/,
`${name}: deselect clears the per-selection latches in the production path`);
assert.match(source, /_trackedIcao = icao24;\n _resetTrackedSelectionState\(\);/,
assert.match(source, /(?:flightState\.)?_trackedIcao = icao24;\n (?:parts\.\w+\.)?_resetTrackedSelectionState\(\);/,
`${name}: selecting a contact starts from a clean per-selection state`);
}
});
@@ -947,16 +948,16 @@ test('both layers gate the tracked-model load and record its failures', async ()
test('the tracked regime never consults the fleet models3d toggle', async () => {
const { readFile } = await import('node:fs/promises');
for (const name of ['flights.js', 'militaryFlights.js']) {
const source = await readFile(new URL(`./${name}`, import.meta.url), 'utf8');
const regime = /function _trackedModelRegimeActive\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
const source = readLayerSource(new URL(`./${name}`, import.meta.url));
const regime = /function (?:parts\.\w+\.)?_trackedModelRegimeActive\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
assert.ok(regime, `${name}: _trackedModelRegimeActive is defined`);
assert.doesNotMatch(regime, /_models3dEnabled|_modelRegimeActive\(\)/,
assert.doesNotMatch(regime, /(?:flightState\.)?_models3dEnabled|(?:parts\.\w+\.)?_modelRegimeActive\(\)/,
`${name}: the tracked contact's handoff is default-on, not gated on the fleet toggle`);
assert.match(regime, /trackedModelZoomActive\(/,
`${name}: the tracked contact uses the shared hysteretic zoom policy`);
// The FLEET must still obey the toggle — the budget decision stays the operator's.
const fleet = /function _modelRegimeActive\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
assert.match(fleet, /if \(!_models3dEnabled\) return false;/,
const fleet = /function (?:parts\.\w+\.)?_modelRegimeActive\(\) \{[\s\S]*?\n\}/.exec(source)?.[0];
assert.match(fleet, /if \(!(?:flightState\.)?_models3dEnabled\) return false;/,
`${name}: the fleet regime still obeys the DISPLAY-rail 3D toggle`);
}
});
+5 -4
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
@@ -244,13 +245,13 @@ test('tracking layers write gevLabelModel and expose only their cached display p
'militaryFlights.js',
'satellites.js',
'militaryInstallations.js',
].map(async (name) => [name, await readFile(new URL(`./${name}`, import.meta.url), 'utf8')]));
].map(async (name) => [name, readLayerSource(new URL(`./${name}`, import.meta.url))]));
const sources = Object.fromEntries(files);
for (const [name, source] of files) {
assert.ok(source.includes('.gevLabelModel ='), `${name} writes the explicit model directly`);
assert.ok(source.includes('.gevDisplayPosition ='), `${name} exposes a display-position cache`);
}
assert.ok(sources['flights.js'].includes('gevDisplayPosition = _trackedDisplayCached'));
assert.match(sources['flights.js'], /gevDisplayPosition = parts\.motion\._trackedDisplayCached/);
assert.ok(sources['militaryFlights.js'].includes('gevDisplayPosition = _trackedDisplayCached'));
assert.ok(sources['satellites.js'].includes('gevDisplayPosition = _trackedDisplayCached'));
assert.equal(sources['flights.js'].includes('_trackedEntity.label.text'), false);
@@ -260,11 +261,11 @@ test('tracking layers write gevLabelModel and expose only their cached display p
test('civilian and military trail heads use the lower-centre model anchor and weak-texture tint', async () => {
const files = await Promise.all(['flights.js', 'militaryFlights.js'].map(async (name) => (
[name, await readFile(new URL(`./${name}`, import.meta.url), 'utf8')]
[name, readLayerSource(new URL(`./${name}`, import.meta.url))]
)));
for (const [name, source] of files) {
assert.ok(
source.includes('const head = _trackedTrailCached() || _trackedDisplayPosition(_trackedIcao);'),
(/const head = (?:parts\.motion\.)?_trackedTrailCached\(\) \|\| (?:parts\.motion\.)?_trackedDisplayPosition\((?:flightState\.)?_trackedIcao\);/).test(source),
`${name} trail head uses the dedicated lower-centre model anchor`,
);
assert.ok(source.includes('const MODEL_COLOR_BLEND_AMOUNT = 0.94;'),
+5 -4
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
@@ -110,16 +111,16 @@ test('slow clean sprite clicks select, while long presses and orbit nudges canno
test('civilian and military click handlers apply duration only at the deselect branch', () => {
const sources = [
readFileSync(new URL('./flights.js', import.meta.url), 'utf8'),
readFileSync(new URL('./militaryFlights.js', import.meta.url), 'utf8'),
readLayerSource(new URL('./flights.js', import.meta.url)),
readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
];
for (const source of sources) {
assert.match(source, /isTrackingSelectionGesture\(gesture\)[\s\S]+scene\.pick/);
assert.match(source, /isTrackingClickGesture\(gesture\)[\s\S]+_clearTracking\([^)]*\{ origin: 'user' \}\)/);
assert.match(source, /isTrackingClickGesture\(gesture\)[\s\S]+(?:parts\.\w+\.)?_clearTracking\([^)]*\{ origin: 'user' \}\)/);
}
assert.doesNotMatch(
sources[0],
/_trackedEntity = _viewer\.entities\.add\(\{\s*id:/,
/(?:flightState\.)?_trackedEntity = (?:flightState\.)?_viewer\.entities\.add\(\{\s*id:/,
'civilian tracked entities must retain Cesium-generated GUIDs',
);
});
+22
View File
@@ -0,0 +1,22 @@
import * as Cesium from 'cesium';
export function createController({ flightState, services, parts, layer, resolveAsset }) {
function _abortActiveUpdates() {
for (const controller of flightState._activeUpdateControllers) controller.abort();
flightState._activeUpdateControllers.clear();
}
function _flightQuery(viewer) {
const cartographic = viewer?.camera?.positionCartographic;
return cartographic ? {
latitude: Cesium.Math.toDegrees(cartographic.latitude),
longitude: Cesium.Math.toDegrees(cartographic.longitude),
} : {};
}
return { _abortActiveUpdates, _flightQuery };
}
+149
View File
@@ -0,0 +1,149 @@
import { classifyAircraft } from '../../data/aircraftClass.js';
import { horizonOccluder } from '../../data/iconOrientation.js';
import * as Cesium from 'cesium';
import { ENRICH_MAX_INFLIGHT, ENRICH_DISPATCH_GAP_MS, ENRICH_AMBIENT_BUDGET_CEIL, ENRICH_AMBIENT_REFILL_TOKENS, ENRICH_AMBIENT_REFILL_WINDOW_MS, ENRICH_AMBIENT_PER_SWEEP } from './policy.js';
export function createEnrichment({ flightState, services, parts, layer, resolveAsset }) {
function _enqueueEnrich(key, query, onData, priority = false) {
if (flightState.lifetime.signal.aborted) return;
if (flightState._enrichSeen.has(key)) return;
flightState._enrichSeen.add(key);
const job = { query, onData };
// Priority (tracked / model-eligible) goes to the FRONT so a deep ambient
// backlog can never delay the plane the user just clicked or zoomed into.
if (priority) flightState._enrichQueue.unshift(job); else flightState._enrichQueue.push(job);
_drainEnrich();
}
function _drainEnrich() {
if (flightState.lifetime.signal.aborted) return;
while (flightState._enrichActive < ENRICH_MAX_INFLIGHT && flightState._enrichQueue.length) {
// Drip: at most one dispatch per ENRICH_DISPATCH_GAP_MS. When the gap
// hasn't elapsed yet, park a single wake-up timer and stop — completions
// and enqueues in the meantime re-enter here harmlessly.
const wait = ENRICH_DISPATCH_GAP_MS - (Date.now() - flightState._enrichLastDispatchMs);
if (wait > 0) {
if (!flightState._enrichDripTimer) {
flightState._enrichDripTimer = setTimeout(() => { flightState._enrichDripTimer = null; _drainEnrich(); }, wait);
}
return;
}
flightState._enrichLastDispatchMs = Date.now();
const job = flightState._enrichQueue.shift();
flightState._enrichActive += 1;
const lifetime = flightState.lifetime;
Promise.resolve().then(() => {
lifetime.signal.throwIfAborted();
return flightState._source.getEnrichment?.(job.query, { signal: lifetime.signal });
})
.then((data) => { if (!lifetime.signal.aborted && data && data.found) job.onData(data); })
.catch(() => { /* enrichment never surfaces errors */ })
.finally(() => { if (lifetime.signal.aborted) return; flightState._enrichActive -= 1; _drainEnrich(); });
}
}
function _requestTypeEnrichment(icao24, priority = false) {
if (!/^[0-9a-f]{6}$/i.test(icao24)) return;
_enqueueEnrich(`t:${icao24}`, { kind: 'type', id: icao24.toLowerCase() }, (data) => {
const meta = flightState._flightData.get(icao24);
if (!meta) return; // evicted while the lookup was in flight
meta.typeCode = data.typeCode || meta.typeCode;
meta.typeName = data.typeName || meta.typeName;
meta.registration = data.registration || meta.registration;
if (meta.typeCode) {
const klass = classifyAircraft({ typeCode: meta.typeCode, category: meta.category });
if (klass !== meta.klass) {
meta.klass = klass;
const bb = flightState._billboards.get(icao24);
if (bb) parts.rendering._applyFleetBillboardPresentation(icao24, bb);
// Hangar fleet: the class's GLB/scale may have changed — resync the
// live model, any in-flight load, and the tracked standalone model.
parts.rendering._syncModelToClass(icao24);
}
}
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) parts.tracking._updateTrackedLabelModel(icao24);
}, priority);
}
function _requestRouteEnrichment(icao24) {
const cs = String(flightState._flightData.get(icao24)?.callsign || '').trim().toUpperCase();
if (!/^[A-Z]{3}\d/.test(cs)) return; // airline-style callsigns only (LLL + digit); GA tails won't resolve
_enqueueEnrich(`r:${cs}`, { kind: 'route', id: cs }, (data) => {
const meta = flightState._flightData.get(icao24);
if (!meta) return;
meta.airline = data.airline || meta.airline;
if (data.origin && data.destination) meta.route = { origin: data.origin, destination: data.destination };
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) parts.tracking._updateTrackedLabelModel(icao24);
}, true); // route lookups only fire for the TRACKED plane — front of the queue
}
/** QA seam: headless harnesses (scripts/qa-enrich-ambient.mjs) shrink the
* bucket knobs via window.__GEV_ENRICH_AMBIENT_QA = {ceil, refillTokens,
* windowMs} — they cannot wait out a real 5-minute window. Read lazily each
* refill so a pre-boot override (or a mid-run windowMs swap) applies.
* Production never sets this; the constants above are the defaults. */
function _ambientBudgetKnobs() {
const o = (typeof window !== 'undefined' && window.__GEV_ENRICH_AMBIENT_QA) || null;
return {
ceil: Number.isFinite(o?.ceil) && o.ceil > 0 ? o.ceil : ENRICH_AMBIENT_BUDGET_CEIL,
refillTokens: Number.isFinite(o?.refillTokens) && o.refillTokens > 0 ? o.refillTokens : ENRICH_AMBIENT_REFILL_TOKENS,
windowMs: Number.isFinite(o?.windowMs) && o.windowMs > 0 ? o.windowMs : ENRICH_AMBIENT_REFILL_WINDOW_MS,
};
}
/** Advance the token bucket: add refillTokens per FULLY elapsed window since
* the anchor, clamp at the ceiling, and move the anchor forward by the whole
* windows consumed (while the bucket sits full this still advances, so idle
* time never banks more than one bucket's worth of burst). */
function _refillAmbientBudget(nowMs) {
const { ceil, refillTokens, windowMs } = _ambientBudgetKnobs();
if (!flightState._enrichAmbientRefillAnchorMs) { flightState._enrichAmbientRefillAnchorMs = nowMs; return; }
const windows = Math.floor((nowMs - flightState._enrichAmbientRefillAnchorMs) / windowMs);
if (windows <= 0) return;
flightState._enrichAmbientBudget = Math.min(ceil, flightState._enrichAmbientBudget + windows * refillTokens);
flightState._enrichAmbientRefillAnchorMs += windows * windowMs;
}
function _sweepAmbientEnrichment() {
_refillAmbientBudget(Date.now());
if (flightState._enrichAmbientBudget <= 0 || !flightState._viewer || !flightState._billboardCollection || !flightState._billboardCollection.show) return;
try {
const camera = flightState._viewer.camera;
const camPos = camera.positionWC;
const occluder = horizonOccluder(camera);
const cull = camera.frustum.computeCullingVolume(camPos, camera.directionWC, camera.upWC);
const cand = [];
for (const [icao24, bb] of flightState._billboards) {
if (flightState._enrichSeen.has(`t:${icao24}`)) continue; // answered / queued / negative this session
if (!/^[0-9a-f]{6}$/i.test(icao24)) continue; // adsbdb keys are 6-char hex only
if (flightState._flightData.get(icao24)?.onGround) continue; // ground traffic never spends ambient budget (click-to-enrich still works)
if (!bb.position || !occluder.isPointVisible(bb.position)) continue; // beyond the limb
Cesium.Cartesian3.clone(bb.position, flightState._scratchModelBS.center);
if (cull.computeVisibility(flightState._scratchModelBS) === Cesium.Intersect.OUTSIDE) continue; // off-screen
cand.push([icao24, Cesium.Cartesian3.distanceSquared(camPos, bb.position)]);
}
cand.sort((a, b) => a[1] - b[1]); // nearest first — what the user is looking at resolves first
const n = Math.min(cand.length, ENRICH_AMBIENT_PER_SWEEP, flightState._enrichAmbientBudget);
for (let i = 0; i < n; i++) {
flightState._enrichAmbientBudget -= 1;
_requestTypeEnrichment(cand[i][0]); // non-priority: fills the back of the queue
}
} catch { /* ambient enrichment is best-effort — never disturb the poll loop */ }
}
return { _enqueueEnrich, _drainEnrich, _requestTypeEnrichment, _requestRouteEnrichment, _ambientBudgetKnobs, _refillAmbientBudget, _sweepAmbientEnrichment };
}
+136
View File
@@ -0,0 +1,136 @@
import * as Cesium from 'cesium';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import { FOCUS_EVIDENCE_DEV } from './policy.js';
export function createEvidence({ flightState, services, parts, layer, resolveAsset }) {
/** Resolve a JSON-safe evidence position into ECEF. DEV-only caller. */
function _focusEvidencePosition(record) {
const cartesian = record?.cartesian;
if (Array.isArray(cartesian) && cartesian.length >= 3
&& cartesian.slice(0, 3).every(Number.isFinite)) {
return Cesium.Cartesian3.fromElements(cartesian[0], cartesian[1], cartesian[2]);
}
if (!Number.isFinite(record?.longitude) || !Number.isFinite(record?.latitude)) return null;
return Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
Number.isFinite(record.altitudeM) ? record.altitudeM : 3_000,
);
}
/** Replace the real fleet with deterministic explicit-position contacts. */
function _setFocusEvidenceAircraft(records = []) {
if (!FOCUS_EVIDENCE_DEV || !flightState._billboardCollection || !flightState._viewer) return { ok: false, count: 0 };
if (flightState._trackedIcao) parts.tracking._clearTracking();
parts.rendering._releaseModels();
for (const bb of flightState._billboards.values()) flightState._billboardCollection.remove(bb);
flightState._billboards.clear();
flightState._flightData.clear();
flightState._positionHistory.clear();
flightState._displayCourse.clear();
flightState._missingPolls.clear();
flightState._focusEvidenceIds.clear();
for (const record of Array.isArray(records) ? records : []) {
const id = String(record?.id || '').trim().toLowerCase();
const position = _focusEvidencePosition(record);
if (!id || !position) continue;
const klass = record.klass || 'airliner';
const altitudeM = Number.isFinite(record.altitudeM)
? record.altitudeM
: (Cesium.Cartographic.fromCartesian(position)?.height || 3_000);
const meta = {
callsign: String(record.callsign || id).toUpperCase(),
altitude: altitudeM,
renderAltitudeM: altitudeM,
velocity: Number.isFinite(record.velocityMps) ? record.velocityMps : 0,
true_track: Number.isFinite(record.trackDeg) ? record.trackDeg : 90,
klass,
onGround: false,
wasAirborne: true,
turnRateDps: 0,
lastContactEpochMs: Date.now(),
rawLat: record.latitude ?? null,
rawLon: record.longitude ?? null,
cullPosition: null,
};
flightState._flightData.set(id, meta);
flightState._focusEvidenceIds.add(id);
const bb = flightState._billboardCollection.add({
position,
image: aircraftIcon(parts.rendering._iconKind(id, klass)),
width: 20,
height: 20,
scale: parts.rendering._fleetBillboardScale(id, klass),
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: parts.rendering._fleetBillboardColor(id),
sizeInMeters: false,
scaleByDistance: parts.rendering._normalBillboardScaleByDistance(),
disableDepthTestDistance: parts.rendering._groundDepthDistance(),
id,
show: true,
});
flightState._billboards.set(id, bb);
}
flightState._count = flightState._billboards.size;
flightState._lastFleetTickMs = 0;
flightState._viewer.scene.requestRender?.();
return { ok: true, count: flightState._count };
}
/** Update explicit evidence positions without rebuilding billboards. */
function _moveFocusEvidenceAircraft(records = []) {
if (!FOCUS_EVIDENCE_DEV) return { ok: false, moved: 0 };
let moved = 0;
for (const record of Array.isArray(records) ? records : []) {
const id = String(record?.id || '').trim().toLowerCase();
if (!flightState._focusEvidenceIds.has(id)) continue;
const position = _focusEvidencePosition(record);
const bb = flightState._billboards.get(id);
if (!position || !bb) continue;
bb.position = position;
const meta = flightState._flightData.get(id);
if (meta) {
if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg;
if (Number.isFinite(record.velocityMps)) meta.velocity = record.velocityMps;
}
moved += 1;
}
flightState._lastFleetTickMs = 0;
flightState._viewer?.scene?.requestRender?.();
return { ok: true, moved };
}
/** JSON-safe visual snapshot for the evidence report. */
function _focusEvidenceSnapshot() {
if (!FOCUS_EVIDENCE_DEV || !flightState._viewer) return [];
return [...flightState._focusEvidenceIds].map((id) => {
const bb = flightState._billboards.get(id);
const screen = bb?.position
? Cesium.SceneTransforms.worldToWindowCoordinates(flightState._viewer.scene, bb.position)
: null;
return {
id,
show: bb?.show === true,
scale: bb?.scale ?? null,
alpha: bb?.color?.alpha ?? null,
x: screen?.x ?? null,
y: screen?.y ?? null,
cameraDistanceM: bb?.position
? Cesium.Cartesian3.distance(flightState._viewer.camera.positionWC, bb.position)
: null,
};
});
}
return { _focusEvidencePosition, _setFocusEvidenceAircraft, _moveFocusEvidenceAircraft, _focusEvidenceSnapshot };
}
+32
View File
@@ -0,0 +1,32 @@
import { createFlightState } from './state.js';
import { createRendering } from './rendering.js';
import { createMotion } from './motion.js';
import { createTracking } from './tracking.js';
import { createController } from './controller.js';
import { createEnrichment } from './enrichment.js';
import { createIngestion } from './ingestion.js';
import { createLifecycle } from './lifecycle.js';
import { createEvidence } from './evidence.js';
import { createTesting } from './testing.js';
import { createQueries } from './queries.js';
/** Compose one civil-flight layer with application-owned scene services. */
export function createCivilFlightLayer({ source, services, resolveAsset = (url) => url } = {}) {
const flightState = createFlightState({ source, services });
const parts = {};
const layer = {};
const context = { flightState, services, parts, layer, resolveAsset };
parts.rendering = createRendering(context);
parts.motion = createMotion(context);
parts.tracking = createTracking(context);
parts.controller = createController(context);
parts.enrichment = createEnrichment(context);
parts.ingestion = createIngestion(context);
parts.lifecycle = createLifecycle(context);
parts.evidence = createEvidence(context);
parts.testing = createTesting(context);
parts.queries = createQueries(context);
Object.assign(layer, parts.queries.methods, parts.lifecycle.methods, parts.ingestion.methods);
Object.defineProperty(layer, 'testing', { value: parts.testing });
return layer;
}
export { TRACKED_MODEL_MAX_PX } from './policy.js';
+539
View File
@@ -0,0 +1,539 @@
import * as Cesium from 'cesium';
import { stickyNumber, stickyText } from '../../data/aircraftMeta.js';
import { geoidSurfaceLastResortM, pickRenderAltitudeM } from '../../data/renderAltitude.js';
import { classifyAircraft } from '../../data/aircraftClass.js';
import { turnRateFromFixHistory, liftRepeatedGroundFix, synthesizeForwardKinematicsFix } from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import { GROUND_FLOOR_CLAMP_RADIUS_KM, GROUND_FLOOR_WARM_MAX_ALT_M, POSITION_HISTORY_LIMIT, LANDED_MISSING_POLL_LIMIT, MISSING_POLL_LIMIT, ERROR_BACKOFF_INTERVAL } from './policy.js';
export function createIngestion({ flightState, services, parts, layer, resolveAsset }) {
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } = services.militaryRegistry;
const { geoidHeight } = services.geoid;
const { cachedGroundFloor, floorAltitudeM, warmGroundFloor } = services.groundFloor;
const { sampleMeshFloorCells } = services.meshFloor;
const flightsLayer = layer;
const methods = {
/**
* Fetch the latest aircraft state vectors from the OpenSky proxy and
* reconcile them with the billboard collection.
*
* Handles HTTP 429 (rate-limit), 401/403 (auth), and transient errors
* with exponential-ish backoff. On success, adds/updates/removes
* billboards and position history, triggers lerp blending for the
* tracked aircraft, and updates its label text.
*
* @param {Cesium.Viewer} viewer
* @returns {Promise<void>}
*/
async update(viewer, { signal = null } = {}) {
const nowMs = Date.now();
const trackingRefreshEpoch = ++flightState._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch,
status: 'source-unavailable',
ids: new Set(),
source: flightState._lastSource,
coverage: flightState._lastCoverage,
};
if (flightState._retryAt && nowMs < flightState._retryAt) {
flightState._backoff = true;
return;
}
const resourceController = new AbortController();
flightState._activeUpdateControllers.add(resourceController);
const updateSignal = signal
? AbortSignal.any([signal, resourceController.signal])
: resourceController.signal;
try {
updateSignal.throwIfAborted();
const snapshot = await flightState._source.getSnapshot(parts.controller._flightQuery(viewer || flightState._viewer), { signal: updateSignal });
updateSignal.throwIfAborted();
flightState._lastStatus = snapshot.status ?? 200;
const usableStates = snapshot.records;
const sourceEpochMs = snapshot.observedAtMs;
const sourceAgeMs = snapshot.ageMs;
const sourceStale = snapshot.stale || snapshot.freshness === 'unknown';
flightState._backoff = sourceStale;
flightState._retryAt = 0;
flightState._lastError = sourceStale
? sourceAgeMs == null ? 'Source snapshot time unavailable'
: `Source snapshot ${Math.max(2, Math.round(sourceAgeMs / 60_000))} min old`
: null;
flightState._lastSource = snapshot.source;
flightState._lastCoverage = snapshot.coverage;
flightsLayer.source = flightState._lastSource;
const currentIcaos = new Set();
const acceptedSnapshotIcaos = new Set();
const now = Cesium.JulianDate.now();
// Field-test round 3 (2026-07-06, Austin fleet-underground): viewer
// subpoint + collected floor cells for the viewer-proximate low-contact
// clamp below — one carto read per poll, one batch warm after the loop.
const viewerCarto = (viewer || flightState._viewer)?.camera?.positionCartographic || null;
const viewerLatDeg = viewerCarto ? Cesium.Math.toDegrees(viewerCarto.latitude) : null;
const viewerLonDeg = viewerCarto ? Cesium.Math.toDegrees(viewerCarto.longitude) : null;
const floorWarmPoints = [];
// Classification and display policy consume source-independent observations.
refreshMilitaryRegistryIfStale();
for (const observation of usableStates) {
const { id: icao24, callsign, originCountry: origin_country,
longitude: lon, latitude: lat, baroAltitudeM: baro_alt, onGround: on_ground,
speedMps: velocity, courseDeg: true_track, ellipsoidAltitudeM: geo_alt,
category, verticalRateMps: vertical_rate } = observation;
acceptedSnapshotIcaos.add(icao24);
const onGround = on_ground === true;
// Known-military aircraft: the dedicated military layer wins
// (icon + track + click) while it is enabled — suppress the
// OpenSky duplicate entirely (except a currently tracked one,
// which hands off on untrack).
const isMil = isMilitaryIcao(icao24);
if (isMil && parts.tracking._militaryLayerSuppresses(icao24)) {
const dupe = flightState._billboards.get(icao24);
if (dupe) {
flightState._billboardCollection.remove(dupe);
flightState._billboards.delete(icao24);
parts.rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
flightState._flightData.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
flightState._missingPolls.delete(icao24);
flightState._geoidNCache.delete(icao24);
}
continue;
}
currentIcaos.add(icao24);
flightState._missingPolls.delete(icao24);
// Sticky merge: OpenSky intermittently drops callsign/velocity/track for
// aircraft it still positions — hold last-known-good instead of
// regressing to the ICAO hex / a 0° (north) heading. Bounded by the
// MISSING_POLL_LIMIT eviction below, which deletes the whole entry.
const prevMeta = flightState._flightData.get(icao24);
// Grounded planes with no baro reading sit at 0 m, not the 10 km
// airborne default (a parked plane must never float).
// NOTE (height-datum fix): `alt` stays the AVIATION field — the sticky
// barometric/MSL altitude read by labels (FL/altitude readout),
// route-plausibility, and follow-camera range heuristics. It is
// NEVER overwritten or renamed. Where the aircraft actually RENDERS
// on the ellipsoidal globe is a SEPARATE value (renderAltitudeM,
// below) — geo_altitude when OpenSky reports it (already WGS84
// ellipsoidal), else baro+geoid as a visual fallback, else ground
// surface when parked. `Cartesian3.fromDegrees` gets renderAltitudeM,
// never `alt` directly.
const alt = stickyNumber(baro_alt, prevMeta?.altitude, onGround ? 0 : 10000);
// geoid undulation N: cached per-aircraft (negligible drift — see
// task brief) once the geoid grid has loaded; unavailable pre-load
// just means the baro fallback branch below adds N=0 for a beat.
let geoidN = flightState._geoidNCache.get(icao24);
if (geoidN === undefined && flightState._geoidReady) {
geoidN = geoidHeight(lat, lon);
flightState._geoidNCache.set(icao24, geoidN);
}
// on_ground surface prior: ONLY synchronous warm-cache reads here —
// never a per-aircraft network fetch inside the poll loop (see the
// batch resolve call below, which fills this cache for NEXT poll). A
// Round 5 SIMPLIFICATION (owner directive: one floor, evenly applied):
// the grounded surface is the round-4 choke point and nothing else —
// rendered-mesh cell first, real (never fallback-poisoned) DEM cell
// second. The old exact-5-decimal warm chain is GONE: it minted a new
// key per parked-jitter poll for every grounded contact ON EARTH,
// hammering Re:Earth into the very failures that poisoned the cache.
let surfaceM = null;
if (onGround) {
surfaceM = cachedGroundFloor(lat, lon); // mesh ?? real DEM (coarse cell)
// Taxiing crosses into a fresh cold cell every poll — always one
// step ahead of the warm batch — so fall back to LAST poll's cell
// (warmed by last poll's batch; aprons are flat across adjacent
// 111 m cells). Round-5 verify caught taxiing contacts stuck at
// the geoid without this (round 2's lesson, at cell granularity).
if (surfaceM == null && Number.isFinite(prevMeta?.rawLat) && Number.isFinite(prevMeta?.rawLon)) {
surfaceM = cachedGroundFloor(prevMeta.rawLat, prevMeta.rawLon);
}
// Grounded contacts near the viewer feed the floor warm/sampler
// (the only ones whose exact height is visible; far contacts are
// subpixel and always-on-top anyway).
if (viewerLatDeg != null &&
parts.queries._approxDistanceKm(viewerLatDeg, viewerLonDeg, lat, lon) <= GROUND_FLOOR_CLAMP_RADIUS_KM) {
floorWarmPoints.push({ lat, lon });
}
// Last synchronous resort for a BRAND-NEW grounded contact with NO
// altitude data at all (nothing warm yet, not even the coarse
// cell): the geoid surface. At the sea-level airports where most
// grounded traffic sits, geoidN IS the local ellipsoidal ground to
// within metres — instantly right — and at elevated fields it is
// far less wrong than the raw 0 m ellipsoid default for the one
// poll until the coarse cell warms. STRICTLY gated on "no geo, no
// baro": a reported baro already reflects the field elevation, and
// pickRenderAltitudeM's surfaceM branch would let this crude guess
// outrank it (caught by the ground-3d track regression).
//
// 2026-08-21: the rule moved to geoidSurfaceLastResortM, which adds
// one more gate — a contact that already HAS a render height keeps
// it. Leaving surfaceM null routes it through the sentinel path
// below, which holds that height.
if (surfaceM == null) {
surfaceM = geoidSurfaceLastResortM({
geoAltM: geo_alt,
baroAltM: baro_alt,
priorRenderM: prevMeta?.renderAltitudeM,
geoidN,
});
}
}
const geoAltitudeM = Number.isFinite(geo_alt) ? geo_alt : null;
const pickedAltM = pickRenderAltitudeM({
geoAltM: geoAltitudeM,
baroAltM: Number.isFinite(baro_alt) ? baro_alt : null,
onGround,
surfaceM,
geoidN,
});
// pickRenderAltitudeM returns the sentinel `null` only when NEITHER
// geo_altitude nor baro_altitude was reported THIS poll. Two fallbacks,
// in priority order:
// (1) hold the previous geoid-corrected render height if we have one —
// a one-poll baro dropout must NOT snap the plane down by the geoid
// undulation N (~46 m in London) and back up next poll. `alt` stays
// sticky for labels, so holding the last render height keeps the two
// layers consistent through the gap.
// (2) otherwise the SAME default policy `alt` already uses, so the two
// never disagree on the genuine "no data yet" case (a never-reported
// aircraft has no prior render height, so it lands here unchanged).
let renderAltitudeM;
if (pickedAltM != null) {
renderAltitudeM = pickedAltM;
} else if (Number.isFinite(prevMeta?.renderAltitudeM)) {
renderAltitudeM = prevMeta.renderAltitudeM;
} else {
renderAltitudeM = alt;
}
// Field-test fix (WAKE01/RS46 class, 2026-07-06; widened round 3):
// floor a low airborne contact's render height at the local coarse
// ground so it can never dive below the mesh. Round 3 (Austin
// fleet-underground): baro can read BELOW an elevated field — SWA696
// showed 450 ft at Austin's 542 ft field elevation — and rollout/taxi
// traffic that OpenSky hasn't flagged on_ground yet renders from that
// baro, so the whole fleet sat buried at AUS in 2D. Clamping every
// global contact would need unbounded terrain resolution; instead the
// clamp covers the TRACKED contact (always) plus every low contact
// within GROUND_FLOOR_CLAMP_RADIUS_KM of the viewer — the only ones
// whose burial is visible. Cells warm in one batch after the loop.
// Airborne only (grounded planes keep the surface-cache path above).
if (!onGround && renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M &&
(icao24 === flightState._trackedIcao ||
(viewerLatDeg != null &&
parts.queries._approxDistanceKm(viewerLatDeg, viewerLonDeg, lat, lon) <= GROUND_FLOOR_CLAMP_RADIUS_KM))) {
renderAltitudeM = floorAltitudeM(renderAltitudeM, cachedGroundFloor(lat, lon));
floorWarmPoints.push({ lat, lon });
}
const position = Cesium.Cartesian3.fromDegrees(lon, lat, renderAltitudeM);
// Landing/takeoff transition: the on_ground flip restyles IN PLACE.
const groundFlipped = !!prevMeta && (prevMeta.onGround === true) !== onGround;
// Either flip direction retires the model's ground snap: a departing plane
// flies free of it, a landing plane earns a fresh sample where it rolls out.
if (groundFlipped) flightState._groundSnap.forget(icao24);
// Store flight metadata for click-to-track labels
const cat = stickyNumber(category, prevMeta?.category, null);
const meta = {
sourceReference: observation.reference,
observedReceiptMs: Date.now(),
callsign: stickyText(callsign, prevMeta?.callsign),
altitude: alt,
// geoAltitudeM/renderAltitudeM are ADDITIVE fields alongside the
// untouched aviation `altitude` — never rename/replace it (labels,
// FL readout, route-plausibility, and follow-camera range math all
// still read `altitude`/baro).
geoAltitudeM,
renderAltitudeM,
onGround,
// Round 7: sticky airborne history — the landed fast-cull only
// applies to contacts that actually flew this session.
wasAirborne: prevMeta?.wasAirborne === true || !onGround,
// Round 6: lifted occlusion-test point for contacts rendering
// at/below the ellipsoid (fleet pass reads it — see the occluder
// note there). Null for the overwhelmingly common airborne case.
cullPosition: renderAltitudeM < 10 ? Cesium.Cartesian3.fromDegrees(lon, lat, 12) : null,
velocity: stickyNumber(velocity, prevMeta?.velocity, 0),
true_track: stickyNumber(true_track, prevMeta?.true_track, 0),
category: cat,
// An adsbdb-enriched type code outranks the coarse OpenSky category.
klass: classifyAircraft({ typeCode: prevMeta?.typeCode ?? null, category: cat }),
turnRateDps: prevMeta?.turnRateDps || 0,
verticalRate: stickyNumber(vertical_rate, prevMeta?.verticalRate, null),
// Analyst seam: OpenSky origin_country (state[2]) — additive, sticky
// like callsign so a transient blank row doesn't blank the field.
originCountry: stickyText(origin_country, prevMeta?.originCountry) || null,
// 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
// while fresh velocity/track messages are still arriving.
lastContactEpochMs: stickyNumber(
observation.contactTimeMs,
prevMeta?.lastContactEpochMs,
null,
),
// adsbdb enrichment — written by the enrichment callbacks, carried across polls:
typeCode: prevMeta?.typeCode ?? null,
typeName: prevMeta?.typeName ?? null,
registration: prevMeta?.registration ?? null,
airline: prevMeta?.airline ?? null,
route: prevMeta?.route ?? null,
// The RAW poll fix lat/lon (this tick's OpenSky state-vector
// coords, pre-dead-reckon) — kept distinct from the continuously
// dead-reckoned billboard position for any consumer that needs the
// actual reported fix.
rawLat: lat,
rawLon: lon,
};
flightState._flightData.set(icao24, meta);
const isTracked = icao24 === flightState._trackedIcao;
// Append to position history stamped with the FEED's fix epoch
// (time_position), not client receipt time — OpenSky positions arrive
// 5-15s stale and receipt-time stamping is what caused the
// back/forward oscillation. Only append when the fix actually
// advances, so stale repeats don't create zero-dt segments.
const fixEpochMs = Number.isFinite(observation.positionTimeMs) && observation.positionTimeMs > 0
? observation.positionTimeMs
: Date.now();
const fixTime = Cesium.JulianDate.fromDate(new Date(fixEpochMs));
if (!flightState._positionHistory.has(icao24)) {
flightState._positionHistory.set(icao24, []);
}
const history = flightState._positionHistory.get(icao24);
const newest = history[history.length - 1];
if (!newest || Cesium.JulianDate.greaterThan(fixTime, newest.time)) {
// Per-fix kinematics: the fix's own velocity/track ride along so the
// extrapolation paths use the values that BELONG to the fix they
// extend, not whatever the latest poll reported.
history.push({
time: fixTime,
epochMs: fixEpochMs,
position: position.clone(),
velocity: meta.velocity,
track: meta.true_track,
});
if (history.length > POSITION_HISTORY_LIMIT) {
history.shift();
}
// Turn rate from the fix-track history — computed once per new fix
// (≤5 samples), consumed by the extrapolation paths at tick rate.
meta.turnRateDps = turnRateFromFixHistory(history);
// Trail accumulation is separate from the 5-fix DR history (PRD F1)
// so the visible trail keeps growing while tracked. Ground traffic
// appends nothing — a touchdown freezes the existing trail.
// Round 2 (owner): ground traffic appends too — taxi history stays
// live after touchdown (grounded flights positions are already
// surface-clamped via the surfaceM chain, so the ground leg drapes).
if (isTracked) parts.tracking._appendTrailFix(position.clone());
} else {
const modelOwnsGroundVisual = parts.rendering._modelOwnsVisual(icao24);
if (!modelOwnsGroundVisual) {
liftRepeatedGroundFix(newest, position, meta.onGround);
}
// Apply fresh kinematics only from a forward synthetic fix. Mutating
// the historical fix reprojects the entire stale interval and snaps
// the rendered aircraft when course or speed changes late.
const kinematicsChanged = newest.velocity !== meta.velocity
|| newest.track !== meta.true_track;
if (kinematicsChanged) {
const synthetic = synthesizeForwardKinematicsFix(newest, {
epochMs: Date.now(),
velocity: meta.velocity,
track: meta.true_track,
turnRateDps: meta.turnRateDps,
});
if (synthetic) {
history.push(synthetic);
if (history.length > POSITION_HISTORY_LIMIT) history.shift();
meta.turnRateDps = turnRateFromFixHistory(history);
if (isTracked) parts.tracking._appendTrailFix(synthetic.position.clone());
}
}
}
if (flightState._billboards.has(icao24)) {
const bb = flightState._billboards.get(icao24);
// Reclassify if the category resolved/changed (first extended poll);
// a ground flip (landing/takeoff) re-scales the SAME billboard in
// place — the transition is a restyle, never a removal.
// Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff.
// Position AND rotation are owned by the fleet pass (_fleetTick);
// course changes land on the next rotation pass (forced below).
if (!isTracked) {
// Refresh affiliation hue without clobbering the tick-owned
// freshness × focus × horizon alpha composition.
bb.color = parts.rendering._fleetBillboardColor(icao24).withAlpha(bb.color?.alpha ?? 1);
}
if (prevMeta?.klass !== meta.klass || groundFlipped || flightState._cockpitContactMode) {
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Poll-path class change (category updates): same model resync rule
// as the enrichment path — the class's GLB/scale may have changed.
if (prevMeta?.klass !== meta.klass) parts.rendering._syncModelToClass(icao24);
} else {
const bb = flightState._billboardCollection.add({
position,
image: aircraftIcon(parts.rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale: parts.rendering._fleetBillboardScale(icao24, meta.klass),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: isTracked ? Cesium.Color.CYAN : parts.rendering._fleetBillboardColor(icao24),
sizeInMeters: false,
scaleByDistance: parts.rendering._normalBillboardScaleByDistance(),
// Grounded/near-surface planes sit at/below the photoreal tile
// surface — render them depth-test-free so they never vanish up
// close (_groundDepthDistance).
disableDepthTestDistance: parts.rendering._groundDepthDistance(),
id: icao24,
show: !isTracked, // hidden if currently tracked (entity replaces it)
});
flightState._billboards.set(icao24, bb);
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Takeoff while TRACKED: ground traffic drew no trail, so start one
// from the fresh airborne history (touchdown needs no action — the
// append gate above simply freezes the existing trail).
if (isTracked && groundFlipped && !meta.onGround) parts.tracking._startTrail(icao24);
// If this is the tracked aircraft, update label text
// (position updates automatically via dead-reckoning CallbackProperty)
if (isTracked && flightState._trackedEntity) {
parts.tracking._updateTrackedLabelModel(icao24);
}
}
// Remove aircraft only after MISSING_POLL_LIMIT consecutive absences.
// OpenSky routinely drops aircraft for a single poll; immediate removal
// made planes blink and yanked the camera off actively tracked flights.
// EXCEPTION: likely-landed planes (last fix low + slow) get only
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
// feed gap, and the full grace left phantom planes parked at airports.
for (const [icao24, bb] of flightState._billboards) {
if (currentIcaos.has(icao24)) continue;
// Partial admissions do not prove absence, but stale retention is bounded.
if (!snapshot.complete && Date.now() - (flightState._flightData.get(icao24)?.observedReceiptMs ?? 0) < 300000) continue;
const misses = (flightState._missingPolls.get(icao24) || 0) + 1;
const limit = parts.queries._likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
if (misses < limit) {
flightState._missingPolls.set(icao24, misses);
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) {
// Honest readout: the tracked plane has no faded billboard (its
// entity owns the visual), so refresh the label — with icao24 now
// in _missingPolls, _trackedLabelText appends the STALE cue so
// last-known velocity/altitude aren't presented as live.
parts.tracking._updateTrackedLabelModel(icao24);
}
continue;
}
flightState._missingPolls.delete(icao24);
// If the tracked flight is truly gone, clear tracking BEFORE deleting
// its state (M3 ordering, keep it): teardown reads the maps this loop
// is about to delete (billboard restore, DR cache reset), and we must
// never leave the camera mid-follow with stale tracking state. The
// camera is then RELEASED IN PLACE — it stays where the follow left
// it, fully free (owner decision 2026-07-02: no overview flyTo).
if (icao24 === flightState._trackedIcao) {
parts.tracking._clearTracking(false, { evicted: true });
}
flightState._billboardCollection.remove(bb);
flightState._billboards.delete(icao24);
parts.rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
flightState._flightData.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
flightState._geoidNCache.delete(icao24);
flightState._displayFloorState.delete(icao24);
}
// Fresh courses arrived — force a rotation pass on the next fleet tick
flightState._lastCamPoseSig = '';
// Ambient type enrichment: give ON-SCREEN planes real types (bounded
// sweep — see _sweepAmbientEnrichment; internally fail-silent).
parts.enrichment._sweepAmbientEnrichment();
// 2026-08-19: the loop above only ever collects FIX cells, but a grounded
// contact renders across every cell its dead-reckoned position drifts
// through. Add those too, so the display clamp has data where the contact
// actually is instead of silently passing.
parts.motion._collectDisplayCorridorCells(floorWarmPoints, viewerLatDeg, viewerLonDeg);
// Field-test round 3: one batch warm of the viewer-proximate low-contact
// floor cells collected in the loop (fire-and-forget, single-flight;
// read synchronously by NEXT poll's clamp — the military-layer pattern).
warmGroundFloor(floorWarmPoints);
// Round 4: sample the RENDERED mesh for those same cells (one-shot per
// cell, budget-capped, viewer-proximate, google-3d regime only). Own
// billboards/models are excluded so a vertical probe can't land on an
// aircraft instead of the pavement.
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
excludeObjects: [...flightState._billboards.values(), ...flightState._models.values(), flightState._trackedModel].filter(Boolean),
viewerLat: viewerLatDeg,
viewerLon: viewerLonDeg,
});
// Round 5: the grounded exact-key warm that used to live here is gone —
// see the note where _warmGroundedAircraftSurfaceCache was removed. The
// viewer-proximate coarse warm + mesh sampler above cover everything
// whose height is actually visible.
// Round 6: re-floor STALE grounded contacts. A parked plane whose
// transponder went quiet stops receiving poll updates, so a floor that
// warms AFTER its last fix never applied — it sat frozen at the geoid
// (ATL verify: FFT4347 at −30.7 m, 305 m under the apron, forever).
// Grounded contacts are static, so lifting the stored fix + billboard
// in place is safe (the DR extrapolates a zero-velocity fix).
parts.motion._refloorStaleGroundedContacts(currentIcaos);
flightState._count = flightState._billboards.size;
// Freshness belongs to the source snapshot, not the moment this browser
// received a cached 200 response.
flightState._lastUpdate = sourceEpochMs;
flightState._lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch,
status: 'accepted',
ids: acceptedSnapshotIcaos,
source: flightState._lastSource,
coverage: flightState._lastCoverage,
};
console.log(`[Data:Flights] Updated: ${flightState._count} aircraft`);
parts.tracking._applyPendingTrackingRestore();
} catch (e) {
if (updateSignal.aborted || e?.name === 'AbortError') {
throw new DOMException('Flights update aborted', 'AbortError');
}
console.warn('[Data:Flights] Fetch error:', e);
flightState._backoff = true;
flightState._retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
flightState._lastStatus = e?.status ?? null;
if (e?.source) { flightState._lastSource = e.source; this.source = flightState._lastSource; }
flightState._lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
} finally {
flightState._activeUpdateControllers.delete(resourceController);
}
}
};
return { methods };
}
+274
View File
@@ -0,0 +1,274 @@
import * as Cesium from 'cesium';
import { PLANE_MODEL_URL } from './policy.js';
export function createLifecycle({ flightState, services, parts, layer, resolveAsset }) {
const { clearFocusTarget } = services.focus;
const { registerSpriteCollection, restoreSpriteOrder, restoreSpriteOrderOnEnable } = services.sprites;
const { onMilitaryLayerActiveChange } = services.militaryRegistry;
const { holdContinuousRender, releaseContinuousRender } = services.render;
const { ensureGeoidReady } = services.geoid;
const { registerPickOwner, unregisterPickOwner } = services.picking;
const methods = {
/** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) {
if (flightState._viewer) throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
flightState._source = source;
flightState._lastSource = source.label || flightState._lastSource;
this.source = flightState._lastSource;
},
/**
* Initialize the flights layer.
* Creates the BillboardCollection, resets all state, and installs the
* click-to-track handler on the scene canvas.
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
*/
init(viewer) {
if (flightState._viewer) throw new Error('Flight layer is already initialized');
if (typeof flightState._source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted) flightState.lifetime = new AbortController();
clearFocusTarget('flights');
flightState._focusEvidenceIds.clear();
flightState._viewer = viewer;
flightState._billboardCollection = new Cesium.BillboardCollection();
viewer.scene.primitives.add(flightState._billboardCollection);
registerSpriteCollection('flights', flightState._billboardCollection);
flightState._modelCollection = new Cesium.PrimitiveCollection();
viewer.scene.primitives.add(flightState._modelCollection);
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
if (!flightState._preloadModel) {
const epoch = flightState._modelEpoch;
Cesium.Model.fromGltfAsync({ url: resolveAsset(PLANE_MODEL_URL), asynchronous: false })
.then((m) => {
if (epoch === flightState._modelEpoch) { flightState._preloadModel = m; flightState._planeModelLoaded = true; }
else { try { m.destroy(); } catch { /* gone */ } }
})
.catch(() => { /* tracked plane just stays a billboard a beat longer */ });
}
flightState._billboards = new Map();
flightState._detectionObjects = new Map();
flightState._flightData = new Map();
flightState._positionHistory = new Map();
flightState._displayCourse.clear();
flightState._groundSnap.clear();
flightState._count = 0;
flightState._lastUpdate = null;
flightState._backoff = false;
flightState._retryAt = 0;
flightState._lastError = null;
flightState._lastStatus = null;
flightState._lastSource = flightState._source.label || 'Aircraft';
flightState._lastCoverage = 'worldwide upstream snapshot';
flightState._trackedIcao = null;
parts.tracking._resetTrackedSelectionState();
flightState._trackedEntity = null;
flightState._cockpitSubjectId = null;
flightState._cockpitContactMode = document.body.classList.contains('cockpit-mode');
flightState._cockpitNearContacts = new Set();
if (!flightState._cockpitModeListener) {
flightState._cockpitModeListener = (event) => parts.tracking._applyCockpitState(event?.detail);
window.addEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener);
}
// Fresh session — full bucket, anchor re-seeded on the first sweep.
flightState._enrichAmbientBudget = parts.enrichment._ambientBudgetKnobs().ceil;
flightState._enrichAmbientRefillAnchorMs = 0;
parts.tracking._installClickHandler(viewer);
// React to Military-layer toggles IMMEDIATELY (suppress/restore sweep)
// instead of waiting out the 30 s poll (M2).
if (!flightState._milActiveChangeUnsub) {
flightState._milActiveChangeUnsub = onMilitaryLayerActiveChange(parts.tracking._onMilitaryActiveChange);
}
restoreSpriteOrder(viewer);
console.log('[Data:Flights] Initialized with billboard icons');
},
/**
* Show the billboard collection and re-install the click handler.
* @param {Cesium.Viewer} viewer
*/
enable(viewer) {
if (flightState._billboardCollection) flightState._billboardCollection.show = true;
holdContinuousRender('flights'); // per-frame animator (perf wave 2)
if (flightState._modelCollection) flightState._modelCollection.show = true;
parts.tracking._setCockpitContactMode(document.body.classList.contains('cockpit-mode'));
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
// only reads geoidHeight() synchronously after this resolves (guarded by
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
if (!flightState._geoidReady) {
const lifetime = flightState.lifetime;
ensureGeoidReady()
.then(() => { if (!lifetime.signal.aborted) flightState._geoidReady = true; })
.catch(() => { /* geoid grid failed to load — baro path stays un-geoid-corrected until retried */ });
}
parts.tracking._installClickHandler(viewer);
registerPickOwner('flights', (pickedId) => flightState._billboards.has(pickedId));
// Force a fresh rotation pass on the first tick after re-enable
flightState._lastCamPoseSig = '';
if (!flightState._preRenderRemove && viewer?.scene) {
flightState._preRenderRemove = viewer.scene.preRender.addEventListener(parts.rendering._fleetTick);
}
if (!flightState._trackedModelPreUpdateRemove && viewer?.scene) {
flightState._trackedModelPreUpdateRemove = viewer.scene.preUpdate.addEventListener(parts.rendering._updateTrackedModel);
}
if (!flightState._moveEndRemove && viewer?.camera) {
// Arrival polish (field test 2026-07-03: "planes look weird when you first
// come to them"): when a camera move SETTLES (voice fly-to, fast pan
// release), force a full rotation pass on the very next frame. The
// pose-signature gate alone can eat the settle — the final easing frames
// of a flight land inside one quantization bucket (10 m / 0.06°), leaving
// every icon wearing its last mid-flight rotation for up to
// ROTATION_REFRESH_MS. Zeroing the tick throttle too means the pass runs
// on the next preRender, not up to FLEET_DR_INTERVAL_MS later. Cost: one
// extra rotation pass per completed camera gesture — nothing per-frame.
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(() => {
flightState._lastCamPoseSig = '';
flightState._lastFleetTickMs = 0;
});
}
restoreSpriteOrderOnEnable('flights', viewer);
},
/**
* Hide all flight billboards and tear down click/keyboard handlers.
* Also clears any active flight tracking so the camera is released.
* @param {Cesium.Viewer} viewer
*/
disable(viewer) {
parts.controller._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore();
if (flightState._billboardCollection) flightState._billboardCollection.show = false;
releaseContinuousRender('flights');
parts.rendering._releaseModels();
if (flightState._modelCollection) flightState._modelCollection.show = false;
parts.tracking._clearTracking();
parts.tracking._destroyTrail();
// Remove click handler + keydown listener while disabled to avoid
// intercepting input when the layer is off
if (flightState._clickHandler) {
flightState._clickHandler.destroy();
flightState._clickHandler = null;
}
if (flightState._trackedEntityChangedRemove) {
flightState._trackedEntityChangedRemove();
flightState._trackedEntityChangedRemove = null;
}
document.removeEventListener('keydown', parts.tracking._onKeyDown);
unregisterPickOwner('flights');
if (flightState._preRenderRemove) {
flightState._preRenderRemove();
flightState._preRenderRemove = null;
}
if (flightState._trackedModelPreUpdateRemove) {
flightState._trackedModelPreUpdateRemove();
flightState._trackedModelPreUpdateRemove = null;
}
if (flightState._moveEndRemove) {
flightState._moveEndRemove();
flightState._moveEndRemove = null;
}
},
/**
* Fully tear down the flights layer — remove primitives, handlers,
* tracked entities, and clear all internal state maps.
* @param {Cesium.Viewer} viewer
*/
destroy(viewer) {
flightState.lifetime.abort();
flightState._enrichActive = 0;
flightState._enrichLastDispatchMs = 0;
parts.controller._abortActiveUpdates();
releaseContinuousRender('flights'); // direct-destroy path (perf wave 2 fix)
parts.tracking._clearTracking();
parts.tracking._destroyTrail();
parts.tracking._cancelPendingTrackingRestore();
if (flightState._clickHandler) {
flightState._clickHandler.destroy();
flightState._clickHandler = null;
}
if (flightState._trackedEntityChangedRemove) {
flightState._trackedEntityChangedRemove();
flightState._trackedEntityChangedRemove = null;
}
if (flightState._milActiveChangeUnsub) {
flightState._milActiveChangeUnsub();
flightState._milActiveChangeUnsub = null;
}
document.removeEventListener('keydown', parts.tracking._onKeyDown);
if (flightState._cockpitModeListener) {
window.removeEventListener('gev:cockpit-mode-changed', flightState._cockpitModeListener);
flightState._cockpitModeListener = null;
}
unregisterPickOwner('flights');
if (flightState._preRenderRemove) {
flightState._preRenderRemove();
flightState._preRenderRemove = null;
}
if (flightState._trackedModelPreUpdateRemove) {
flightState._trackedModelPreUpdateRemove();
flightState._trackedModelPreUpdateRemove = null;
}
if (flightState._moveEndRemove) {
flightState._moveEndRemove();
flightState._moveEndRemove = null;
}
parts.rendering._releaseModels();
if (flightState._billboardCollection) {
viewer.scene.primitives.remove(flightState._billboardCollection);
flightState._billboardCollection = null;
}
if (flightState._modelCollection) {
viewer.scene.primitives.remove(flightState._modelCollection); // removing destroys it + its models
flightState._modelCollection = null;
}
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
flightState._modelPending.clear();
flightState._modelGen.clear();
if (flightState._preloadModel) { try { flightState._preloadModel.destroy(); } catch { /* gone */ } flightState._preloadModel = null; }
flightState._planeModelLoaded = false;
flightState._billboards.clear();
flightState._detectionObjects.clear();
flightState._flightData.clear();
flightState._positionHistory.clear();
flightState._displayCourse.clear();
flightState._groundSnap.clear();
flightState._displayFloorState.clear();
flightState._enrichQueue.length = 0;
flightState._enrichSeen.clear();
if (flightState._enrichDripTimer) { clearTimeout(flightState._enrichDripTimer); flightState._enrichDripTimer = null; }
flightState._missingPolls.clear();
flightState._focusEvidenceIds.clear();
flightState._count = 0;
flightState._lastUpdate = null;
flightState._cockpitContactMode = false;
flightState._cockpitNearContacts = new Set();
flightState._cockpitSubjectId = null;
flightState._trackingRefreshEpoch += 1;
flightState._lastTrackingRefreshOutcome = {
epoch: flightState._trackingRefreshEpoch,
status: 'destroyed',
ids: new Set(),
source: flightState._lastSource,
coverage: flightState._lastCoverage,
};
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
flightState._viewer = null;
}
};
return { methods };
}
+852
View File
@@ -0,0 +1,852 @@
import * as Cesium from 'cesium';
import { COURSE_HOLD_SPEED_MPS, lerpAngleDeg, speedRamp, courseBetweenCartesians, staleCoastLimitSeconds, arcOffsetEnu, courseSlewCapDps, limitCourseStep, corridorPathLatLon } from '../../data/motionModel.js';
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
import { modelVisualAnchor, modelAnchorWorld } from '../../data/modelVisualAnchor.js';
import { FOCUS_EVIDENCE_DEV, RENDER_DELAY_SEC, TRACKED_MODEL_MAX_PX, TRACKED_MODEL_MIN_PX, TRACKED_BILLBOARD_SCALE_BY_DISTANCE, DR_CORRECTION_MS, COURSE_SLEW_DT_MAX_SEC, COURSE_MAX_DPS, HELD_FLOOR_MAX_DRIFT_KM, NEIGHBOR_FLOOR_PROBE_MS, FLOOR_SEED_GRACE_MS, FLOOR_EASE_MAX_STEP, FLOOR_EASE_TAU_MS, FLOOR_EASE_EPSILON_M, DISPLAY_CORRIDOR_RADIUS_KM, DISPLAY_CORRIDOR_LOOKAHEAD_SEC, DISPLAY_CORRIDOR_CELL_BUDGET, DISPLAY_CORRIDOR_FAIR_SHARE } from './policy.js';
export function createMotion({ flightState, services, parts, layer, resolveAsset }) {
const { cachedGroundFloor, GROUND_FLOOR_LIFT_M, neighborFloorM, stickyFloorCell, displayFloorHeightM, coarseFloorCoord, corridorFloorCells, allocateCorridorCells } = services.groundFloor;
const { trackedModelScaleForPixelCap } = services.camera;
const { nearFarScalarValueAtDistance, clearFocusTarget, publishFocusTargetFromCachedPosition } = services.focus;
// Round 5: the old `_warmGroundedAircraftSurfaceCache` (global exact-5-decimal
// warm for every grounded contact on Earth) is GONE. Parked-aircraft GPS
// jitter minted fresh keys every poll → thousands of upstream points per
// minute → Re:Earth proxy failures → geoid-fallback POISON cached at
// sea-level heights → sunken sprites/trails and rejected mesh samples. The
// coarse ~111 m floor cells (viewer-proximate, collected in the poll loop)
// are the only DEM warm the layer needs.
/**
* Round 6: lifts STALE grounded contacts onto floors that warmed after
* their last feed fix. A parked plane whose transponder went quiet keeps
* coasting on its final meta — if that fix predated the floor warm, it sat
* frozen underground forever. Runs once per poll over the (bounded) flight
* map; touches only grounded, feed-absent, demonstrably-below-floor
* contacts, and patches the stored fix + billboard in place (zero-velocity
* DR renders the patched fix verbatim).
* @param {Set<string>} currentIcaos - Contacts present in THIS poll (already
* floored by the live path — skipped here).
*/
function _refloorStaleGroundedContacts(currentIcaos) {
for (const [icao24, info] of flightState._flightData) {
if (!info?.onGround || currentIcaos.has(icao24)) continue;
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon)) continue;
const floor = cachedGroundFloor(info.rawLat, info.rawLon);
if (!Number.isFinite(floor)) continue;
const lifted = floor + GROUND_FLOOR_LIFT_M;
if (Number.isFinite(info.renderAltitudeM) && info.renderAltitudeM >= floor - 1) continue;
info.renderAltitudeM = lifted;
info.cullPosition = null; // above the ellipsoid now (or floors say otherwise next poll)
const position = Cesium.Cartesian3.fromDegrees(info.rawLon, info.rawLat, lifted);
const history = flightState._positionHistory.get(icao24);
const newest = history?.[history.length - 1];
if (newest) newest.position = Cesium.Cartesian3.clone(position, newest.position);
const bb = flightState._billboards.get(icao24);
if (bb) bb.position = position;
}
}
/**
* Dead-reckon an aircraft's current position using ENU (East-North-Up) frame math.
*
* Projects forward from the last known API fix using the aircraft's ground
* velocity and true_track heading. The ENU transform avoids repeated lat/lon
* trig, keeping the per-frame cost low.
*
* If this aircraft is the tracked target and a lerp is in progress (new API
* fix just arrived), the function first blends from the old dead-reckoned
* position toward the corrected fix before projecting forward.
*
* @param {string} icao24 - ICAO 24-bit transponder address of the aircraft.
* @returns {Cesium.Cartesian3|null} Estimated ECEF position, or null if no history exists.
*/
function _deadReckon(icao24, result) {
const info = flightState._flightData.get(icao24);
if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) {
const position = flightState._billboards.get(icao24)?.position;
flightState._drCourseDeg = info?.true_track || 0;
flightState._drSpeedMps = info?.velocity || 0;
flightState._drCourseHold = flightState._drSpeedMps < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = false;
return position ? Cesium.Cartesian3.clone(position, result || new Cesium.Cartesian3()) : null;
}
const history = flightState._positionHistory.get(icao24);
if (!history || history.length === 0) {
flightState._drCourseDeg = null; flightState._drSpeedMps = null; flightState._drCourseHold = false; flightState._drExtrapolating = false;
return null;
}
const out = result || new Cesium.Cartesian3();
// Render one poll interval behind real time so we interpolate between
// two KNOWN fixes whenever possible (see RENDER_DELAY_SEC rationale).
const renderTime = Cesium.JulianDate.addSeconds(
Cesium.JulianDate.now(), -RENDER_DELAY_SEC, flightState._scratchRenderTime
);
// Bracketing pair: interpolate — no extrapolation error, no snap-back.
for (let i = history.length - 1; i >= 1; i--) {
const a = history[i - 1];
const b = history[i];
if (
Cesium.JulianDate.lessThanOrEquals(a.time, renderTime) &&
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
) {
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
const t = span > 0
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
: 1.0;
// Course of the DISPLAYED motion. The chord is only trustworthy when the
// segment covers real ground (at hover its direction is GPS jitter; on a
// slow tight turn it STEPS the whole per-segment turn at each boundary),
// so it is blended against the reported per-fix track by displayed
// ground speed — and the track is TIME-INTERPOLATED between the fixes so
// a slow turner's nose advances continuously through the segment instead
// of snapping once per poll. Helicopters always use the reported track
// (rotorcraft chords are noise-dominated at their typical speeds).
const chordLenM = Cesium.Cartesian3.distance(a.position, b.position);
const segSpeed = span > 0 ? chordLenM / span : ((info && info.velocity) || 0);
const fallbackTrack = (info && info.true_track) || 0;
const trackFrom = Number.isFinite(a.track) ? a.track : fallbackTrack;
const trackTo = Number.isFinite(b.track) ? b.track : trackFrom;
const trackCourse = lerpAngleDeg(trackFrom, trackTo, t);
const w = (info && info.klass === 'helicopter') ? 0 : speedRamp(segSpeed);
const chordCourse = w > 0 ? courseBetweenCartesians(a.position, b.position) : null;
flightState._drCourseDeg = chordCourse != null ? lerpAngleDeg(trackCourse, chordCourse, w) : trackCourse;
flightState._drSpeedMps = segSpeed;
flightState._drCourseHold = segSpeed < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = false;
return Cesium.Cartesian3.lerp(a.position, b.position, t, out);
}
}
const newest = history[history.length - 1];
const elapsedSec = Cesium.JulianDate.secondsDifference(renderTime, newest.time);
if (elapsedSec <= 0) {
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
// pair exists yet). Render at the DELAYED renderTime — preserving the 30s-behind
// invariant — by extrapolating the OLDEST fix BACKWARD to renderTime. As history
// fills, renderTime advances toward the first fix and the icon glides FORWARD into
// the bracketing interpolation above with NO freeze and NO backward snap. (Holding
// the oldest fix froze the icon; extrapolating the NEWEST fix to wall-clock now made
// the icon jump back ~one poll interval the instant interpolation took over.)
const oldest = history[0];
const lookbackSec = Cesium.JulianDate.secondsDifference(oldest.time, renderTime); // ≥ 0
return _extrapolateFix(oldest, info, -Math.min(lookbackSec, 60), out, (info && info.turnRateDps) || 0);
}
// Newest POSITION is older than renderTime. OpenSky can still be receiving
// fresh contact/kinematic messages for that aircraft; freezing at a hard
// 60 s-after-position boundary produced the visible stop → catch-up → stop
// cadence. Coast through the latest real contact plus a bounded grace
// window, with an absolute cap so a stale cached feed cannot drift forever.
const coastLimitSec = staleCoastLimitSeconds({
// `epochMs` is captured once when the poll is normalized. Avoid allocating
// a Date per aircraft on every 12 Hz fleet tick.
fixEpochMs: Number.isFinite(newest.epochMs)
? newest.epochMs
: Cesium.JulianDate.toDate(newest.time).getTime(),
lastContactEpochMs: info?.lastContactEpochMs,
// Permit one minute of contact grace but cap any cached-feed drift at
// five minutes. Source backoff is exposed separately as a STALE cue.
minimumSec: 60,
maximumSec: 300,
});
return _extrapolateFix(
newest,
info,
Math.min(elapsedSec, coastLimitSec),
out,
(info && info.turnRateDps) || 0,
);
}
/**
* Dead-reckon a fix along its own velocity/track by `dt` seconds, integrating a
* constant-rate-turn arc when `turnRateDps` is significant (straight line
* otherwise). Positive `dt` projects FORWARD (after the fix); negative `dt`
* projects BACKWARD (before the fix) — used for warm-up, estimating where the
* aircraft was before its first observed fix. ENU frame: east = +X, north = +Y,
* up = +Z; heading 0 deg = north, 90 deg = east. Sets `_drCourseDeg` to the
* arc's instantaneous end course on every path.
* Arc math adapted from skylight (https://github.com/cpaczek/skylight, MIT).
*/
function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
const speed = Number.isFinite(fix.velocity) ? fix.velocity : ((info && info.velocity) || 0);
const heading = Number.isFinite(fix.track) ? fix.track : ((info && info.true_track) || 0);
flightState._drSpeedMps = speed;
flightState._drCourseHold = speed < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = true;
if (speed === 0 || dt === 0) {
flightState._drCourseDeg = heading;
return Cesium.Cartesian3.clone(fix.position, out);
}
// Constant-rate-turn arc (straight line when turnRateDps ≈ 0) — a plane in a
// standard-rate turn is ~90° of arc wrong per 30 s if extrapolated straight.
arcOffsetEnu(speed, heading, turnRateDps, dt, flightState._scratchArc);
flightState._drCourseDeg = flightState._scratchArc.endCourseDeg;
Cesium.Cartesian3.fromElements(flightState._scratchArc.east, flightState._scratchArc.north, 0, flightState._scratchOffset);
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(fix.position, Cesium.Ellipsoid.WGS84, flightState._scratchEnu);
return Cesium.Matrix4.multiplyByPoint(enu, flightState._scratchOffset, out);
}
/**
* True while the tracked aircraft's delayed display time (now − RENDER_DELAY_SEC)
* predates its oldest real fix — i.e. _deadReckon is extrapolating backward, with no
* real history yet sitting BEHIND the displayed icon. The trail must draw nothing in
* this window (every accumulated point is ahead of the icon).
* @returns {boolean}
*/
function _isTrackWarmingUp() {
if (!flightState._trackedIcao) return false;
const history = flightState._positionHistory.get(flightState._trackedIcao);
if (!history || history.length === 0) return true;
const renderTime = Cesium.JulianDate.addSeconds(
Cesium.JulianDate.now(), -RENDER_DELAY_SEC, flightState._scratchWarmupTime
);
return Cesium.JulianDate.lessThan(renderTime, history[0].time);
}
/** Resolve the selected aircraft's actual rendered square extent this frame. */
function _trackedFocusSizePx(icao24, position) {
const camera = flightState._viewer?.camera;
const scene = flightState._viewer?.scene;
if (!camera?.positionWC || !position || !scene) return 28;
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
if (parts.rendering._modelOwnsVisual(icao24)) {
const spec = parts.rendering._modelSpec(flightState._flightData.get(icao24)?.klass);
const scale = trackedModelScaleForPixelCap({
baseScale: spec.scale,
nativeRadiusM: spec.nativeRadiusM,
rangeM,
viewportHeightPx: scene.canvas.clientHeight,
fovyRad: camera.frustum.fovy,
maximumPixelSize: TRACKED_MODEL_MAX_PX,
});
const focalLengthPx = scene.canvas.clientHeight / (2 * Math.tan(camera.frustum.fovy / 2));
const projectedDiameterPx = (2 * spec.nativeRadiusM * scale * focalLengthPx) / rangeM;
return Math.max(TRACKED_MODEL_MIN_PX, Math.min(TRACKED_MODEL_MAX_PX, projectedDiameterPx));
}
const billboard = flightState._trackedEntity?.billboard;
const time = flightState._viewer.clock.currentTime;
const width = billboard?.width?.getValue(time) ?? 28;
const height = billboard?.height?.getValue(time) ?? 28;
const scale = billboard?.scale?.getValue(time)
?? (CLASS_SCALE_2D[flightState._flightData.get(icao24)?.klass] || 1);
const scaleByDistance = billboard?.scaleByDistance?.getValue(time)
?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM);
return Math.max(width, height) * scale * distanceScale;
}
/**
* Per-frame-cached, discontinuity-smoothed tracked DISPLAY position. Cached by Cesium
* frame number so the position / rotation / trail-head callbacks share ONE computation
* (and one reconciliation-state update) per frame. Returns a stable module holder, or
* null when the aircraft has no fix.
* @param {string} icao24
* @returns {Cesium.Cartesian3|null}
*/
function _trackedDisplayPosition(icao24) {
const frame = flightState._viewer?.scene?.frameState?.frameNumber ?? -1;
if (frame === flightState._cachedDRFrame && icao24 === flightState._drReconcileIcao) return flightState._cachedDRPosition;
// Does the reconciliation state belong to THIS aircraft? (Capture before overwriting
// _drReconcileIcao, so a track switch doesn't inherit the old plane's _drPrevRaw.)
const sameTrack = flightState._drReconcileValid && flightState._drReconcileIcao === icao24;
const raw = _deadReckon(icao24, flightState._scratchDrRaw);
flightState._cachedDRCourse = flightState._drCourseDeg;
flightState._cachedDRSpeedMps = flightState._drSpeedMps;
flightState._cachedDRHold = flightState._drCourseHold;
flightState._cachedDRFrame = frame;
flightState._drReconcileIcao = icao24;
if (!raw) {
flightState._cachedDRPosition = null;
flightState._drReconcileValid = false;
clearFocusTarget('flights', icao24);
return null;
}
const nowMs = Date.now();
const info = flightState._flightData.get(icao24);
if (sameTrack) {
const dtSec = Math.max(0.001, (nowMs - flightState._drPrevMs) / 1000);
const speed = (info && info.velocity) || 0;
// Plausible single-frame motion (m): real velocity × frame Δt, ×4 slack + 25 m base.
const plausible = speed * dtSec * 4 + 25;
if (Cesium.Cartesian3.distance(raw, flightState._drPrevRaw) > plausible) {
// Discontinuity — re-anchor so the DISPLAYED position stays continuous, then decay.
Cesium.Cartesian3.subtract(flightState._drPrevDisplay, raw, flightState._drCorrection);
flightState._drCorrectionStartMs = nowMs;
}
} else {
Cesium.Cartesian3.fromElements(0, 0, 0, flightState._drCorrection);
flightState._drCorrectionStartMs = nowMs - DR_CORRECTION_MS; // fully decayed
}
const elapsed = nowMs - flightState._drCorrectionStartMs;
const factor = elapsed >= DR_CORRECTION_MS ? 0 : 1 - elapsed / DR_CORRECTION_MS;
let display = Cesium.Cartesian3.multiplyByScalar(flightState._drCorrection, factor, flightState._trackedPosHolder);
Cesium.Cartesian3.add(raw, display, display);
// Same display floor the fleet pass applies — otherwise selecting a correctly
// floored grounded billboard swapped it for an unfloored tracked entity and
// dropped the cyan target back under the mesh. Applied HERE, at the single
// point every VISUAL consumer's per-frame position is computed, so the
// readout anchor, detection bracket, trail head and follow-camera all read
// the one floored value (the anti-jitter contract forbids recomputing per
// consumer). NOT the single point for DATA: `_describeFlight` deliberately
// reports sensor truth — see the note there. Skipped while a tracked 3D model
// owns the visual: it rides groundSnap's one-shot sample and moving its input
// would force a re-sample (T7).
display = _floorGroundedDisplayPosition(icao24, info, display, parts.rendering._modelOwnsVisual(icao24), nowMs);
Cesium.Cartesian3.clone(raw, flightState._drPrevRaw);
Cesium.Cartesian3.clone(display, flightState._drPrevDisplay);
flightState._drPrevMs = nowMs;
flightState._drReconcileValid = true;
flightState._cachedDRPosition = display;
const focusSizePx = _trackedFocusSizePx(icao24, flightState._cachedDRPosition);
// Publish only the exact per-frame display cache the tracked entity/camera
// consumes. Re-running DR from a later frame phase recreates the historical
// target-vs-camera jitter bug.
publishFocusTargetFromCachedPosition({
ownerLayer: 'flights',
id: icao24,
scene: flightState._viewer?.scene,
camera: flightState._viewer?.camera,
displayPosition: flightState._cachedDRPosition,
widthPx: focusSizePx,
heightPx: focusSizePx,
});
return display;
}
/** The tracked plane's current display position WITHOUT recomputing — the exact value the
* follow-camera already settled on this frame (in the Viewer _onTick). getDetectableObjects + the
* readout run in postRender at a LATER frameNumber, so calling _trackedDisplayPosition there would
* re-run the dead-reckon on a fresh sample and double-advance the reconciliation → the label jitters
* against the now-stable plane (the model's jitter fix, resurfacing in the labels). Returns null when
* there's no valid fix for the tracked aircraft, so callers fall back to the billboard position. */
function _trackedDisplayCached() {
return (flightState._drReconcileValid && flightState._drReconcileIcao === flightState._trackedIcao) ? flightState._cachedDRPosition : null;
}
/**
* The position the tracked aircraft is VISUALLY at this frame — the translation its
* 3D model is actually rendering with when the model owns the visual, otherwise the
* cached dead-reckoned position the billboard uses.
*
* This exists because a grounded plane's model rides a one-shot ground snap while its
* billboard deliberately stays at the reported (buried) altitude — a ~100 m vertical
* split at an inland airport. Anything anchored to the display position while the model
* is what you can see drifts below the aircraft and only converges as the coarse floor
* cell warms ("the buoy"), sometimes never.
*
* It reads `modelMatrix`, which `_updateTrackedModel` already wrote this frame — no
* sampling, no `_modelDisplayPosition` call from postRender, and no new dead reckoning,
* so the follow-camera anti-jitter contract on `gevDisplayPosition` is untouched.
*/
function _trackedVisualCached() {
if (flightState._trackedIcao && parts.rendering._modelOwnsVisual(flightState._trackedIcao)) {
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
return modelVisualAnchor(
flightState._trackedModel.modelMatrix,
spec.visualCenterNative,
Number.isFinite(flightState._trackedModel.computedScale) ? flightState._trackedModel.computedScale : spec.scale,
flightState._trackedVisualPos,
);
}
return _trackedDisplayCached();
}
/** Trail endpoint for the rendered tracked owner. Brackets/readouts stay on
* the visual centre; only the trail moves to the model's lower-centre hardpoint. */
/** The tracked model's rendered bounding radius (m), or 0 when no model of this
* contact is drawing. Carries Cesium's effective `computedScale`, which may be
* above `scale` to satisfy minimumPixelSize — the trail head has to be judged
* against the size the operator SEES, not the nominal one. */
/** World-space origin of the tracked model, or null when no model of this
* contact is drawing. This is the centre the rendered bounding sphere is
* measured from, so the trail clip and the envelope agree on one frame. */
function _trackedModelCenterWorld() {
if (!flightState._trackedIcao || !flightState._trackedModel || !parts.rendering._modelOwnsVisual(flightState._trackedIcao)) return null;
return Cesium.Matrix4.getTranslation(flightState._trackedModel.modelMatrix, flightState._scratchTrailClip);
}
function _trackedModelEnvelopeM() {
if (!flightState._trackedIcao || !flightState._trackedModel || !parts.rendering._modelOwnsVisual(flightState._trackedIcao)) return 0;
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
const scale = Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale
: spec.scale;
return spec.nativeRadiusM * scale;
}
function _trackedTrailCached() {
if (flightState._trackedIcao && parts.rendering._modelOwnsVisual(flightState._trackedIcao)) {
const spec = parts.rendering._modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled
// half-correction put this offset on the lateral axis — see
// modelAnchorWorld).
return modelAnchorWorld(flightState._trackedModel, spec.trailAnchorNative, flightState._trackedTrailPos);
}
return _trackedDisplayCached();
}
/** Smoothed world course for the tracked aircraft this frame. Reads the
* frame-cached course (set by _trackedDisplayPosition — the follow-camera's
* own computation), NEVER re-runs _deadReckon. Safe to call more than once
* per frame: the second call sees dt≈0 and the limiter is a no-op.
*
* The smoothed value lives in the SHARED per-icao _displayCourse entry (see
* its declaration): on click the limiter continues from whatever nose the
* fleet pass was displaying, and on untrack the fleet pass continues from
* whatever nose this path last wrote — the tracked and fleet consumers of
* the same aircraft can never disagree across the handoff. */
function _trackedDisplayCourse() {
const info = flightState._flightData.get(flightState._trackedIcao);
const fallback = (info && info.true_track) || 0;
const cacheValid = flightState._drReconcileValid && flightState._drReconcileIcao === flightState._trackedIcao && flightState._cachedDRCourse != null;
const raw = cacheValid ? flightState._cachedDRCourse : fallback;
const nowMs = Date.now();
const dt = flightState._trackedCourseMs
? Math.min(COURSE_SLEW_DT_MAX_SEC, (nowMs - flightState._trackedCourseMs) / 1000)
: 0;
flightState._trackedCourseMs = nowMs;
const prev = flightState._displayCourse.get(flightState._trackedIcao);
// Hover hold: at near-zero displayed speed both the chord and the reported
// track are noise — keep the last stable nose direction instead of chasing.
if (cacheValid && flightState._cachedDRHold && prev != null) return prev;
const cap = courseSlewCapDps(cacheValid ? flightState._cachedDRSpeedMps : ((info && info.velocity) ?? NaN), COURSE_MAX_DPS);
const course = limitCourseStep(prev, raw, cap, dt);
flightState._displayCourse.set(flightState._trackedIcao, course);
return course;
}
/** Reset reconciliation + per-frame cache when tracking stops or switches
* target. Deliberately does NOT touch _displayCourse: the smoothed course
* entry belongs to the AIRCRAFT (not to the tracked session) and must
* survive the handoff back to the fleet pass. */
function _resetTrackedDisplay() {
flightState._drReconcileValid = false;
flightState._drReconcileIcao = null;
flightState._cachedDRFrame = -1;
flightState._cachedDRPosition = null;
flightState._cachedDRCourse = null;
flightState._cachedDRSpeedMps = null;
flightState._cachedDRHold = false;
flightState._trackedCourseMs = 0;
}
/**
* The floor to stand a grounded contact on while its own cell is unresolved.
*
* Owner directive (2026-08-21, after a Re:Earth outage buried a parked contact
* at a Texas field): "hold the last known altitude until a fresh one comes in.
* Never render otherwise." Two tiers, strongest first:
* own — a floor this contact's OWN cell resolved to while it stood there.
* Nothing weaker can improve on it, and its cell warming again is picked
* up by the live read, not here. Valid only within
* HELD_FLOOR_MAX_DRIFT_KM of where it was measured.
* neighbor — the LOWEST of at least two resolved ADJACENT cells (~111 m away,
* the same apron). Two at least, because one reading cannot be checked
* against anything, and the LOWEST because a high reading beside a cold
* cell is as likely to be a terminal roof as ground — see neighborFloorM.
*
* BOTH are validated measurements out of the shared floor cache. A third tier
* that read the rendered mesh directly, where no DEM existed to check it, was
* built and then REMOVED: run against a real GPU with the proxy down it
* recorded a coarse-LOD 20.6 m for ground that is really ~122 m (see the note
* in meshFloorSampler.js). Nothing in this chain is a guess.
*
* A neighbour hold keeps re-probing at the throttle so it can UPGRADE: a parked
* contact that first answered from one neighbour would otherwise keep it after
* a better one warmed. A held value is never dropped for nothing — a failed
* probe leaves the previous answer standing — and a re-probe that lands LOWER
* eases rather than steps (see the ease note in the clamp below).
*
* A REHYDRATED SEED is the one exception to the tier order. A floor parked by
* `_retireDisplayFloorState` and picked back up on a later re-ground was
* measured while the contact stood somewhere it no longer necessarily is: it is
* a memory, not a reading, and it outranks nothing. So `seeded` demotes an
* `own` floor below the neighbour tier — when two fresh adjacent cells can
* answer, they answer, and the seed is discarded. It serves only while nothing
* fresh contradicts it, which is exactly the flap case it exists for (a rotation
* outruns its own cells, so nothing nearby is warm either). Without this a
* contact that re-grounded 0.5 km away kept a 200 m floor while its new
* neighbourhood read 100 m and 105 m, and rendered 100 m in the air.
*
* @param {object} state - The contact's `_displayFloorState` entry (mutated).
* @param {{lat: number, lon: number}} cell - Cell the display is reading now.
* @param {number} nowMs - Tick clock.
* @returns {number|null} Floor to use, or null when nothing anywhere can answer.
*/
function _heldDisplayFloorM(state, cell, nowMs) {
if (state.heldTier && !(Number.isFinite(state.heldM) && state.heldCell
&& parts.queries._approxDistanceKm(cell.lat, cell.lon, state.heldCell.lat, state.heldCell.lon)
<= HELD_FLOOR_MAX_DRIFT_KM)) {
// Out of range: the held value is no longer a measurement of anywhere this
// contact has been. Drop it rather than stretch it.
_dropHeldFloor(state);
}
if (state.heldTier === 'own' && !state.seeded) return state.heldM;
if (state.probeMs != null && nowMs - state.probeMs < NEIGHBOR_FLOOR_PROBE_MS) return state.heldM;
state.probeMs = nowMs;
const near = neighborFloorM(cell);
if (near != null) return _adoptHeldFloorM(state, near, cell, 'neighbor');
return state.heldM;
}
/** Records a held floor and the tier it came from; returns it. Every caller
* passes a floor measured for the cell the contact is reading NOW, so an
* adoption always clears the seed flag: live evidence has arrived. */
function _adoptHeldFloorM(state, floorM, cell, tier) {
state.heldM = floorM;
state.heldCell = cell;
state.heldTier = tier;
state.seeded = false;
return floorM;
}
/** Forgets the held floor and everything that describes where it came from,
* leaving the rest of the contact's display state alone. */
function _dropHeldFloor(state) {
state.heldM = null;
state.heldCell = null;
state.heldTier = null;
state.seeded = false;
}
/** Whether a parked seed has been away longer than the grace window.
*
* ONE judgement, asked from BOTH sides of the park, because neither side sees
* the whole story on its own. While the contact keeps reporting, the retire
* path asks it and drops the entry. But a contact can be parked and then make
* no calls at all — off the poll for a long-haul cruise, out of the corridor
* radius, tab hidden — and then re-ground; nothing ran in between, so an
* expiry checked only on the retire path never fires and an arbitrarily old
* measurement walks back in (measured: parked 198 s, still reused). The
* rehydration side therefore asks the same question against the wall clock
* before it clears `retiredMs`.
* @param {object} state @param {number} nowMs - Tick clock. */
function _seedExpired(state, nowMs) {
return state.retiredMs != null && nowMs - state.retiredMs > FLOOR_SEED_GRACE_MS;
}
/** Retires a contact's display-floor state. Called the moment it stops being a
* grounded billboard — airborne, model-owned, or gone.
*
* The floor itself is kept as a rehydration seed for FLOOR_SEED_GRACE_MS (see
* above) and MARKED as one; everything that describes the contact's CURRENT
* rendering is cleared, so a re-ground recomputes from scratch and cannot be
* mistaken for a hold release. Nothing visual is touched, so the T7
* model-ownership gate is unaffected.
* @param {string} icao24 @param {number} nowMs - Tick clock. */
function _retireDisplayFloorState(icao24, nowMs) {
const state = flightState._displayFloorState.get(icao24);
if (!state) return;
if (state.retiredMs == null) {
state.retiredMs = nowMs;
state.seeded = true; // what it answers with next is a memory, not a reading
state.heldActive = false; // a later landing is an arrival, not a release
state.easedM = null;
state.easeMs = null;
state.probeMs = null; // re-ground may probe immediately
state.out = null;
state.effectiveM = null;
Cesium.Cartesian3.clone(Cesium.Cartesian3.ZERO, state.in); // invalidate the memo
} else if (_seedExpired(state, nowMs)) {
flightState._displayFloorState.delete(icao24); // airborne long enough to be anywhere
}
}
/**
* Floors a GROUNDED contact's DISPLAYED position onto the local ground.
*
* `renderAltitudeM` is chosen once per poll from the floor of the FIX's coarse
* cell. The position that renders is the dead-reckoned one, which drifts away
* from that fix for the whole segment — and for up to 300 s / several hundred
* metres while a ground contact coasts through its stale-feed grace. Across a
* graded apron (KAUS spans ~119–140 m ellipsoidal) that drift buries the
* sprite under the mesh it is now over; `scripts/qa-floor-verify.mjs` measured
* −15.5 m. A second, smaller share comes from the fix-time floor itself: a
* taxiing contact whose current cell is still cold falls back to the PREVIOUS
* fix's cell (see the grounded `surfaceM` chain) and nothing revisits that
* height once the cell warms — the stale re-floor sweep deliberately skips
* contacts present in the poll. Both are cured by reading the floor at the
* coordinate actually being displayed.
*
* Discipline:
* - READ-ONLY against the shared floor cache. No latch, no heal, no sampling.
* Keeping cold cells rare is `_collectDisplayCorridorCells`'s job, not this
* one's. A cold cell used to mean NO clamp at all, which was only safe while
* the un-clamped height was a real reading — and for a grounded contact with
* no altitude data it is not: the poll path's last resort is the geoid, tens
* of metres under the mesh at an inland field. When the cell cannot answer,
* `_heldDisplayFloorM` holds the last floor that DID (owner, 2026-08-21).
* Still never an invented surface: every tier is a measurement, and when
* none exists the position passes through as before.
* - Grounded contacts only. Airborne heights are the fix-time clamp's job.
* - NEVER when a 3D model owns the visual (T7): the model rides groundSnap's
* one-shot tileset sample and the billboard hides behind it, so clamping the
* hidden billboard would put a SECOND ground chain on one contact — and the
* one the operator is not looking at. (The original rationale was narrower:
* lifting the datum dragged groundSnap's input past its 50 m
* move-invalidation and forced a re-sample every frame. groundSnap now
* measures that distance on the ellipsoid, so a purely vertical change costs
* nothing; the gate stays for the reason above.) Same gate the military
* layer's grounded billboard lift uses.
*
* @param {string} icao24 - Contact key (owns one `_displayFloorState` entry).
* @param {object|null|undefined} info - `_flightData` record for this contact.
* @param {Cesium.Cartesian3|null} pos - Dead-reckoned display position.
* @param {boolean} modelOwnsVisual - Whether a 3D model is drawing this contact.
* @param {number} [nowMs] - Tick clock, passed by both callers so the release
* ease advances on the same clock the rest of the tick uses.
* @returns {Cesium.Cartesian3|null} `pos` itself when nothing moves (the common
* case — no allocation, no rebuild), otherwise the lifted position.
*/
function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs = Date.now()) {
if (!pos || !info?.onGround || modelOwnsVisual) {
_retireDisplayFloorState(icao24, nowMs);
return pos;
}
const state = flightState._displayFloorState.get(icao24);
const carto = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84, flightState._scratchDisplayCarto);
// Boundary hysteresis: a position jittering across a cell edge would flip
// floors at fleet-tick rate (see stickyFloorCell).
const cell = stickyFloorCell(
Cesium.Math.toDegrees(carto.latitude), Cesium.Math.toDegrees(carto.longitude), state?.cell,
);
const floor = cachedGroundFloor(cell.lat, cell.lon);
const next = state || {
cell, in: new Cesium.Cartesian3(), out: null, effectiveM: null,
heldM: null, heldCell: null, heldTier: null, heldActive: false, seeded: false,
probeMs: null, easedM: null, easeMs: null, retiredMs: null,
};
// Back on the ground. Judge the seed's AGE here, before `retiredMs` is
// cleared: a contact that made no calls while it was away never reached the
// retire path's own expiry branch, so this is the only place that can tell an
// hour-old measurement from a three-poll-old one.
if (_seedExpired(next, nowMs)) _dropHeldFloor(next);
// The seed (if any survived) is live again, and the drift bound plus the
// neighbour tier in the hold chain decide whether it still describes ground
// this contact is on.
next.retiredMs = null;
// The floor to clamp against: this cell when it has one, otherwise the last
// one that resolved for this contact (never the geoid the poll path fell to).
// Snapshot what the contact was standing on BEFORE the chain overwrites it —
// the ease decision below needs the previous value, and `_heldDisplayFloorM`
// adopts into the same fields.
const wasHeld = next.heldActive;
const stoodOnM = next.heldM;
let effective = floor;
if (Number.isFinite(effective)) {
_adoptHeldFloorM(next, effective, cell, 'own');
next.heldActive = false;
} else {
effective = _heldDisplayFloorM(next, cell, nowMs);
next.heldActive = Number.isFinite(effective);
}
// The floor moved DOWN under a contact that was standing on a BORROWED one.
// Dropping it by that difference in a single tick is the snap the owner asked
// not to have, so approach it instead. Two ways in, and both need it:
// - the real floor arrives below the hold (releasing the hold);
// - a re-probe finds a LOWER neighbour than the one being held, which the
// 12 m spread bound can make a large step on a mesa edge (200 m held, a
// 120 m neighbour warms, the bounded answer is 132 m) — and can happen
// AGAIN while the first approach is still running.
// Scoped to a borrowed floor on purpose: an ordinary cell-to-cell change
// between two resolved floors is the existing path and keeps its timing.
if (next.easedM == null && wasHeld && Number.isFinite(stoodOnM)
&& Number.isFinite(effective) && effective < stoodOnM) {
next.easedM = stoodOnM; // start from where the contact is actually drawn
next.easeMs = nowMs;
}
if (next.easedM != null) {
if (!Number.isFinite(effective) || effective >= next.easedM) {
// Nothing to approach, or the floor rose: take it whole and stop.
next.easedM = null;
next.easeMs = null;
} else {
// Exponential approach from the DISPLAYED value. The target may have
// moved since last tick; that changes only where this is heading, never
// where it is, so there is no seam to jump across.
const dtMs = Math.max(0, nowMs - next.easeMs);
next.easeMs = nowMs;
const closed = Math.min(FLOOR_EASE_MAX_STEP, 1 - Math.exp(-dtMs / FLOOR_EASE_TAU_MS));
let value = next.easedM + (effective - next.easedM) * closed;
if (Math.abs(value - effective) <= FLOOR_EASE_EPSILON_M) {
value = effective; // arrive exactly, so a parked contact stops rebuilding
next.easedM = null;
next.easeMs = null;
} else {
next.easedM = value;
}
effective = value;
}
}
// Same input position AND the same EFFECTIVE floor ⇒ the same answer as last
// tick, so skip the rebuild. A clamped stationary contact (parked, coasting
// on a zero-velocity fix) hits this every tick; without it the identical
// Cartesian was rebuilt at ~12 Hz forever. The test is deliberately on the
// OUTPUT of the hold chain, not on its inputs: keying it to the raw cell
// floor let a parked contact whose cell never warmed return a memoized
// unresolved answer forever, so an adjacent-cell floor warming later was
// never adopted. One owned entry per grounded
// contact — O(grounded), dropped on eviction, on destroy, and the moment the
// contact stops being a grounded billboard.
if (state && state.effectiveM === effective && Cesium.Cartesian3.equals(pos, state.in)) {
return state.out || pos;
}
const lifted = displayFloorHeightM(carto.height, effective);
next.cell = cell;
next.effectiveM = effective;
Cesium.Cartesian3.clone(pos, next.in);
if (lifted == null) {
next.out = null;
} else {
// The cache OWNS its output: returning a shared scratch would let the next
// contact in the fleet loop overwrite a position already handed out.
next.out = Cesium.Cartesian3.fromRadians(
carto.longitude, carto.latitude, lifted, Cesium.Ellipsoid.WGS84,
next.out || new Cesium.Cartesian3(),
);
}
if (!state) flightState._displayFloorState.set(icao24, next);
return next.out || pos;
}
/**
* Adds the cells each grounded contact's DISPLAY is about to render over to
* this poll's floor warm/sample batch.
*
* The poll loop otherwise collects FIX cells only, so the clamp above has data
* exactly where the contact ISN'T. A contact taxiing at 10 m/s crosses a
* ~111 m cell every ~11 s while the batch runs once per 30 s poll, so it stays
* permanently ahead of its own floor data (the "taxiing cache" failure mode, at
* cell granularity) and the clamp silently passes.
*
* The corridor therefore follows the direction the display is actually MOVING,
* which is not always toward the fix:
* - INTERPOLATING between two fixes — the display is walking to the newest
* fix, so that fix is the endpoint (exact, no projection error).
* - EXTRAPOLATING — coasting past the newest fix on a stale feed, or the
* pre-history warm-up — the display travels along its course AWAY from that
* fix. Aiming at the fix here warms the BACKTRAIL while the contact stays
* one sampling cycle ahead and buried, so the endpoint is the position its
* own kinematics put it at two poll intervals from now.
*
* Runs EVERY poll, warm cells included, for the same reason the fix cells do:
* `warmGroundFloor` skips cells that already have a real DEM, but the mesh
* sampler needs to see a cell again AFTER its DEM prior lands, since a sample
* without that prior is rejected. Offering a cell once would leave it DEM-only
* for the session — at fields where the photogrammetric mesh sits well above
* bare earth that is still metres of burial.
*
* Budgeting is need-ranked and dedupe-first: cells this poll already collected
* cost NOTHING (a parked contact's corridor is its own fix cell, so it never
* competes), candidates are ordered by how many of their cells are actually
* cold, and each takes at most DISPLAY_CORRIDOR_FAIR_SHARE before anyone takes
* seconds. Insertion-order spending starved the contacts that needed it most.
*
* Purely additive to the existing batch: same fire-and-forget DEM resolve, same
* one-shot DEM-validated mesh sampler. No latch, no heal.
*
* @param {Array<{lat: number, lon: number}>} out - This poll's warm points.
* @param {number|null} viewerLat @param {number|null} viewerLon - Viewer subpoint.
*/
function _collectDisplayCorridorCells(out, viewerLat, viewerLon) {
if (viewerLat == null || viewerLon == null) return;
// Cells the poll already collected (grounded + low-airborne fix cells).
const seen = new Set();
for (const p of out) {
const c = coarseFloorCoord(p.lat, p.lon);
seen.add(`${c.lat},${c.lon}`);
}
const candidates = [];
for (const [icao24, info] of flightState._flightData) {
if (!info?.onGround) continue;
// T7: a contact whose 3D model is the visual never reads a display floor.
if (parts.rendering._modelOwnsVisual(icao24)) continue;
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon)) continue;
if (parts.queries._approxDistanceKm(viewerLat, viewerLon, info.rawLat, info.rawLon) > DISPLAY_CORRIDOR_RADIUS_KM) continue;
const dr = _deadReckon(icao24, flightState._scratchCorridorPos);
if (!dr) continue;
// Read the sibling scratches IMMEDIATELY, before any other _deadReckon call.
const extrapolating = flightState._drExtrapolating;
const speedMps = Number.isFinite(flightState._drSpeedMps) ? flightState._drSpeedMps : (info.velocity || 0);
const courseDeg = flightState._drCourseDeg != null ? flightState._drCourseDeg : (info.true_track || 0);
const c = Cesium.Cartographic.fromCartesian(dr, Cesium.Ellipsoid.WGS84, flightState._scratchCorridorCarto);
const lat = Cesium.Math.toDegrees(c.latitude);
const lon = Cesium.Math.toDegrees(c.longitude);
const cells = corridorFloorCells(corridorPathLatLon({
extrapolating,
displayLat: lat,
displayLon: lon,
courseDeg,
speedMps,
// Same turn the dead-reckon integrates — a sustained-turn taxi leaves a
// straight tangent within a few hundred metres.
turnRateDps: info.turnRateDps || 0,
fixLat: info.rawLat,
fixLon: info.rawLon,
lookaheadSec: DISPLAY_CORRIDOR_LOOKAHEAD_SEC,
}));
let cold = 0;
for (const cell of cells) {
if (cachedGroundFloor(cell.lat, cell.lon) == null) cold += 1;
}
candidates.push({ cells, cold, speedMps });
}
flightState._corridorEpoch += 1;
for (const cell of allocateCorridorCells(
candidates, seen, DISPLAY_CORRIDOR_CELL_BUDGET, DISPLAY_CORRIDOR_FAIR_SHARE, flightState._corridorEpoch,
)) {
out.push(cell);
}
}
return { _refloorStaleGroundedContacts, _deadReckon, _extrapolateFix, _isTrackWarmingUp, _trackedFocusSizePx, _trackedDisplayPosition, _trackedDisplayCached, _trackedVisualCached, _trackedModelCenterWorld, _trackedModelEnvelopeM, _trackedTrailCached, _trackedDisplayCourse, _resetTrackedDisplay, _heldDisplayFloorM, _adoptHeldFloorM, _dropHeldFloor, _seedExpired, _retireDisplayFloorState, _floorGroundedDisplayPosition, _collectDisplayCorridorCells };
}
+74
View File
@@ -0,0 +1,74 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createCivilFlightLayer } from './index.js';
import { createFlightState } from './state.js';
import { createEnrichment } from './enrichment.js';
function services() {
const names = ['picking', 'sprites', 'trails', 'aircraftPresentation', 'camera',
'militaryRegistry', 'labels', 'groundFloor', 'meshFloor', 'geoid', 'focus',
'readout', 'context', 'render', 'recession'];
return { ...Object.fromEntries(names.map((name) => [name, {}])),
groundSnap: { createGroundSnap: () => ({ clear() {} }) } };
}
test('constructing independent flight layers performs no source request and isolates selection', () => {
let requests = 0;
const source = { label: 'Fixture aircraft', getSnapshot() { requests++; } };
const first = createCivilFlightLayer({ source, services: services() });
const second = createCivilFlightLayer({ source, services: services() });
first.testing._armFlightTrackingRestoreForTest('abc123');
assert.equal(first.testing._pendingFlightTrackingRestoreForTest(), 'abc123');
assert.equal(second.testing._pendingFlightTrackingRestoreForTest(), null);
first.testing._setFlightTrackingRefreshOutcomeForTest({ ids: ['abc123'] });
assert.notEqual(first.testing, second.testing);
assert.equal(first.getStats().source, 'Fixture aircraft');
assert.equal(requests, 0);
});
test('a layer without a source fails before touching the viewer', () => {
const layer = createCivilFlightLayer({ services: services() });
assert.throws(() => layer.init({}), /snapshot source/);
});
test('flight state owns distinct mutable records, scratch objects and floor samplers', () => {
const first = createFlightState({ services: services() });
const second = createFlightState({ services: services() });
for (const key of ['_flightData', '_billboards', '_positionHistory', '_displayFloorState',
'_groundSnap', '_scratchCarto', '_models', '_activeUpdateControllers', 'lifetime']) {
assert.notEqual(first[key], second[key], key);
}
});
test('aborted enrichment cannot write into a later lifecycle or decrement its active count', async () => {
let resolve;
let signal;
const source = { getEnrichment(query, options) {
signal = options.signal;
return new Promise((done) => { resolve = done; });
} };
const flightState = createFlightState({ source, services: services() });
const enrichment = createEnrichment({ flightState, services: {}, parts: {} });
let writes = 0;
enrichment._enqueueEnrich('test', { kind: 'type', id: 'abc123' }, () => writes++);
await Promise.resolve();
assert.equal(flightState._enrichActive, 1);
flightState.lifetime.abort();
flightState.lifetime = new AbortController();
flightState._enrichActive = 0;
resolve({ found: true });
await new Promise((done) => setImmediate(done));
assert.equal(signal.aborted, true);
assert.equal(writes, 0);
assert.equal(flightState._enrichActive, 0);
});
test('a synchronous enrichment failure stays bounded and releases its slot', async () => {
const flightState = createFlightState({ services: services(), source: {
getEnrichment() { throw new Error('fixture failure'); },
} });
const enrichment = createEnrichment({ flightState, services: {}, parts: {} });
assert.doesNotThrow(() => enrichment._enqueueEnrich('test', {}, () => assert.fail()));
await new Promise((done) => setImmediate(done));
assert.equal(flightState._enrichActive, 0);
});
+468
View File
@@ -0,0 +1,468 @@
import * as Cesium from 'cesium';
export const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
/** Amber tint for known-military aircraft rendered by this layer (matches the military layer's icon color). */
export const MIL_TINT = Cesium.Color.fromCssColorString('#FFB800');
// --- 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
// silhouette + rotation pipeline, clickable/trackable/detectable, sticky metadata
// updating normally. Landing/takeoff is a TRANSITION — the on_ground flip restyles
// the existing billboard in place, never a removal. Ground planes draw no trails
// and are excluded from the ambient enrichment sweep (click-to-enrich still
// works). In 3D mode they take model slots like airborne planes (owner decision
// 2026-07-03 — no air/ground distinction), placed by the one-shot ground snap
// (see _modelDisplayPosition).
//
// TINT: full-strength, same pipeline as airborne (white / amber-military /
// cyan-tracked). Day 1 shipped a slate-gray 50%-alpha "muted" ground tint; the
// owner killed it the same day ("just leave them as white, dude … in NYC I can
// barely see them, extremely grayed out"). "On the ground" reads from the ×0.8
// scale + missing trail; "feed-dropped, coasting" stays the 45%-alpha stale
// fade — a full-alpha ground icon can never be confused with it.
/** Ground billboards render slightly smaller so airport clutter stays visually minor. */
export const GROUND_SCALE = 0.8;
// --- 3D model rendering (B3) ---------------------------------------------------------
// When enabled, aircraft render as 3D glTF models once the camera is below MODEL_ALT_CEIL_M
// (zoomed in); higher up they stay flat billboards. Eligibility is FRUSTUM-based (on-screen), and
// the slots go to either the nearest planes ('proximity') or every in-view plane ('all'), each
// backed by a hard cap so a draw-call explosion can't tank the frame (no instancing yet).
export const PLANE_MODEL_URL = '/models/airplane.glb';
export const MODEL_ALT_CEIL_M = 800000;
// m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
export const MODEL_MIN_PX = 24;
// floor so distant models stay visible WITHOUT ballooning into a giant
// min-pixel blob (was 54 — far planes at the All radius became white
// star-bursts); ~matches the 2D icon size so the model↔billboard read is consistent
export const TRACKED_MODEL_MIN_PX = 40;
// keep the glTF silhouette comparable to the selected 2D glyph at handoff
export const TRACKED_MODEL_MAX_PX = 200;
// owner-selected close-range tracked-target feel
export const MODEL_NATIVE_RADIUS_M = 34.41;
export const MODEL_SCALE = 1;
// airplane.glb is transform-applied and baked to real-world meters
// Per-mode caps. Each model is its own draw call (no instancing yet), so these bound the frame cost.
export const MODEL_MAX = 150;
// 'proximity' cap (the planes immediately around you)
export const MODEL_MAX_ALL = 350;
// 'all' cap (everything out to ~the horizon)
// Per-mode ADD / KEEP radii. The two modes differ by RADIUS, not just cap — otherwise they look
// IDENTICAL whenever fewer than a cap's worth of planes are in range (field bug: Proximity and All
// rendered the same). 'proximity' = a tight ring; 'all' = roughly to the horizon (state-scale). Each
// band has hysteresis (KEEP > ADD) so a plane doesn't release+reload its model when it straddles the
// add edge (the zoom/pan flicker). Beyond ADD a model would force-clamp to minimumPixelSize into a
// giant floating blob (the old 422 km airport-cluster bug), so far planes stay 2D dots; the cap +
// on-screen priority then spend the model slots on planes you can actually see.
export const MODEL_PROX_ADD_M = 150000;
// proximity: model NEW planes within 150 km
export const MODEL_PROX_KEEP_M = 185000;
// proximity: KEEP modeled planes out to 185 km
export const COCKPIT_MODEL_MAX = 60;
// max concurrent GLBs in cockpit (never raises the map cap)
export const MODEL_ALL_ADD_M = 400000;
// all: model NEW planes within 400 km (~to the horizon)
export const MODEL_ALL_KEEP_M = 450000;
// all: KEEP modeled planes out to 450 km
export const MODEL_HEADING_OFFSET_DEG = 180;
// airplane.glb nose is opposite Cesium heading-0
// Owner launch-polish direction: models should read as clean light silhouettes,
// with only a weak diffuse contribution from the existing approved textures.
export const MODEL_COLOR_BLEND_AMOUNT = 0.94;
// Grounded-model belly offset: airplane.glb's centred origin sits 6.719 m ABOVE its
// lowest vertex (glTF Y-up scene AABB with node transforms applied — same reader as
// modelScale.test.mjs, measured after its 24× transform bake). × class multiplier ≈ 5.0–9.7 m
// of lift, so a ground-snapped model rests its lowest geometry (gear/belly) ON the sampled
// tile skin instead of sinking to the fuselage-centerline origin. Locked against the GLB
// by modelScale.test.mjs.
export const MODEL_BELLY_OFFSET_NATIVE = 6.719;
export const CYAN_TRANSPARENT = Cesium.Color.CYAN.withAlpha(0);
/** @constant {number} ERROR_BACKOFF_INTERVAL - Cooldown (ms) after transient errors */
export const ERROR_BACKOFF_INTERVAL = 20000;
// transient error retry
/** @constant {number} POSITION_HISTORY_LIMIT - Max position samples kept per aircraft for dead reckoning */
export const POSITION_HISTORY_LIMIT = 5;
export const COCKPIT_CONTACT_SIZE_PX = 6;
export const COCKPIT_CIVILIAN_COLOR = Cesium.Color.fromCssColorString('#DCEEFF');
export const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = new Cesium.NearFarScalar(
1000, 3.0, 8000000, 0.5,
);
// ---------------------------------------------------------------------------
// Track-history trail state (PRD WS-F F1/F4): a fading polyline behind the
// tracked aircraft. The accumulation array is intentionally SEPARATE from
// _positionHistory (capped at POSITION_HISTORY_LIMIT=5 for dead reckoning)
// so the visible trail can grow to TRAIL_MAX_POINTS fixes.
// ---------------------------------------------------------------------------
/** @constant {string} Civilian trail hue (PRD F4, pinned). */
export const TRAIL_COLOR = '#00d4ff';
/** @constant {number} Combined cap on trail vertices (backfill + live accumulation). */
export const TRAIL_MAX_POINTS = 400;
// ---------------------------------------------------------------------------
// Render-behind smoothing (PRD WS-C C2 — approved product decision):
// the fleet renders at now - RENDER_DELAY_SEC so positions interpolate
// BETWEEN two known fixes instead of extrapolating ahead and snapping back
// when the next poll lands. All consumers (labels, HUD, detection,
// frame_overhead) share this delayed clock; removing the delay reintroduces
// the back/forward oscillation and is a regression.
// ---------------------------------------------------------------------------
/** @constant {number} Display latency in seconds (= one poll interval). */
export const RENDER_DELAY_SEC = 30;
/** @constant {number} Polls an aircraft may miss before removal (transient OpenSky dropouts). */
export const MISSING_POLL_LIMIT = 3;
// --- Landed-plane fast cull (owner field report 2026-07-02: "phantom" planes
// lingered ~2 min at airports after touchdown). OpenSky's on_ground flag LAGS
// the actual landing, so a landed plane's last airborne-classified fixes show
// it low + slow on the runway; when such a plane then drops out of the poll,
// it has landed (the feed reclassified it to ground traffic we filter out),
// not hit a transient gap — evict after ONE missed poll instead of the full
// grace. Thresholds: below ~150 m baro (MSL, so this only fires near
// sea-level-ish fields — deliberately conservative; a high-elevation airport
// ghost just falls back to the normal grace) AND below ~45 kts ground speed
// (≈23 m/s — rollout/taxi; nothing in normal FLIGHT is this slow, so cruise
// planes always keep the full grace that absorbs real feed gaps).
/** @constant {number} Max baro altitude (m, MSL) for the landed fast cull. */
export const LANDED_ALT_MAX_M = 150;
/** @constant {number} Max ground speed (m/s) for the landed fast cull (~45 kts). */
export const LANDED_SPEED_MAX_MPS = 23;
/** @constant {number} Missed-poll allowance for likely-landed planes (1 = removed on the first missed poll). */
export const LANDED_MISSING_POLL_LIMIT = 1;
// Field-test rounds 1+3 (2026-07-06): below-ground floor clamp scope. Only
// contacts rendering below the alt ceiling are ever clamped/warmed (terrain
// tops out well under it outside the extreme Himalaya; cruise traffic can't
// be below ground and costs zero lookups). The radius bounds the FLEET clamp
// to viewer-visible traffic — clamping thousands of global contacts would
// need unbounded terrain resolution (the tracked contact clamps regardless).
/** @constant {number} Max render altitude (m, ellipsoidal) eligible for the ground-floor clamp. */
export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500;
/** @constant {number} Max viewer distance (km) for the fleet ground-floor clamp. */
export const GROUND_FLOOR_CLAMP_RADIUS_KM = 150;
// ---------------------------------------------------------------------------
// Icon orientation (2026-06-10 playtest fix): rotation is computed by
// projecting each aircraft's course vector into WINDOW coordinates
// (iconOrientation.js) with alignedAxis always ZERO — exact at every camera
// pitch/heading, including tracked-entity orbit mode. Rotation passes run on
// the fleet tick only when the camera pose changed (or 1s drift catch-up).
// The same tick horizon-culls billboards: with the Cesium globe hidden there
// is no far-side depth, so planes otherwise show through the planet.
// ---------------------------------------------------------------------------
/** @constant {number} Fleet dead-reckoning tick interval (ms) — ~12Hz, not per-frame. */
export const FLEET_DR_INTERVAL_MS = 80;
/** @constant {number} Max ms between rotation passes while the camera is idle. */
export const ROTATION_REFRESH_MS = 1000;
/** Max course slew (deg/s) — well above real turns (≤4°/s), hides fix-boundary
* steps. Scaled down toward COURSE_MIN_DPS at low speed (courseSlewCapDps). */
export const COURSE_MAX_DPS = 60;
/** Never spend a long render stall's full elapsed time in one visible course step. */
export const COURSE_SLEW_DT_MAX_SEC = 0.25;
// ---------------------------------------------------------------------------
// adsbdb enrichment (best-effort, fail-silent). Bounded fan-out: max 4
// concurrent requests, dispatches dripped ≥ENRICH_DISPATCH_GAP_MS apart
// (≤5/s — adsbdb is a free community API; the dev-server proxy additionally
// caches per-key on disk forever, negative results included, so repeat
// sessions never re-hit adsbdb). Each key is requested at most once per
// session. Priority jobs (tracked plane, model-eligible planes) jump the
// queue; the ambient fleet sweep (below) fills the back at poll cadence.
// ---------------------------------------------------------------------------
export const ENRICH_MAX_INFLIGHT = 4;
/** Min ms between request dispatches — the drip that bounds the fan-out to ≤5/s. */
export const ENRICH_DISPATCH_GAP_MS = 200;
// ---------------------------------------------------------------------------
// Ambient fleet type enrichment (2026-07-02 field data: OpenSky's live
// category field is 0/"no info" for ~94% of planes, so ambient classification
// defaulted nearly the whole fleet to the airliner silhouette). Each poll,
// ON-SCREEN planes — same horizon-occluder + frustum tests the fleet tick's
// model-eligibility pass uses; no new per-plane raycast — that haven't been
// requested this session are enqueued NEAREST-TO-CAMERA FIRST, bounded by:
// - the shared queue's 4-concurrent / 200 ms-drip dispatch (above),
// - ≤ ENRICH_AMBIENT_PER_SWEEP new enqueues per poll (= one poll interval
// of drip, so the backlog can't outgrow a poll and re-sorts fresh), and
// - a ROLLING token-bucket budget (below) bounding the sustained ambient
// request rate (repeat sessions resolve instantly from the proxy's
// permanent disk cache).
// Fail-silent by contract: the sweep never throws into the poll loop, never
// blocks rendering, and never touches tracking state. When a type answer
// lands, _requestTypeEnrichment's callback swaps the billboard glyph + scale
// in place (bb.scale composes multiplicatively with scaleByDistance).
// ---------------------------------------------------------------------------
// Rolling ambient budget (2026-07-03 field fix). The old ONE-SHOT session cap
// (300, refilled only in init) burned out in the first two polls of a busy
// region and never recovered — an hours-long session showed airliner
// monoculture in every NEW region until planes were clicked (the tracked path
// is uncapped). Token bucket instead: starts full at the ceiling, refills
// ENRICH_AMBIENT_REFILL_TOKENS every ENRICH_AMBIENT_REFILL_WINDOW_MS, clamped
// at the ceiling (no banking beyond one bucket). Numbers: 150 / 5 min sustains
// 0.5 req/s worst case — an order of magnitude under the 5/s drip that (with
// the 4-concurrent limit + the proxy's permanent disk cache) is the REAL
// politeness bound on adsbdb; the 300 ceiling preserves the old first-look
// burst so a fresh region still classifies quickly.
/** Bucket ceiling: max ambient tokens held at once (= the initial burst). */
export const ENRICH_AMBIENT_BUDGET_CEIL = 300;
/** Tokens added back per refill window. */
export const ENRICH_AMBIENT_REFILL_TOKENS = 150;
/** Refill window length (ms). */
export const ENRICH_AMBIENT_REFILL_WINDOW_MS = 5 * 60 * 1000;
/** Max new ambient enqueues per poll sweep (≈ rate × poll interval). */
export const ENRICH_AMBIENT_PER_SWEEP = 150;
// ---------------------------------------------------------------------------
// Tracked-display reconciliation. _deadReckon gives the RAW position from real
// fixes; at the warm-up→interpolation handoff (and on feed glitches / backfill
// splices) that raw value can step discontinuously. We absorb a step into a
// correction offset that decays to zero over DR_CORRECTION_MS, so the tracked
// icon, camera, and trail head never visibly jump — with ZERO steady-state lag
// (the correction stays ~0 whenever motion is already continuous).
// ---------------------------------------------------------------------------
export const DR_CORRECTION_MS = 900;
/** Bounded on-demand loading for the tracked model. The tracked regime is
* DEFAULT-ON and its driver runs every `scene.preUpdate`, so a missing or
* corrupt GLB — or a dead network — would otherwise spin load→reject at frame
* rate for as long as the contact stays selected. Failures are counted PER
* SELECTION: a short backoff absorbs a transient blip, then the layer stops
* asking until the operator selects something else. The billboard is the
* visual throughout (the handoff only fades it once a model actually renders),
* so a latched failure degrades to exactly the pre-3D presentation. */
export const TRACKED_MODEL_MAX_LOAD_FAILS = 3;
export const TRACKED_MODEL_RETRY_BACKOFF_MS = 1500;
/** @constant {number} Cap on NEW cells the display corridors may add to one
* poll's warm/sample batch — a view full of ground traffic must not balloon
* it. Cells the poll already collected are free (deduped before budgeting). */
export const DISPLAY_CORRIDOR_CELL_BUDGET = 64;
/** @constant {number} Cells any single contact may claim in the first pass, so
* one long corridor cannot spend the whole budget while other contacts get
* nothing. Leftovers are handed out in a second pass. */
export const DISPLAY_CORRIDOR_FAIR_SHARE = 4;
/** @constant {number} How far ahead a COASTING contact's corridor reaches:
* two poll intervals, so the ground it covers before the next batch lands is
* already warm. */
export const DISPLAY_CORRIDOR_LOOKAHEAD_SEC = RENDER_DELAY_SEC * 2;
/** @constant {number} Corridors are only collected this close to the viewer:
* the mesh sampler ignores anything past 15 km, and a far contact's exact
* datum is subpixel. */
export const DISPLAY_CORRIDOR_RADIUS_KM = 25;
/** @constant {number} How far a contact may travel from the cell that supplied
* its held floor before that floor stops describing the ground under it.
*
* Sized to the same worst-case ground segment `CORRIDOR_MAX_CELLS` is sized
* for — a 27 m/s rollout covers ~810 m in one poll interval — with headroom
* for a couple of polls of coasting. That is ~9 cells: aprons, taxiways and
* runways really are flat at that scale. An early draft used 5 km, which is
* ~45 cells and can leave the airfield entirely — the KAUS note in this file
* records a 21 m spread across the field alone — so the bound is the distance
* the contact can actually have travelled since the measurement rather than a
* comfortable-looking number. */
export const HELD_FLOOR_MAX_DRIFT_KM = 1;
/** @constant {number} Minimum gap between adjacent-cell probes for one contact
* whose held floor is missing or came from a borrowed tier. A contact standing
* on its own resolved floor never probes at all.
*
* This per-contact throttle is the ONLY rationing on the probe path, and that
* is deliberate. A probe is eight synchronous `Map` reads against the shared
* floor cache — no fetch, no `sampleHeight`, nothing async — so it cannot
* queue work anywhere; every DEM request is driven by `warmGroundFloor` from
* the poll loop, already bounded by DISPLAY_CORRIDOR_CELL_BUDGET and the
* resolver's single-flight chain. Measured worst case (`scripts/qa-floorhold-
* probe-cost.mjs`): 200 synchronized all-cold contacts probing on the SAME
* tick cost 2.1 ms, 2.6% of one 80 ms fleet tick, and 2.1 ms per second of
* wall clock sustained under this throttle. An earlier draft added a global
* per-tick budget with a fairness queue on top of that; it protected ~2 ms and
* cost two starvation defects, so it was deleted. Nothing can starve here
* because there is no shared resource to be starved of. */
export const NEIGHBOR_FLOOR_PROBE_MS = 500;
/** @constant {number} Time constant of the downward floor ease. A floor that
* drops UNDER a contact standing on a borrowed one is approached
* exponentially: each tick closes `1 - e^(-dt/TAU)` of the remaining gap, so
* at the 80 ms fleet cadence a tick moves ~20% of what is left and the value
* is within a few centimetres inside ~1.6 s. FLOOR_EASE_MAX_STEP caps that
* fraction so a delayed tick cannot close more.
*
* Exponential rather than a fixed-duration interpolation because the target
* MOVES: a second, lower neighbour can warm mid-ease. A from/duration ease
* re-evaluated against a new target jumps by the eased fraction of the change
* (measured: a 100 m single-tick drop late in an ease). Approaching from the
* CURRENTLY DISPLAYED value has no such seam — retargeting is just a different
* destination for the same continuous follow, and the per-tick bound holds
* however often the target moves.
*
* Rises are never eased: up is the safe direction, and easing up would park
* the contact under the mesh for the duration — the exact failure this whole
* path exists to prevent.
*
* Reachability note (2026-08-21, after neighborFloorM moved to a low lean):
* the ordinary arrival flows no longer produce a downward move at all — a
* borrowed floor is now the apron rather than a roof, so the contact rises
* once and stays (`scripts/qa-floorhold-staircase.mjs`: one step, zero float,
* in every scenario that settles). This machinery still guards the re-latch
* paths — a lower neighbour warming later, or an own-cell resolve below a
* hold — which the pins exercise directly. Whether those paths are worth the
* code is a follow-up judgement, deliberately not made in this change. */
export const FLOOR_EASE_TAU_MS = 360;
/** @constant {number} Hard ceiling on the fraction of the remaining gap ONE
* tick may close, whatever its dt. The exponential alone is timing-dependent:
* it closes 19.9% at the 80 ms fleet cadence but 28.4% at 120 ms and 75% after
* a 500 ms stall (a hidden tab, a long frame, a GC pause), which would turn a
* delayed tick back into the snap this approach exists to prevent. Clamping
* the fraction rather than dt keeps the never-snap property a property of the
* code instead of a property of the schedule; a stalled tick simply resumes
* the approach at full rate rather than jumping most of the way. */
export const FLOOR_EASE_MAX_STEP = 0.22;
/** @constant {number} Distance from the target at which the ease finishes
* exactly, so a parked contact stops rebuilding its position. Two orders of
* magnitude under GROUND_FLOOR_LIFT_M — invisible. */
export const FLOOR_EASE_EPSILON_M = 0.02;
/** @constant {number} How long a retired hold stays usable as a rehydration
* seed — three poll intervals.
*
* Deleting the state outright was the first cut, and an owner sighting found
* what that costs: VIR138M at JFK, 45 kt down the runway, "clearly on good
* ground, then suddenly popped below the ground, then popped back up".
* OpenSky's `on_ground` flag is not clean through a rotation — it flaps — and
* the fix's own height source switches at the same moment, from the resolved
* surface to baro + geoid N, which at a sea-level field IS the geoid and sits
* below the runway. A single airborne poll therefore wiped the only thing that
* was hiding that: the contact came back grounded with no prior, outrunning
* its own floor cells at 23 m/s, and rendered at the geoid until something
* ahead of it warmed (`scripts/qa-floorhold-staircase.mjs` §F1: 12 of 22
* grounded ticks below the runway, and not recovering).
*
* So a retired hold is PARKED, not destroyed, and a contact that re-grounds
* soon after picks it back up. What makes that safe is the bound already in
* `_heldDisplayFloorM`: a seed only answers within HELD_FLOOR_MAX_DRIFT_KM of
* where it was measured AND only while no fresh neighbour contradicts it, so a
* genuine departure-and-landing-elsewhere still starts clean — the grace window
* is belt to those braces, retiring the seed outright once a contact has been
* airborne long enough to have gone anywhere. */
export const FLOOR_SEED_GRACE_MS = 90_000;
/** Flip the whole 3D fleet's boost state by dropping models so the eligibility
* pass reloads them with creation-time boost options (both directions — a
* boosted model must not stay flat white back in Normal). Destroying 350
* GPU-backed models synchronously inside the style handler stalls the render
* thread (review P1; same failure the cockpit path documents), so the release
* is BATCHED through the fleet tick: each tick drops a bounded slice, showing
* each plane's billboard first (gap-proof per plane, no double-image window).
* Models are tagged with the boost state they loaded under, so queue entries
* whose model already matches the current state (rapid style cycling, or a
* reload that already happened) are skipped. In-flight loads are invalidated
* immediately (cheap gen bumps); the tracked model is a single primitive and
* reloads synchronously. */
export const IR_RELOAD_BATCH = 40;
+774
View File
@@ -0,0 +1,774 @@
import { displayedKinematics } from '../../data/motionModel.js';
import * as Cesium from 'cesium';
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
import { rankContactMatch, contactMatchWins, CONTACT_MATCH_TIER } from '../../data/contactMatch.js';
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
import { LANDED_ALT_MAX_M, LANDED_SPEED_MAX_MPS, FOCUS_EVIDENCE_DEV, FLEET_DR_INTERVAL_MS } from './policy.js';
export function createQueries({ flightState, services, parts, layer, resolveAsset }) {
const { tr3bTypeLabel, tr3bAircraftClass } = services.aircraftPresentation;
const { formatFlightLevel } = services.labels;
const { isMilitaryIcao } = services.militaryRegistry;
const { applyTrackedCameraFrame } = services.camera;
const { setFocusDeemphasisParams, getFocusDeemphasisParams, setFocusEvidenceNowMs, advanceFocusEvidenceNowMs } = services.focus;
const { setAircraftRecessionParams, getAircraftRecessionParams } = services.recession;
/**
* Cheap equirectangular distance (km) — plenty accurate for the ~150 km
* ground-floor clamp gate; runs once per contact per poll, so no trig-heavy
* haversine needed.
* @param {number} lat1 @param {number} lon1 @param {number} lat2 @param {number} lon2
* @returns {number} Approximate great-circle distance in km.
*/
function _approxDistanceKm(lat1, lon1, lat2, lon2) {
const dLat = (lat2 - lat1) * 111.32;
const dLon = (lon2 - lon1) * 111.32 * Math.cos(((lat1 + lat2) / 2) * Math.PI / 180);
return Math.hypot(dLat, dLon);
}
/**
* True when the aircraft's latest metadata reads "on or about the runway"
* (low + slow) — see the landed fast-cull rationale above. Both gates must
* hold, so a plane missing either datum keeps the normal grace.
* @param {string} icao24 - ICAO 24-bit transponder address.
* @returns {boolean}
*/
function _likelyLanded(icao24) {
const info = flightState._flightData.get(icao24);
if (!info) return false;
// Round 7 (owner: "fewer planes than OpenSky's own map" + "parked planes
// never heal"): the fast cull only applies to contacts that were AIRBORNE
// this session — its original target, the post-LANDING ghost. OpenSky's
// ground coverage flaps constantly, so fast-culling every grounded contact
// put parked planes in an evict/re-enter churn: each re-entry was a
// brand-new contact with cold floor state (geoid-height first poll), so
// the apron looked half-empty AND perpetually sunken. First-seen-grounded
// contacts now ride the normal MISSING_POLL_LIMIT grace and keep their
// identity (and warmed floors) across feed flaps.
if (info.wasAirborne !== true) return false;
// Was airborne, then grounded, then VANISHED from the poll — landed ghost.
if (info.onGround) return true;
return Number.isFinite(info.altitude) && info.altitude < LANDED_ALT_MAX_M
&& Number.isFinite(info.velocity) && info.velocity < LANDED_SPEED_MAX_MPS;
}
/**
* Normalize a value to a trimmed string. A whitespace-only field (" ") is
* truthy, so every label chain must trim FIRST and then fall through.
* @param {*} value - Any value (typically a metadata string or null).
* @returns {string} Trimmed string, or '' if falsy.
*/
function _toCleanText(value) {
return String(value || '').trim();
}
/**
* The layer's ONE label convention: spoken callsign → tail registration → raw
* ICAO hex. Mirrors militaryFlights.js so the same aircraft reads identically
* in both layers.
*
* Registration is aircraft IDENTITY, not route, so unlike the origin/destination
* line it is NOT plausibility-gated — an adsbdb tail number describes the
* airframe itself and cannot go stale the way a leg can.
*
* This is a DISPLAY string only. Identity everywhere in this layer is `icao24`
* (the `_billboards`/`_flightData` key, the `sourceId` detection declutter hashes,
* the `id` that trackById/Context cohorts resolve) — never the label.
* @param {string} icao24 - ICAO 24-bit transponder address (the identity key).
* @param {Object|null|undefined} info - `_flightData` record for this aircraft.
* @returns {string} Display label; never empty.
*/
function _contactLabel(icao24, info) {
return _toCleanText(info?.callsign) || _toCleanText(info?.registration) || icao24;
}
/**
* Build a public descriptor for one aircraft using its best current position
* (dead-reckoned when history exists, billboard position otherwise).
* @param {string} icao24 - ICAO 24-bit transponder address.
* @returns {{icao24: string, callsign: string|null, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
* Descriptor with a cloned position, or null if the aircraft is unknown.
*/
function _describeFlight(icao24) {
// DATA, NOT PIXELS — deliberately UNFLOORED (2026-08-19). The display floor
// lifts grounded contacts onto the visible mesh so the sprite you see is not
// buried; that is a rendering correction, not a measurement. This descriptor
// feeds query/analyst/subject APIs — `findByQuery` (voice track-by-name),
// `getTrackedInfo` (cockpit + readout altitude), `getTrackedSubject`
// (proximity counts and distances) — where the honest answer is what the
// aircraft REPORTED, not where its icon was nudged to avoid clipping tiles.
// `altitudeM` is therefore the barometric/aviation value and `renderAltitudeM`
// the fix-time datum, neither of them the floored display height. No
// user-visible surface renders `position` as the plane's on-screen location —
// every visual consumer reads the floored per-frame cache instead (see
// `_trackedDisplayPosition`). If that ever changes, floor this path too.
const info = flightState._flightData.get(icao24);
const bb = flightState._billboards.get(icao24);
const basePos = parts.motion._deadReckon(icao24) || (bb ? bb.position : null);
if (!basePos) return null;
const displayed = displayedKinematics({
derivedSpeedMps: flightState._drSpeedMps,
derivedTrackDeg: flightState._drCourseDeg,
reportedSpeedMps: info?.velocity,
reportedTrackDeg: info?.true_track,
});
const carto = Cesium.Cartographic.fromCartesian(basePos, Cesium.Ellipsoid.WGS84, flightState._scratchCarto);
if (!carto) return null;
return {
icao24,
callsign: String(info?.callsign || '').trim() || null,
position: Cesium.Cartesian3.clone(basePos),
latitude: Cesium.Math.toDegrees(carto.latitude),
longitude: Cesium.Math.toDegrees(carto.longitude),
// The cockpit instrument reports aviation altitude, not the Cesium
// ellipsoid height of the camera/ground-clamped render position. The
// latter can be slightly negative over terrain near the surface.
altitudeM: Number.isFinite(info?.altitude) ? info.altitude : carto.height,
renderAltitudeM: Number.isFinite(info?.renderAltitudeM) ? info.renderAltitudeM : carto.height,
onGround: info?.onGround === true,
velocityMps: displayed.speedMps,
track: displayed.trackDeg,
stale: Boolean(flightState._missingPolls.get(icao24) || flightState._backoff),
airline: info?.airline ?? null,
// CLASS label follows the TR-3B conversion so every downstream card
// (cockpit, Contacts, analyst) agrees with the triangle on screen.
typeName: tr3bTypeLabel(icao24, info?.typeName ?? null),
typeCode: tr3bTypeLabel(icao24, info?.typeCode ?? null),
// IDENTITY, deliberately NOT converted: registration is the airframe's tail
// number and feeds `_contactLabel`'s callsign → registration → hex chain, so
// a converted contact keeps the label convention every other contact uses.
// Trimmed like `callsign` above so every consumer (cockpit readout, voice
// narration, getTrackedSubject) can use it as a label link without
// re-guarding a whitespace-only enrichment value.
registration: _toCleanText(info?.registration) || null,
origin: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.origin.code : null,
destination: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.destination.code : null,
route: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? {
origin: { ...info.route.origin },
destination: { ...info.route.destination },
} : null,
};
}
function _normalizeTrackedIcao(candidate) {
const normalized = String(candidate ?? '').trim().toLowerCase();
return normalized || null;
}
/**
* Map one aircraft's internal poll record to a plain JSON-safe analyst
* record (analyst query engine seam). Pure — no Cesium types, no fetches;
* enrichment fields read the CACHED adsbdb values only. Missing/unknown
* fields are null, never NaN/undefined. The route-plausibility verdict is
* computed by the CALLER (it needs the billboard position) and passed in,
* so an implausible cached route is never surfaced as fact.
* @param {string} icao24 - ICAO 24-bit transponder address.
* @param {Object|null|undefined} info - `_flightData` record for this aircraft.
* @param {{military?: boolean, routeOk?: boolean}} [flags] - Shared-registry
* military flag + route-plausibility verdict.
* @returns {{id: string, icao24: string, callsign: string|null, lat: number|null,
* 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}}
*/
function mapAnalystRecord(icao24, info, { military = false, routeOk = false } = {}) {
const num = (v) => (Number.isFinite(v) ? v : null);
const text = (v) => { const t = String(v ?? '').trim(); return t || null; };
const callsign = text(info?.callsign);
return {
// Display identity for the narration layer. `id` is NOT a queryable field
// (see ANALYST_LAYERS) and follow-ups carry whole records, so this is a
// label, not a key — the engine keys on `icao24` below.
id: callsign || text(info?.registration) || icao24,
icao24,
callsign,
lat: num(info?.rawLat),
lon: num(info?.rawLon),
altitudeM: num(info?.altitude), // barometric/MSL — the aviation field, not the render height
speedMps: num(info?.velocity),
heading: num(info?.true_track),
verticalRateMps: num(info?.verticalRate),
onGround: info?.onGround === true,
military,
// A converted contact reports the class it RENDERS as, so an analyst
// filter/superlative agrees with the triangle on screen.
aircraftClass: tr3bAircraftClass(icao24, text(info?.klass)),
originCountry: text(info?.originCountry),
operator: text(info?.airline),
routeOrigin: routeOk ? text(info?.route?.origin?.code) : null,
routeDestination: routeOk ? text(info?.route?.destination?.code) : null,
};
}
const methods = { mapAnalystRecord,
id: 'flights',
name: 'Live Flights',
icon: '✈️',
source: flightState._lastSource,
// Browser-harness seam: isolates synthetic display-floor scenarios without
// changing any production lifecycle or cache policy.
_clearDisplayFloorStateForTest: parts.testing._clearDisplayFloorStateForTest,
/** @type {number} Polling interval (ms) between update() calls */
updateInterval: 30000,
/**
* Live layer params.
* `models3d` toggles 3D glTF model rendering for the FLEET (altitude-gated): when on,
* surrounding aircraft become 3D models once the camera is zoomed in past MODEL_ALT_CEIL_M.
* The TRACKED contact is NOT gated by this — it takes its 3D model by camera distance
* regardless (see `_trackedModelRegimeActive` / trackedModelRegime.js).
* `models3dMode` is 'proximity' (nearest MODEL_MAX in view) or 'all' (every in-view plane).
* @param {{models3d?: boolean, models3dMode?: 'proximity'|'all', selectedFlightsTrackingId?: string|null}} params
*/
setParams(params = {}, { origin = 'programmatic' } = {}) {
if (isExplicitLayerStateOrigin(origin)
&& !Object.hasOwn(params, 'selectedFlightsTrackingId')) {
parts.tracking._cancelPendingTrackingRestore();
}
if (typeof params.models3d === 'boolean' && params.models3d !== flightState._models3dEnabled) {
flightState._models3dEnabled = params.models3d;
if (!flightState._models3dEnabled) {
parts.rendering._releaseModels();
parts.tracking._syncTracked2dRotation();
// Restore fleet billboards (the horizon-cull pass re-asserts next tick), but NEVER the
// tracked plane's own fleet billboard — its tracked entity is the visual, so re-showing it
// here would double-image the tracked plane when 3D is turned off mid-track.
if (flightState._billboardCollection) for (const [icao, bb] of flightState._billboards) { if (icao !== flightState._trackedIcao) bb.show = true; }
}
}
if ((params.models3dMode === 'proximity' || params.models3dMode === 'all') && params.models3dMode !== flightState._models3dMode) {
// The next fleet tick re-derives the eligible set under the new cap and releases the overflow.
flightState._models3dMode = params.models3dMode;
if (flightState._cockpitContactMode) {
parts.tracking._refreshCockpitNearContacts();
flightState._lastFleetTickMs = 0;
}
}
if (typeof params.irBoost === 'boolean' && params.irBoost !== flightState._irBoost) {
flightState._irBoost = params.irBoost;
parts.rendering._reloadModelsForIrBoost();
// Sprites don't reload with the models — swap the TR-3B glyph between its
// cold and thermal-reactive variants directly. Bounded by the operator's
// own conversions, so this never touches the ordinary fleet.
parts.tracking._refreshTr3bForStyle();
}
if (Object.hasOwn(params, 'selectedFlightsTrackingId')) {
const requested = _normalizeTrackedIcao(params.selectedFlightsTrackingId);
if (requested === flightState._trackedIcao) {
flightState._pendingTrackingRestore = null;
} else if (requested === null) {
parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin });
} else {
const generation = ++flightState._trackingIntentGeneration;
flightState._pendingTrackingRestore = { id: requested, generation, origin };
if (flightState._trackedIcao) parts.tracking._clearTracking(false, { origin });
parts.tracking._applyPendingTrackingRestore();
}
}
return true;
},
getParams() {
return {
models3d: flightState._models3dEnabled,
models3dMode: flightState._models3dMode,
irBoost: flightState._irBoost,
selectedFlightsTrackingId: flightState._trackedIcao,
};
},
/**
* Re-render a contact whose TR-3B conversion just flipped (Easter egg).
* Callers own the registry write; this only re-derives what renders.
* @param {string} icao24 - ICAO 24-bit address.
* @returns {boolean} True when this layer owns the contact.
*/
refreshTr3b(icao24) { return parts.tracking._refreshTr3bContact(icao24); },
/**
* Return a subsample of currently visible aircraft for detection overlay
* rendering (e.g. bounding boxes drawn on-screen by the CCTV detection layer).
*
* Uses a deterministic stride + seed to select a spatially distributed
* subset without sorting or shuffling.
*
* @param {object} [options]
* @param {number} [options.maxCount] - Maximum number of objects to return.
* @param {number} [options.seed] - Deterministic offset into the stride pattern.
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
*/
getDetectableObjects(options = {}) {
if (!flightState._billboardCollection || !flightState._billboardCollection.show) return [];
// Compute a stride that evenly samples the billboard map.
// seed shifts the starting offset so successive calls can sample
// different aircraft without shuffling the underlying Map order.
const maxCount = Number.isFinite(options.maxCount)
? Math.max(1, Math.floor(options.maxCount))
: flightState._billboards.size;
const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0;
const stride = Math.max(1, Math.ceil(flightState._billboards.size / maxCount));
const start = seed % stride;
const result = [];
let idx = 0;
for (const [icao24, bb] of flightState._billboards) {
const shouldTake = ((idx - start) % stride) === 0;
idx++;
if (!shouldTake) continue;
if (flightState._cockpitContactMode && icao24.toLowerCase() === flightState._cockpitSubjectId) continue;
const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (billboard hidden) so the detection box
// doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden.
const model = flightState._models.get(icao24);
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24);
if (!isTracked && !bb.show && !modelOwnsVisual) continue;
const info = flightState._flightData.get(icao24);
let object = flightState._detectionObjects.get(icao24);
if (!object) {
object = { sourceId: icao24, type: 'AIR', _weldPos: new Cesium.Cartesian3() };
flightState._detectionObjects.set(icao24, object);
}
// WELD: anchor to whatever actually owns the visual. A model-owned contact is
// read straight off the translation the fleet tick already wrote into its
// modelMatrix, so bracket and label sit on the aircraft you can see instead of
// on the buried billboard position, which for a grounded plane is ~100 m below
// and rises only as the coarse ground-floor cell warms. Zero extra sampling and
// no `_modelDisplayPosition` call from postRender. Sprite-owned contacts keep
// `bb.position` — sprite and bracket are co-located there, so association holds.
const spec = modelOwnsVisual ? parts.rendering._modelSpec(info?.klass) : null;
const pos = isTracked
? (parts.motion._trackedVisualCached() || bb.position)
: (modelOwnsVisual
? modelVisualAnchor(
model.modelMatrix,
spec.visualCenterNative,
Number.isFinite(model.computedScale) ? model.computedScale : spec.scale,
object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
)
: bb.position);
if (!pos) continue;
object.position = pos;
object.skipLabel = isTracked;
// Card text only — declutter/cohort identity is `object.sourceId` (icao24).
const id = _contactLabel(icao24, info);
if (object.id !== id) object.id = id;
const altitude = info?.altitude;
if (object._altitude !== altitude) {
object._altitude = altitude;
object.metric = formatFlightLevel(altitude); // altitude is metres
}
result.push(object);
if (result.length >= maxCount) break;
}
if (flightState._detectionObjects.size > flightState._billboards.size + 512) {
for (const icao24 of flightState._detectionObjects.keys()) {
if (!flightState._billboards.has(icao24)) flightState._detectionObjects.delete(icao24);
}
}
return result;
},
/**
* Find a single aircraft by free-text query.
* Match priority: exact icao24 hex, exact callsign, callsign prefix,
* then callsign substring (all case-insensitive, trimmed).
* @param {string} query - ICAO24 hex or full/partial callsign.
* @returns {{icao24: string, callsign: string|null, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
* Best match with a cloned, dead-reckoned position, or null if none.
*/
findByQuery(query) {
if (!flightState._flightData || flightState._flightData.size === 0) return null;
const q = String(query || '').trim().toLowerCase();
if (!q) return null;
// Registration is searched alongside callsign because it is what the
// operator SEES: a callsign-less contact reads as its tail number on the
// card, in the analyst's answer, and in the Contacts list. Matching only
// callsigns meant "follow 6606" — and the analyst → track_entity handoff
// the tool instructions prescribe — answered "nothing matched" for the
// very identity the app had just shown. Ranking is shared with the
// military layer (contactMatch.js) so the two cannot disagree, and it is
// strictly tiered so a registration can never out-rank a real callsign on
// feed order alone.
let best = null;
for (const [icao24, info] of flightState._flightData) {
const candidate = {
tier: rankContactMatch({
query: q,
hex: icao24,
callsign: info?.callsign,
registration: info?.registration,
}),
id: icao24,
};
if (!contactMatchWins(candidate, best)) continue;
best = candidate;
if (candidate.tier === CONTACT_MATCH_TIER.HEX_EXACT) break;
}
return best ? _describeFlight(best.id) : null;
},
/**
* Find aircraft near a given ECEF position, sorted ascending by distance.
* Return shape mirrors militaryFlightsLayer.getNearby (id/icao24/position/distance).
* @param {Cesium.Cartesian3} center - Reference position in ECEF coordinates.
* @param {number} range - Maximum distance in meters (Infinity if not finite).
* @param {number} [maxCount=50] - Maximum number of results to return.
* @param {object} [options] Query membership options.
* @param {boolean} [options.includeHidden=false] Include loaded horizon-hidden aircraft.
* @returns {Array<{id: string, icao24: string, callsign: string|null, position: Cesium.Cartesian3, distance: number, aircraftClass: string|null, altitudeM: number|null, velocityMps: number|null, track: number|null}>}
*/
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
if (!center || !flightState._billboardCollection || !flightState._billboardCollection.show) return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 50;
const maxRange = Number.isFinite(range) && range > 0 ? range : Number.POSITIVE_INFINITY;
const now = Cesium.JulianDate.now();
const nearby = [];
for (const [icao24, bb] of flightState._billboards) {
const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (billboard hidden) so proximity counts
// don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden.
if (!aircraftIncludedInNearby({
isTracked,
billboardShown: bb.show,
modelRendering: parts.rendering._modelOwnsVisual(icao24),
includeHidden,
})) continue;
const trackedPos = isTracked ? parts.motion._trackedDisplayCached() : null; // cached, no recompute (anti-jitter)
const pos = trackedPos || bb.position;
if (!pos) continue;
const distance = Cesium.Cartesian3.distance(center, pos);
if (distance > maxRange) continue;
const info = flightState._flightData.get(icao24);
const callsign = info?.callsign?.trim() || null;
nearby.push({
// Label. Callers that need identity read `icao24` (Context cohorts do).
id: _contactLabel(icao24, info),
icao24,
callsign,
position: pos,
distance,
// Filter surface: the cockpit next/previous path matches on THIS field
// (militaryAwareness.aircraftClassMatchesFilter), so a converted contact
// has to report the class it renders as or a `tr3b` filter skips it.
aircraftClass: tr3bAircraftClass(icao24, String(info?.klass || '').trim().toLowerCase() || null),
altitudeM: info?.altitude ?? null,
velocityMps: info?.velocity ?? null,
track: info?.true_track ?? null,
});
}
nearby.sort((a, b) => a.distance - b.distance);
return nearby.slice(0, limit);
},
/**
* Return id/label/position for up to maxCount currently rendered aircraft.
* Cheap snapshot for voice-tool framing — billboard positions only, no
* dead reckoning and no cloning.
* @param {number} [maxCount=500] - Maximum number of entries to return.
* @returns {Array<{id: string, label: string, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number}>}
*/
/**
* Whether this layer still carries a contact, in O(1).
*
* Presence consumers must not infer absence from `getAllPositions`: it stops
* at its cap, and this layer routinely carries ~11k contacts against a
* 1,000-row cap, so "not in the returned rows" is not "gone". Id matching
* mirrors trackById: exact key first, then lowercase.
* A disabled layer keeps its records but hides the collection, so it must
* decline rather than answer from data the user can no longer see —
* otherwise a preserved subject reads as fresh off stale hidden state.
* @param {string} icao24 Contact identifier.
* @returns {boolean|null} Presence, or null when the layer is disabled or
* holds no data and therefore cannot answer.
*/
hasContact(icao24) {
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._billboards.size === 0) return null;
if (!icao24) return false;
const id = String(icao24).trim();
return flightState._billboards.has(id) || flightState._billboards.has(id.toLowerCase());
},
getAllPositions(maxCount = 500) {
if (!flightState._billboardCollection || flightState._billboards.size === 0) return [];
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 500;
const result = [];
for (const [icao24, bb] of flightState._billboards) {
const pos = bb.position;
if (!pos) continue;
const carto = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84, flightState._scratchCarto);
if (!carto) continue;
const info = flightState._flightData.get(icao24);
result.push({
id: icao24, // identity (trackById resolves this)
label: _contactLabel(icao24, info),
position: pos,
latitude: Cesium.Math.toDegrees(carto.latitude),
longitude: Cesium.Math.toDegrees(carto.longitude),
altitudeM: carto.height,
airline: info?.airline ?? null,
typeName: info?.typeName ?? null,
typeCode: info?.typeCode ?? null,
registration: info?.registration ?? null,
origin: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.origin.code : null,
destination: info?.route && parts.tracking._routeIsPlausible(icao24, info.route) ? info.route.destination.code : null,
});
if (result.length >= limit) break;
}
return result;
},
/**
* Snapshot the layer's in-memory records as plain JSON-safe objects for
* the analyst query engine. On-demand only (called at most once per
* spoken query) — zero per-frame cost, no listeners, no caching, no
* enrichment fetches (cached adsbdb values only). Returns [] while the
* layer is disabled or empty.
* @param {number} [maxCount=2000] - Maximum records to return (truncation).
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
*/
getAnalystRecords(maxCount = 2000) {
if (!flightState._billboardCollection || !flightState._billboardCollection.show || flightState._flightData.size === 0) return [];
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 2000;
const result = [];
for (const [icao24, info] of flightState._flightData) {
const routeOk = !!info?.route && parts.tracking._routeIsPlausible(icao24, info.route);
result.push(mapAnalystRecord(icao24, info, { military: isMilitaryIcao(icao24), routeOk }));
if (result.length >= limit) break;
}
return result;
},
/**
* Start camera-tracking an aircraft by ICAO24 address.
* @param {string} icao24 - ICAO 24-bit transponder address.
* @returns {boolean} True if the aircraft exists and tracking started.
*/
trackById(icao24, { origin = 'programmatic' } = {}) {
if (!icao24) return false;
let id = String(icao24).trim();
if (!flightState._billboards.has(id)) id = id.toLowerCase();
if (!flightState._billboards.has(id)) return false;
if (parts.tracking._isExplicitTrackingOrigin(origin)) parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao === id) return parts.tracking._publishTrackedSelection(id, origin);
parts.tracking._trackFlight(id, { origin });
return true;
},
/** Resolve a shared Follow target only against the latest accepted refresh. */
async resolveTrackingRestoreTarget(icao24, { signal = null, origin = 'share-restore' } = {}) {
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') };
const id = _normalizeTrackedIcao(icao24);
if (!id) return { status: 'missing', reason: 'invalid-target' };
const outcome = flightState._lastTrackingRefreshOutcome;
if (outcome.status !== 'accepted') {
return {
status: 'source-unavailable',
reason: 'OpenSky snapshot unavailable',
refreshEpoch: outcome.epoch,
source: outcome.source,
coverage: outcome.coverage,
};
}
if (!outcome.ids.has(id)) {
return {
status: 'missing',
reason: 'target-absent-from-snapshot',
refreshEpoch: outcome.epoch,
source: outcome.source,
coverage: outcome.coverage,
};
}
if (signal?.aborted) return { status: 'cancelled', reason: String(signal.reason || 'aborted') };
const followed = this.trackById(id, { origin });
return followed
? {
status: 'found',
refreshEpoch: outcome.epoch,
source: outcome.source,
coverage: outcome.coverage,
}
: { status: 'source-unavailable', reason: 'target-not-renderable', refreshEpoch: outcome.epoch };
},
/** Reapply the canonical follow frame without recreating the selected flight. */
refocusTrackedById(icao24, { origin = 'programmatic' } = {}) {
if (!icao24 || flightState._cockpitContactMode || !flightState._viewer || !flightState._trackedEntity) return false;
let id = String(icao24).trim();
if (!flightState._billboards.has(id)) id = id.toLowerCase();
if (
id !== flightState._trackedIcao
|| !flightState._viewer.entities?.contains?.(flightState._trackedEntity)
|| flightState._viewer.trackedEntity !== flightState._trackedEntity
) return false;
flightState._trackedCameraFrameStop?.();
flightState._viewer.camera.cancelFlight();
flightState._viewer.trackedEntity = flightState._trackedEntity;
flightState._trackedCameraFrameStop = applyTrackedCameraFrame(
flightState._viewer,
flightState._trackedEntity,
flightState._trackedEntity.viewFrom,
) || null;
parts.tracking._publishTrackedSelection(id, origin);
return true;
},
/**
* Stop tracking the currently followed aircraft (no-op if none).
* @returns {boolean} Always true.
*/
stopTracking({ origin = 'programmatic' } = {}) {
parts.tracking._cancelPendingTrackingRestore();
parts.tracking._clearTracking(false, { origin });
return true;
},
cancelPendingTrackingRestore() {
parts.tracking._cancelPendingTrackingRestore();
},
/**
* Describe the currently tracked aircraft at its dead-reckoned position.
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
* Tracked aircraft info, or null when nothing is tracked.
*/
getTrackedInfo() {
if (!flightState._trackedIcao) return null;
const described = _describeFlight(flightState._trackedIcao);
if (!described) return null;
const { position, ...rest } = described;
return rest;
},
/**
* Return the current aircraft as a Context subject without changing
* tracking or camera ownership.
* @returns {{layerId: string, id: string, label: string, position: Cesium.Cartesian3}|null}
* Detached subject descriptor, or null when no aircraft is tracked.
*/
getTrackedSubject() {
if (!flightState._trackedIcao) return null;
const described = _describeFlight(flightState._trackedIcao);
if (!described?.position) return null;
return {
layerId: 'flights',
id: described.icao24,
// Same label chain as getNearby/getDetectableObjects: a callsign-less
// contact reads as its registration, never as the raw ICAO hex.
label: described.callsign
|| _toCleanText(described.registration)
|| described.icao24,
position: Cesium.Cartesian3.clone(described.position),
};
},
...(FOCUS_EVIDENCE_DEV ? {
__focusEvidence: Object.freeze({
setAircraft: parts.evidence._setFocusEvidenceAircraft,
moveAircraft: parts.evidence._moveFocusEvidenceAircraft,
snapshot: parts.evidence._focusEvidenceSnapshot,
setTuning({ focus = {}, horizon = {} } = {}) {
return {
focus: setFocusDeemphasisParams(focus),
horizon: setAircraftRecessionParams(horizon),
};
},
getTuning() {
return {
focus: { ...getFocusDeemphasisParams() },
horizon: { ...getAircraftRecessionParams() },
};
},
takeFrameClock(startMs = 1_000_000_000) {
if (!flightState._viewer || !Number.isFinite(startMs)) return { ok: false, nowMs: null };
flightState._viewer.useDefaultRenderLoop = false;
setFocusEvidenceNowMs(startMs);
flightState._lastFleetTickMs = startMs - FLEET_DR_INTERVAL_MS;
// Cross a browser task boundary, then close Cesium's pending-loop
// latch. Any already-queued callback still observes the false gate,
// while manual evidence renders can begin without waiting on VSYNC.
return new Promise((resolve) => setTimeout(() => {
if (flightState._viewer?._cesiumWidget) flightState._viewer._cesiumWidget._renderLoopRunning = false;
resolve({ ok: true, nowMs: startMs });
}, 0));
},
advanceFrameClock(deltaMs = FLEET_DR_INTERVAL_MS) {
return advanceFocusEvidenceNowMs(deltaMs);
},
releaseFrameClock() {
setFocusEvidenceNowMs(null);
if (flightState._viewer) flightState._viewer.useDefaultRenderLoop = true;
},
}),
} : {}),
/**
* Return layer health/status for the HUD stats chip.
* @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}}
*/
getStats() {
const retryInSec = flightState._retryAt ? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000)) : 0;
return {
count: flightState._count,
lastUpdate: flightState._lastUpdate,
stale: flightState._backoff,
error: flightState._lastError,
status: flightState._lastStatus,
retryInSec,
source: flightState._lastSource,
coverage: flightState._lastCoverage,
};
}
};
return { _approxDistanceKm, _likelyLanded, _toCleanText, _contactLabel, _describeFlight, _normalizeTrackedIcao, mapAnalystRecord, methods };
}
+874
View File
@@ -0,0 +1,874 @@
import * as Cesium from 'cesium';
import { CLASS_SCALE_2D, CLASS_MODEL_REAL, CLASS_SCALE_3D, CLASS_MODEL_URL } from '../../data/aircraftClass.js';
import { visualCenterForModel, trailAnchorForModel } from '../../data/modelVisualAnchor.js';
import { cockpitContactDotImage } from '../../data/cockpitContactDot.js';
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
import { cameraPoseSignature, horizonOccluder, screenProjectedRotation } from '../../data/iconOrientation.js';
import { limitCourseStep, courseSlewCapDps } from '../../data/motionModel.js';
import { MIL_TINT, GROUND_SCALE, MODEL_COLOR_BLEND_AMOUNT, MODEL_SCALE, PLANE_MODEL_URL, MODEL_NATIVE_RADIUS_M, MODEL_BELLY_OFFSET_NATIVE, COCKPIT_CONTACT_SIZE_PX, COCKPIT_CIVILIAN_COLOR, MODEL_ALT_CEIL_M, MODEL_MAX_ALL, MODEL_MAX, COCKPIT_MODEL_MAX, MODEL_ALL_ADD_M, MODEL_PROX_ADD_M, MODEL_ALL_KEEP_M, MODEL_PROX_KEEP_M, MODEL_HEADING_OFFSET_DEG, IR_RELOAD_BATCH, MODEL_MIN_PX, TRACKED_MODEL_MIN_PX, TRACKED_MODEL_MAX_PX, FLEET_DR_INTERVAL_MS, COURSE_SLEW_DT_MAX_SEC, ROTATION_REFRESH_MS, COURSE_MAX_DPS } from './policy.js';
export function createRendering({ flightState, services, parts, layer, resolveAsset }) {
const { isMilitaryIcao } = services.militaryRegistry;
const { tr3bIconKind, isTr3b } = services.aircraftPresentation;
const { trackedModelScaleForPixelCap } = services.camera;
const { focusNowMs, getFocusTarget, nearFarScalarValueAtDistance, advanceProjectedSpriteFocus } = services.focus;
const { applyAircraftBillboardTreatment, applyAircraftModelTreatment } = services.recession;
/** Fleet (untracked) billboard tint: amber for known-military, white otherwise.
* Ground traffic gets NO special tint (owner verdict 2026-07-03 field test). */
function _fleetBillboardColor(icao24) {
return isMilitaryIcao(icao24) ? MIL_TINT : Cesium.Color.WHITE;
}
/** Fleet billboard scale: per-class scale, ×GROUND_SCALE while grounded. */
function _fleetBillboardScale(icao24, klass) {
return (CLASS_SCALE_2D[klass] || 1) * (flightState._flightData.get(icao24)?.onGround ? GROUND_SCALE : 1);
}
/** Depth-test policy for aircraft billboards. Round 5 (owner directive
* 2026-07-06: "I just want the planes and their lines to ALWAYS be
* visible... evenly applied"): EVERY contact renders depth-test-free at
* every distance — grounded, low, and airborne alike. The photoreal mesh
* writes depth and residual baro/floor error will always leave some sprite
* geometry at or below it; a uniform rule beats the grounded-only /
* low-AGL-only conditions that kept leaving classes of contacts buried
* (2026-07-03 Van Nuys grounded case; 2026-07-06 Austin QNH-below-field
* case). Far-side planes are still removed by the fleet tick's horizon
* occluder, which never depended on depth. Kept as a function so the
* callers' restyle sites stay diff-stable. */
function _groundDepthDistance() {
return Number.POSITIVE_INFINITY;
}
function _modelSpec(klass) {
const cached = flightState._specCache.get(klass);
if (cached) return cached;
const real = CLASS_MODEL_REAL[klass];
let spec;
if (real) {
spec = {
url: real.url,
scale: 1,
nativeRadiusM: real.radiusM,
bellyM: real.bellyM,
blendAmount: MODEL_COLOR_BLEND_AMOUNT,
visualCenterNative: visualCenterForModel(real.url),
trailAnchorNative: trailAnchorForModel(real.url),
};
} else {
const scale = MODEL_SCALE * (CLASS_SCALE_3D[klass] || 1);
const url = CLASS_MODEL_URL[klass] || PLANE_MODEL_URL;
spec = {
url,
scale,
nativeRadiusM: MODEL_NATIVE_RADIUS_M,
bellyM: MODEL_BELLY_OFFSET_NATIVE * scale,
blendAmount: MODEL_COLOR_BLEND_AMOUNT,
visualCenterNative: visualCenterForModel(url),
trailAnchorNative: trailAnchorForModel(url),
};
}
flightState._specCache.set(klass, spec);
return spec;
}
function _normalBillboardScaleByDistance() {
// Preserve the established close-range 3× scale. Any smaller owner-visible
// default belongs in a separate evidence-backed proposal.
return new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5);
}
function _cockpitBillboardScaleByDistance() {
return new Cesium.NearFarScalar(1000, 1.15, 8000000, 0.65);
}
/** Sprite kind for one contact's billboard. Identity for every aircraft except
* the ones the operator converted into a TR-3B (Easter egg), which draw the
* black-triangle glyph — its thermal-reactive variant while an IR style owns
* the scene. Routing EVERY `aircraftIcon()` call through this is what makes a
* conversion survive the poll reconciler and the two-tier raster swap. */
const _iconKind = (icao24, klass) => tr3bIconKind(icao24, klass, { hot: flightState._irBoost });
/** Apply the current normal/cockpit visual contract to one owned fleet billboard. */
function _applyFleetBillboardPresentation(icao24, bb) {
if (!bb) return;
const limbScale = flightState._billboardLimbScale.get(bb) ?? 1;
const isCockpitContact = flightState._cockpitContactMode && icao24 !== flightState._trackedIcao;
const isCockpitNear = isCockpitContact && flightState._cockpitNearContacts.has(icao24);
if (isCockpitContact && !isCockpitNear) {
const freshnessAlpha = bb.color?.alpha ?? 1;
bb.image = cockpitContactDotImage();
bb.width = COCKPIT_CONTACT_SIZE_PX;
bb.height = COCKPIT_CONTACT_SIZE_PX;
bb.scale = limbScale;
bb.scaleByDistance = _cockpitBillboardScaleByDistance();
bb.color = (isMilitaryIcao(icao24) ? MIL_TINT : COCKPIT_CIVILIAN_COLOR).withAlpha(freshnessAlpha);
bb.rotation = 0;
return;
}
const meta = flightState._flightData.get(icao24);
bb.image = aircraftIcon(_iconKind(icao24, meta?.klass), bb._gevIconLarge ? TRACKED_ICON_PX : undefined);
bb.width = icao24 === flightState._trackedIcao ? 24 : 20;
bb.height = icao24 === flightState._trackedIcao ? 24 : 20;
bb.scale = _fleetBillboardScale(icao24, meta?.klass) * limbScale;
bb.scaleByDistance = _normalBillboardScaleByDistance();
bb.color = _fleetBillboardColor(icao24).withAlpha(bb.color?.alpha ?? 1);
}
/**
* Per-preRender fleet pass at ~12Hz: dead-reckons every untracked billboard,
* horizon-culls billboards beyond the limb (no far-side depth with the globe
* hidden), and refreshes screen-projected icon rotations whenever the camera
* pose changed (plus a 1s drift catch-up while idle). Driven by
* scene.preRender — NOT camera.changed, whose granularity is globally
* degraded by other layers mutating camera.percentageChanged.
* @returns {void}
*/
/** Model tint, mirroring the billboard color rules. */
function _modelColor(icao24) {
if (icao24 === flightState._trackedIcao) return Cesium.Color.CYAN;
return isMilitaryIcao(icao24) ? MIL_TINT : Cesium.Color.WHITE;
}
/** The FLEET's 3D-model regime: models3d enabled AND the camera zoomed in past the altitude
* ceiling. Since 2026-08-22 the toggle DEFAULTS ON in `proximity`, which is itself the
* budget: models only appear below MODEL_ALT_CEIL_M and only for the nearest MODEL_MAX in
* view. The toggle still OWNS the fleet — an operator who wants every in-view plane arms
* `all`, and one who wants none turns 3D off — this predicate is unchanged. The TRACKED
* contact does not route through here: it is one model, it is what the camera is aimed at,
* and it takes its own default-on, hysteretic zoom regime
* (`_trackedModelRegimeActive`). */
function _modelRegimeActive() {
if (!flightState._models3dEnabled) return false;
const h = flightState._viewer?.camera?.positionCartographic?.height ?? Infinity;
return h < MODEL_ALT_CEIL_M;
}
/** Active model cap — the eligibility pre-pass AND _ensureModel's admission checks must use the
* SAME value, else 'all' (MODEL_MAX_ALL) would mark planes eligible that _ensureModel then refuses
* at the lower MODEL_MAX, silently degrading 'all' to 'proximity'. */
function _modelCap() {
const mapCap = flightState._models3dMode === 'all' ? MODEL_MAX_ALL : MODEL_MAX;
// `Math.min` on purpose: cockpit may only ever LOWER the GLB budget. Cockpit is
// already the heaviest mode (20 Hz camera setView ahead of scene update, photoreal
// retraversal, the cloud pass) and every model is its own draw call.
return flightState._cockpitContactMode ? Math.min(COCKPIT_MODEL_MAX, mapCap) : mapCap;
}
/** Active ADD radius (m) — new planes inside this range get a model. Mode-aware: 'all' reaches far. */
function _modelAddDistM() {
return flightState._models3dMode === 'all' ? MODEL_ALL_ADD_M : MODEL_PROX_ADD_M;
}
/** Active KEEP radius (m) — a modeled plane keeps its model out to here (hysteresis vs ADD). */
function _modelKeepDistM() {
return flightState._models3dMode === 'all' ? MODEL_ALL_KEEP_M : MODEL_PROX_KEEP_M;
}
/** World model matrix from a position + course heading (pitch/roll 0; ENU frame). Writes into
* `result` and returns it — pass each model's OWN `.modelMatrix` so models never share one
* mutable matrix object. `Model.modelMatrix` is a plain field (not a cloning setter); Cesium
* clones it per frame in updateModelMatrix(). Sharing a single scratch made every model render at
* the LAST-written transform — all stacked on one plane — and, once the tracked model wrote the
* scratch every frame, the stack point oscillated frame-to-frame: the "flickering like mad" bug. */
function _modelMatrix(pos, headingDeg, result = flightState._scratchModelMtx) {
flightState._scratchModelHpr.heading = Cesium.Math.toRadians((headingDeg || 0) + MODEL_HEADING_OFFSET_DEG);
flightState._scratchModelHpr.pitch = 0;
flightState._scratchModelHpr.roll = 0;
return Cesium.Transforms.headingPitchRollToFixedFrame(
pos, flightState._scratchModelHpr, Cesium.Ellipsoid.WGS84, undefined, result,
);
}
/** Everything scene.sampleHeight must NOT hit when snapping a grounded model: the
* vertical pick ray at a plane's own lat/lon otherwise lands on its (or a parked
* neighbor's) billboard/model instead of the tile skin. Cesium's ray-pick exclusion
* matches picked-object IDs, and every billboard AND model in this layer carries its
* icao as `id`, so the icao strings cover both; the tracked entity is excluded as the
* object itself. Built lazily — only when a sample actually fires (one-shot). */
function _groundSampleExclusions() {
const out = [...flightState._billboards.keys()];
if (flightState._trackedEntity) out.push(flightState._trackedEntity);
return out;
}
/** Position a 3D MODEL renders at. Airborne planes use their dead-reckoned position
* verbatim. GROUNDED planes' meta altitude is last-known baro or 0 m — nowhere near
* the photoreal tile skin in ellipsoid heights (buried ~100+ m at inland airports,
* hovering ~30 m at sea-level ones), and unlike the ground billboards a depth-tested
* model can't hide behind disableDepthTestDistance. So a modeled grounded plane rides
* a ONE-SHOT cached scene.sampleHeight of the skin at its lat/lon (groundSnap.js,
* CCTV-B9b discipline: never per-frame; taxiing >50 m retires the cached value to a
* bounded last-known and resamples), plus
* the belly offset so it sits on its gear rather than sinking to the model-origin
* fuselage centerline. Until the FIRST sample lands (tiles streaming / sample miss)
* there is no safe depth-tested placement at all: this returns null, the caller
* keeps the 2D billboard visible and the model hidden, and it retries later. Once
* a contact has resolved once, a later outage holds that measurement inside
* groundSnap's drift bound instead — a taxiing aircraft does not pop back to 2D
* because a resample is mid-backoff. */
function _modelDisplayPosition(icao24, pos, result) {
const meta = flightState._flightData.get(icao24);
if (!meta || !meta.onGround) return pos;
const h = flightState._groundSnap.heightFor(flightState._viewer, icao24, pos, _groundSampleExclusions);
if (h == null) return null;
const carto = Cesium.Cartographic.fromCartesian(pos, Cesium.Ellipsoid.WGS84, flightState._scratchGroundCarto);
carto.height = h + _modelSpec(meta.klass).bellyM;
return Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, carto.height, Cesium.Ellipsoid.WGS84, result);
}
/** Atomically hand one fleet contact from its billboard to a safely placed,
* ready 3D model. Missing/loading models and unresolved terrain always leave
* the billboard owning the visual, so no render frame can hide both.
* `beforeShow` runs only on the committing path — the per-tick model treatment
* belongs to a model that is about to draw, not to one still waiting. */
function _driveFleetModelHandoff(icao24, model, bb, pos, course, beforeShow) {
if (!model) {
bb.show = true;
return false;
}
const displayPos = _modelDisplayPosition(icao24, pos, flightState._scratchGroundPos);
if (!displayPos) {
model.show = false; // no ground evidence → nothing safe to place a depth-tested model at
bb.show = true;
return false;
}
// The matrix is written BEFORE the readiness test on purpose: a model can flip
// `ready` during scene update after this tick, and a first rendered frame on a
// stale load-start matrix is the one-frame jump this ordering prevents.
_modelMatrix(displayPos, course, model.modelMatrix);
if (!model.ready) {
model.show = false; // not loaded yet → keep the 2D icon, no half-model flash
bb.show = true;
return false;
}
beforeShow?.();
if (!model.show) model.show = true;
if (bb.show) bb.show = false; // hand off ONLY once the model renders
return true;
}
/**
* Whether a 3D model — not the billboard — is what the user actually SEES for
* this contact, and therefore whether the display floor should stand aside.
*
* Model EXISTENCE is not ownership. A fleet model that is still loading, or has
* no resolved ground to stand on, stays hidden while `bb.show` stays true (the
* gap-proof handoff: "hand off ONLY once the model renders"), and a tracked model
* is retained but hidden whenever the model regime is off — 3D disabled, camera
* zoomed past the ceiling, or cockpit mode. Gating on `has()`/existence therefore
* suppressed the clamp in exactly the states where the BILLBOARD is the visual,
* putting the burial straight back.
*
* Cesium's own default `show === true` is the reason admission clears it
* explicitly: a fleet model registered in `_models` the instant its glTF resolves
* would otherwise claim ownership — at the identity matrix — for the frames
* between admission and the next fleet tick, while the BILLBOARD was still the
* visual. Ownership means actually rendering, and every site that sets `show`
* true does so only after committing a matrix.
*
* The two halves:
* - Fleet: the rendering test alone. Safe for the ground snap either way,
* because a fleet model is positioned from the RAW dead-reckon, not from
* the billboard.
* - Tracked: the rendering test AND `_trackedModelRegimeActive()`. The tracked
* model is fed from `_trackedDisplayPosition`, so once it is live the clamp
* must stand aside: two different chains would otherwise be deciding one
* contact's ground, and the billboard's is the one the operator is not
* looking at (T7). The regime check is what makes a regime
* flip take effect on the same frame rather than waiting for
* `_updateTrackedModel` to clear `show`; the rendering test is what keeps a
* null or still-loading tracked model from claiming a visual it is not
* drawing yet.
* @param {string} icao24
* @returns {boolean}
*/
function _modelOwnsVisual(icao24) {
if (icao24 === flightState._trackedIcao) {
return parts.tracking._trackedModelRegimeActive() && _modelIsRendering(flightState._trackedModel);
}
return _modelIsRendering(flightState._models.get(icao24));
}
/** A model is the visual only once it actually draws — loaded AND shown.
*
* `show` is sufficient evidence of a safe placement because only ONE site ever
* sets it true for a fleet model (`_driveFleetModelHandoff`, after the matrix is
* committed) and one for the tracked model (`_updateTrackedModel`, likewise);
* everything else — admission, an unresolved ground, a not-yet-ready glTF, the
* limb cull, a regime exit — only ever clears it. */
function _modelIsRendering(model) {
return !!model && model.ready === true && model.show === true;
}
/** Spec identity for a LOADED model: URL and scale together (same-URL classes
* differ by scale — airliner vs quadjet both ship airplane.glb). */
const _specKeyFor = (klass) => {
const spec = _modelSpec(klass);
return `${spec.url}@${spec.scale}`;
};
/** Class-change model sync (enrichment AND poll-path klass updates): when the
* aircraft's live model or in-flight load no longer matches its class's spec,
* drop it so the eligibility pass reloads the right asset at the right scale.
* Gap-proof: the fleet billboard is re-shown BEFORE the release so the
* contact never goes invisible for the tick gap; _releaseModel's generation
* bump also invalidates any pending load. The tracked standalone model gets
* the same rule (its billboard entity is always the fallback visual). */
function _syncModelToClass(icao24) {
const key = _specKeyFor(flightState._flightData.get(icao24)?.klass);
const current = flightState._models.get(icao24);
if ((current && current._gevSpecKey !== key) || (!current && flightState._modelPending.has(icao24))) {
const bb = flightState._billboards.get(icao24);
if (bb && icao24 !== flightState._trackedIcao) bb.show = true;
_releaseModel(icao24);
}
if (icao24 === flightState._trackedIcao && flightState._trackedModel && flightState._trackedModel._gevSpecKey !== key) {
_releaseTrackedModel();
}
}
function _reloadModelsForIrBoost() {
flightState._irReloadQueue = [...flightState._models.keys()];
for (const icao of flightState._modelPending) {
if (!flightState._models.has(icao)) flightState._modelGen.set(icao, (flightState._modelGen.get(icao) || 0) + 1);
}
_releaseTrackedModel();
}
function _drainIrReloadQueue() {
if (!flightState._irReloadQueue) return;
const batch = flightState._irReloadQueue.splice(0, IR_RELOAD_BATCH);
for (const icao of batch) {
const model = flightState._models.get(icao);
if (!model || model._gevIrBoost === flightState._irBoost) continue; // already right state
const bb = flightState._billboards.get(icao);
if (bb && icao !== flightState._trackedIcao) bb.show = true;
_releaseModel(icao);
}
if (flightState._irReloadQueue.length === 0) flightState._irReloadQueue = null;
}
/** Lazily create the glTF model for an aircraft (fire-and-forget; billboard shows until ready). */
async function _ensureModel(icao24) {
// Never model the TRACKED aircraft — it owns a separate entity billboard, and the fleet
// tick skips it, so a model here would be orphaned + double-rendered.
if (icao24 === flightState._trackedIcao) return;
parts.enrichment._requestTypeEnrichment(icao24, true); // model-eligible: about to render in 3D — jump the ambient backlog
if (flightState._models.has(icao24) || flightState._modelPending.has(icao24)) return;
// Count PENDING loads in the cap so a zoomed-in tick can't fire 100s of concurrent loads
// (the cap is rechecked post-await too, before the add). Mode-aware so 'all' can reach MAX_ALL.
if ((flightState._models.size + flightState._modelPending.size) >= _modelCap()) return;
const epoch = flightState._modelEpoch; // lifecycle token: if destroy() bumps it, this load is dead
const gen = flightState._modelGen.get(icao24) || 0; // capture; if it changes during the load, we're stale
flightState._modelPending.add(icao24);
let model = null;
// Spec identity captured at load START — if enrichment reclassifies the
// aircraft mid-load, the post-await admission below rejects the stale asset.
// Boost state likewise: the creation options bake it in, so a mid-load
// toggle must reject too (the reload queue only covers ADMITTED models).
const specKey = _specKeyFor(flightState._flightData.get(icao24)?.klass);
const loadIrBoost = flightState._irBoost;
try {
const spec = _modelSpec(flightState._flightData.get(icao24)?.klass);
model = await Cesium.Model.fromGltfAsync({
url: resolveAsset(spec.url),
asynchronous: false,
minimumPixelSize: MODEL_MIN_PX,
scale: spec.scale,
color: flightState._irBoost ? Cesium.Color.WHITE : _modelColor(icao24),
colorBlendMode: Cesium.ColorBlendMode.MIX,
// Launch presentation keeps the code-side tint dominant for every approved
// model; IR boost removes the remaining diffuse hint with flat UNLIT white.
colorBlendAmount: flightState._irBoost ? 1.0 : spec.blendAmount,
customShader: flightState._irBoost ? flightState._IR_UNLIT_SHADER : undefined,
id: icao24, // so scene.pick returns the icao for click-to-track
});
} catch {
// asset/decode fail — stay billboard. Only touch this lifecycle's state if still current
// (a destroy/re-init may have swapped the globals while this load was in flight).
if (epoch === flightState._modelEpoch) { flightState._modelPending.delete(icao24); _cleanupModelGen(icao24); }
return;
}
// A load from a PREVIOUS lifecycle (destroy→init happened mid-load) must NOT mutate the new
// epoch's _modelPending/_modelGen or add to the new collection — just drop its model.
if (epoch !== flightState._modelEpoch) { try { model.destroy(); } catch { /* gone */ } return; }
flightState._modelPending.delete(icao24);
// Post-await admission: reject (and DESTROY the loaded model) if anything changed during the
// load — a release bumped the generation (track/untrack/remove), the layer toggled off / was
// torn down, the aircraft is gone or now tracked, a model already exists, or the cap filled.
// Recheck the shared Display 3D toggle and altitude ceiling after the async
// load. Cockpit uses the same OFF / Proximity / All contract as map Display.
const stale = (flightState._modelGen.get(icao24) || 0) !== gen
|| !_modelRegimeActive() || !flightState._modelCollection || flightState._modelCollection.isDestroyed()
|| !flightState._flightData.has(icao24) || icao24 === flightState._trackedIcao
|| flightState._models.has(icao24) || flightState._models.size >= _modelCap()
// Class reclassified mid-load → this GLB/scale is for the OLD class.
|| _specKeyFor(flightState._flightData.get(icao24)?.klass) !== specKey
// IR boost flipped mid-load → this model baked the wrong shader/tint.
|| flightState._irBoost !== loadIrBoost;
if (stale) {
try { model.destroy(); } catch { /* already gone */ }
_cleanupModelGen(icao24); // bound the map
return;
}
// Keep the pick identity explicit on the resolved primitive. This also
// protects injected/custom loaders that do not copy the creation option.
model.id = icao24;
model._gevSpecKey = specKey; // class-change sync compares against this
model._gevIrBoost = loadIrBoost; // boost-flip reload queue compares against this
// Admitted, not yet the visual. Cesium's default is show=true, which would let
// an unplaced primitive claim ownership from the billboard for the frames
// between admission and the next fleet tick (and draw at the identity matrix,
// i.e. the Earth's centre). The handoff turns it on once it has a matrix.
model.show = false;
flightState._modelCollection.add(model);
flightState._models.set(icao24, model);
flightState._planeModelLoaded = true; // GLB is cached now — the tracked entity's model can fade in its billboard
}
/** Remove the 3D model for ONE aircraft (removal / military-suppression / track handoff).
* Bumps the load generation so any in-flight load for this icao is rejected on completion. */
function _releaseModel(icao24) {
const m = flightState._models.get(icao24);
const pending = flightState._modelPending.has(icao24);
// Only bump the generation when there's something to invalidate (an in-flight load or a
// live model) — so removing never-modeled aircraft doesn't grow _modelGen.
if (m || pending) {
flightState._modelGen.set(icao24, (flightState._modelGen.get(icao24) || 0) + 1);
}
if (m) {
if (flightState._modelCollection && !flightState._modelCollection.isDestroyed()) { try { flightState._modelCollection.remove(m); } catch { /* gone */ } }
flightState._models.delete(icao24);
}
// Do NOT clear _modelPending here — the in-flight load's OWN post-await removes it. Keeping
// it (a) prevents a duplicate load from starting and (b) keeps the bumped gen entry alive so
// the resolving load's gen-check still rejects it.
_cleanupModelGen(icao24);
}
/** Drop an icao's generation entry once nothing references it (no live model, no in-flight
* load) — keeps _modelGen bounded. Shared by _releaseModel + both _ensureModel exit paths. */
function _cleanupModelGen(icao24) {
if (!flightState._modelPending.has(icao24) && !flightState._models.has(icao24)) flightState._modelGen.delete(icao24);
}
/** Remove all live models (toggle-off / zoom-out); billboards take back over next tick. */
function _releaseModels() {
flightState._irReloadQueue = null; // a full release supersedes any pending boost-flip drain
// Invalidate in-flight loads so a completion after this bulk release can't add a model.
for (const icao of flightState._modelPending) flightState._modelGen.set(icao, (flightState._modelGen.get(icao) || 0) + 1);
if (flightState._modelCollection && !flightState._modelCollection.isDestroyed()) {
for (const m of flightState._models.values()) { try { flightState._modelCollection.remove(m); } catch { /* gone */ } }
}
flightState._models.clear();
}
/** Destroy the standalone tracked-aircraft model and invalidate any in-flight load. */
function _releaseTrackedModel() {
flightState._trackedModelGen++;
flightState._trackedModelLoading = false;
if (flightState._trackedModel) {
if (flightState._modelCollection && !flightState._modelCollection.isDestroyed()) { try { flightState._modelCollection.remove(flightState._trackedModel); } catch { /* gone */ } }
flightState._trackedModel = null;
}
}
/** Per-frame driver for the standalone tracked model. Runs every preUpdate (not the 80 ms fleet
* cadence) so the centered plane moves smoothly. The tracked entity stays a pure billboard, so the
* follow-camera's bounding sphere is ALWAYS ready — toggling 3D, or tracking while already zoomed
* in, can never stall or freeze the centering (the old model-graphic-on-entity failure mode). */
function _updateTrackedModel() {
const active = flightState._trackedIcao && parts.tracking._trackedModelRegimeActive()
&& flightState._modelCollection && !flightState._modelCollection.isDestroyed();
if (!active) { if (flightState._trackedModel) flightState._trackedModel.show = false; return; }
// Ask the frame-cached source directly. If the entity callback already ran,
// this is a no-op; if model loading completed between phases, it establishes
// this frame's single sample before the model renders. Camera, detection, and
// readout consumers then reuse that exact cached position.
const pos = parts.motion._trackedDisplayPosition(flightState._trackedIcao) || flightState._billboards.get(flightState._trackedIcao)?.position;
if (!pos) { if (flightState._trackedModel) flightState._trackedModel.show = false; return; }
if (!flightState._trackedModel && !flightState._trackedModelLoading && parts.tracking._trackedModelLoadAllowed()) {
flightState._trackedModelLoading = true;
const gen = flightState._trackedModelGen;
const trackedSpec = _modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
const trackedKey = _specKeyFor(flightState._flightData.get(flightState._trackedIcao)?.klass);
const trackedIrBoost = flightState._irBoost;
Cesium.Model.fromGltfAsync({
url: resolveAsset(trackedSpec.url),
asynchronous: false,
minimumPixelSize: TRACKED_MODEL_MIN_PX,
scale: trackedSpec.scale,
color: flightState._irBoost ? Cesium.Color.WHITE : Cesium.Color.CYAN,
colorBlendMode: Cesium.ColorBlendMode.MIX,
// The tracked aircraft uses the same dominant light tint as the fleet;
// IR boost removes the remaining diffuse hint with flat UNLIT white.
colorBlendAmount: flightState._irBoost ? 1.0 : trackedSpec.blendAmount,
customShader: flightState._irBoost ? flightState._IR_UNLIT_SHADER : undefined,
// Pick id (H1): without it, clicking the very plane being tracked read as
// EMPTY SPACE (scene.pick → primitive with no id) → an unintended
// deselect. With the icao, the click handler recognizes it as ours.
id: flightState._trackedIcao,
}).then((m) => {
// Untracked / re-tracked / torn down during the load → drop it.
if (gen !== flightState._trackedModelGen || !flightState._modelCollection || flightState._modelCollection.isDestroyed()) { try { m.destroy(); } catch { /* gone */ } return; }
// Class reclassified OR boost flipped mid-load (no release ran —
// _trackedModel was still null): drop the stale asset; driver reloads.
if (_specKeyFor(flightState._flightData.get(flightState._trackedIcao)?.klass) !== trackedKey || flightState._irBoost !== trackedIrBoost) {
try { m.destroy(); } catch { /* gone */ }
flightState._trackedModelLoading = false;
return;
}
// Assign after resolution as well as in the creation options so the
// standalone primitive always exposes the tracked aircraft pick id.
m.id = flightState._trackedIcao;
m._gevSpecKey = trackedKey; // class-change sync compares against this
m.show = false; // admitted, not yet the visual — the driver shows it once placed
// Seed the world transform before the primitive enters the scene. A model
// can become ready+shown between render phases; leaving Cesium's identity
// default here produces a one-frame jump to the Earth's center.
const currentPos = parts.motion._trackedDisplayCached()
|| flightState._billboards.get(flightState._trackedIcao)?.position;
if (currentPos) {
const displayPos = _modelDisplayPosition(flightState._trackedIcao, currentPos, flightState._scratchGroundPos);
if (displayPos) _modelMatrix(displayPos, parts.motion._trackedDisplayCourse(), m.modelMatrix);
}
flightState._trackedModel = m;
flightState._trackedModelLoading = false;
// A good load retires this selection's failure budget — a contact that
// recovers after a transient blip is not one attempt from giving up.
flightState._trackedModelFailIcao = null;
flightState._trackedModelFailCount = 0;
flightState._trackedModelRetryAtMs = 0;
flightState._modelCollection.add(m);
flightState._planeModelLoaded = true; // GLB cached — the tracked billboard can fade out once the model is up
}).catch((err) => {
if (gen !== flightState._trackedModelGen) return; // superseded load — not this selection's failure
flightState._trackedModelLoading = false;
parts.tracking._noteTrackedModelLoadFailure(trackedSpec.url, err);
});
return; // billboard carries the visual until the model is ready
}
if (flightState._trackedModel) {
// Keep the transform current while GPU resources are still loading. Cesium
// can flip ready during scene update after this callback; waiting for ready
// here would let that first rendered frame use a stale load-start matrix.
const displayPos = _modelDisplayPosition(flightState._trackedIcao, pos, flightState._scratchGroundPos);
if (!displayPos) {
flightState._trackedModel.show = false; // no ground evidence → the billboard carries it
return;
}
_modelMatrix(displayPos, parts.motion._trackedDisplayCourse(), flightState._trackedModel.modelMatrix);
if (!flightState._trackedModel.ready) return;
const spec = _modelSpec(flightState._flightData.get(flightState._trackedIcao)?.klass);
flightState._trackedModel.scale = trackedModelScaleForPixelCap({
baseScale: spec.scale,
nativeRadiusM: spec.nativeRadiusM,
rangeM: Cesium.Cartesian3.distance(flightState._viewer.camera.positionWC, displayPos),
viewportHeightPx: flightState._viewer.scene.canvas.clientHeight,
fovyRad: flightState._viewer.camera.frustum.fovy,
maximumPixelSize: TRACKED_MODEL_MAX_PX,
});
flightState._trackedModel.show = true;
}
}
function _fleetTick() {
if (!flightState._viewer || !flightState._billboardCollection || !flightState._billboardCollection.show) return;
const scene = flightState._viewer.scene;
const camera = flightState._viewer.camera;
const nowMs = focusNowMs(Date.now());
// (The tracked trail head is now the per-frame _trailHeadEntity segment — no 1 Hz
// primitive rebuild needed here anymore.)
if ((nowMs - flightState._lastFleetTickMs) < FLEET_DR_INTERVAL_MS) return;
const tickDtSec = flightState._lastFleetTickMs
? Math.min(COURSE_SLEW_DT_MAX_SEC, (nowMs - flightState._lastFleetTickMs) / 1000)
: 0.08;
flightState._lastFleetTickMs = nowMs;
_drainIrReloadQueue(); // bounded per-tick slice of any pending boost-flip reload
if (flightState._cockpitContactMode) parts.tracking._refreshCockpitNearContacts();
const poseSig = cameraPoseSignature(camera);
// Only the nearby Cockpit silhouettes need projected course; far dots are
// rotation-free. The per-contact gate below keeps the pip path cheap.
const doRotations = (poseSig !== flightState._lastCamPoseSig || (nowMs - flightState._lastRotPassMs) >= ROTATION_REFRESH_MS);
if (doRotations) {
flightState._lastCamPoseSig = poseSig;
flightState._lastRotPassMs = nowMs;
}
const occluder = horizonOccluder(camera);
const focusTarget = getFocusTarget();
// 3D model regime: only when enabled AND the camera is zoomed in past the altitude ceiling.
// Drop all models the moment we leave it (toggled off / zoomed out) so billboards resume.
const useModels = _modelRegimeActive();
// Drop live models AND invalidate in-flight loads on leaving the regime (else a load that
// resolves after zoom-out could briefly add a model outside the 3D-model regime).
if (!useModels && (flightState._models.size || flightState._modelPending.size)) _releaseModels();
// 3D-model eligibility: by DISTANCE (the mode's add/keep band), with ON-SCREEN PRIORITY under the
// cap. FOUR passes, visible-first, so the slots are spent on what you can see: (1) KEEP on-screen
// already-modeled (hysteresis for visible planes); (2) ADD on-screen new inside the add radius,
// nearest first; (3) KEEP off-screen already-modeled; (4) ADD off-screen new with leftover slots
// (so a plane just off the cone still models — the "planes right next to me aren't 3D" complaint).
// Crucially KEEP is SPLIT by frustum: an off-screen retained model (pass 3) can never starve an
// on-screen plane that wants one (passes 1–2) — a single KEEP-everything pass could fill the cap
// with off-screen retained models. Pure-distance let off-screen planes eat the cap; pure-frustum
// filtering dropped near off-screen planes entirely. This does both right.
let modelEligible = null;
if (useModels) {
const cap = _modelCap();
const camPos = camera.positionWC;
const addM = _modelAddDistM();
const addDistSq = addM * addM;
const keepM = _modelKeepDistM();
const keepDistSq = keepM * keepM;
const cull = camera.frustum.computeCullingVolume(camPos, camera.directionWC, camera.upWC);
// Candidates = planes within the KEEP radius, nearest first, each tagged with on-screen-ness.
const cand = [];
for (const [icao, bb] of flightState._billboards) {
if (icao === flightState._trackedIcao) continue;
// A converted TR-3B renders as a billboard and can never take a model, so
// it must not occupy a CAP SLOT either — excluded here at selection time,
// not just at the handoff below, or accumulated conversions would starve
// ordinary contacts of 3D models. (The handoff guard stays as defence.)
if (isTr3b(icao)) continue;
// Ground planes compete for model slots like everyone else (owner decision
// 2026-07-03: "3D mode is respected regardless of whether a plane is on the
// ground or in the air — no distinction"). The cap + nearest-first ordering
// below already bound airport clusters; grounded placement is handled by the
// one-shot ground snap in _modelDisplayPosition.
const d2 = Cesium.Cartesian3.distanceSquared(camPos, bb.position);
if (d2 > keepDistSq) continue; // beyond the keep radius → never eligible
Cesium.Cartesian3.clone(bb.position, flightState._scratchModelBS.center);
cand.push([icao, d2, cull.computeVisibility(flightState._scratchModelBS) !== Cesium.Intersect.OUTSIDE]);
}
cand.sort((a, b) => a[1] - b[1]); // nearest first
if (cand.length > cap && (nowMs - flightState._lastModelCapWarnMs) > 5000) {
console.warn(`[Data:Flights] ${cand.length} planes in 3D range; capped at ${cap} (${flightState._models3dMode}). On-screen planes are prioritized.`);
flightState._lastModelCapWarnMs = nowMs;
}
modelEligible = new Set();
// 1. KEEP on-screen already-modeled (visible retained — no flicker for what you can see).
for (const [icao, , inF] of cand) { if (modelEligible.size >= cap) break; if (inF && flightState._models.has(icao)) modelEligible.add(icao); }
// 2. ADD on-screen NEW inside the add radius, nearest first (visible additions win the cap).
for (const [icao, d2, inF] of cand) { if (modelEligible.size >= cap) break; if (inF && d2 <= addDistSq && !modelEligible.has(icao)) modelEligible.add(icao); }
// 3. KEEP off-screen already-modeled (hysteresis, but LOWER priority than anything visible — so a
// retained off-screen model can never starve an on-screen plane that wants one; dropping it is
// invisible and it re-adds the moment it's back in view).
for (const [icao, , inF] of cand) { if (modelEligible.size >= cap) break; if (!inF && flightState._models.has(icao)) modelEligible.add(icao); }
// 4. ADD off-screen NEW inside the add radius with any leftover slots.
for (const [icao, d2, inF] of cand) { if (modelEligible.size >= cap) break; if (!inF && d2 <= addDistSq && !modelEligible.has(icao)) modelEligible.add(icao); }
const toRelease = [];
for (const icao of flightState._models.keys()) {
if (icao !== flightState._trackedIcao && !modelEligible.has(icao)) toRelease.push(icao);
}
for (const icao of toRelease) _releaseModel(icao);
}
for (const [icao24, bb] of flightState._billboards) {
if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion
const info = flightState._flightData.get(icao24);
const dr = parts.motion._deadReckon(icao24, flightState._scratchFleetPos);
// The dead-reckoned point drifts away from the fix whose cell supplied the
// height, so a grounded contact's sprite ends up under the mesh it taxied
// (or coasted) over. Re-floor at the DISPLAYED coordinate — read-only, and
// never while a 3D model owns the visual (T7).
const display = parts.motion._floorGroundedDisplayPosition(icao24, info, dr, _modelOwnsVisual(icao24), nowMs);
// Gate the write — assigning Billboard.position dirties the whole
// collection's vertex buffer, so skip sub-meter moves.
if (display && Cesium.Cartesian3.distanceSquared(display, bb.position) > 1.0) {
bb.position = display;
}
// Round 6: occlusion-test a LIFTED point for contacts rendering below
// (or within a wingspan of) the ellipsoid — EllipsoidalOccluder judges a
// sub-ellipsoid point near the limb "beyond the horizon" and the fleet
// pass would hide a plane that is really just low over high-N terrain
// waiting for its floor to warm (ATL grounded contacts at geoid −31 m).
const beyondHorizon = !occluder.isPointVisible(info?.cullPosition || bb.position);
// A billboard flipping INTO view (horizon reveal while the camera idles)
// gets its rotation refreshed THIS tick even without a pose change —
// otherwise it reappears wearing its stale (often creation-north) nose for
// up to ROTATION_REFRESH_MS. (Model-handed-off planes also read show=false
// here; harmless — the model branch below `continue`s past the rotation.)
const revealed = !beyondHorizon && !bb.show;
if (bb.show === beyondHorizon) bb.show = !beyondHorizon;
if (beyondHorizon) {
// Also hide any 3D model — otherwise a model that crossed the limb would keep
// rendering through the hidden globe at its last matrix.
const m = flightState._models.get(icao24);
if (m && m.show) m.show = false;
continue;
}
// One order-independent write site composes freshness × focus × limb haze
// for alpha, and base class/ground scale × limb taper for scale. Cesium's
// locked NearFarScalar remains a separate multiplicative stage. This
// narrowly amends always-visible rendering without count culling or zero
// alpha; nearer focus behavior remains tunable rather than universal.
const cameraDistanceM = Cesium.Cartesian3.distance(camera.positionWC, bb.position);
const distanceScale = nearFarScalarValueAtDistance(bb.scaleByDistance, cameraDistanceM);
const focus = advanceProjectedSpriteFocus(
bb,
bb.position,
scene,
camera,
nowMs,
focusTarget,
undefined,
(bb.width || 20) * (bb.scale || 1) * distanceScale * 0.5,
(bb.height || 20) * (bb.scale || 1) * distanceScale * 0.5,
);
const isCockpitNear = flightState._cockpitContactMode && flightState._cockpitNearContacts.has(icao24);
const baseColor = flightState._cockpitContactMode && !isCockpitNear
? (isMilitaryIcao(icao24) ? MIL_TINT : COCKPIT_CIVILIAN_COLOR)
: _fleetBillboardColor(icao24);
const treatment = applyAircraftBillboardTreatment({
billboard: bb,
baseScale: flightState._cockpitContactMode && !isCockpitNear ? 1 : _fleetBillboardScale(icao24, info?.klass),
baseAlpha: flightState._missingPolls.get(icao24) ? 0.45 : 1,
baseColor,
focusFactor: focus.factor,
cameraDistanceM,
cameraHeightM: camera.positionCartographic?.height,
});
flightState._billboardLimbScale.set(bb, treatment.factors.scale);
// Two-tier glyph raster (owner playtest 2026-08-16): the billboard atlas
// has no mipmaps, so no single texture stays crisp across the ~25–150
// device-px range scaleByDistance produces. Swap between the 64 px fleet
// raster and the 192 px close raster on the billboard's ACTUAL on-screen
// size — post-treatment bb.scale, so focus/limb recession counts — with
// hysteresis so zoom oscillation never thrashes the atlas.
if (!flightState._cockpitContactMode || isCockpitNear) {
const glyphDevPx = (bb.width || 20) * (bb.scale || 1)
* distanceScale * (globalThis.devicePixelRatio || 1);
const wantLarge = bb._gevIconLarge ? glyphDevPx > 56 : glyphDevPx > 76;
if (wantLarge !== !!bb._gevIconLarge) {
bb._gevIconLarge = wantLarge;
bb.image = aircraftIcon(_iconKind(icao24, info?.klass), wantLarge ? TRACKED_ICON_PX : undefined);
}
}
// Smoothed display course: the path direction _deadReckon just reported
// for THIS aircraft (nothing else calls _deadReckon in between), rate-
// limited so segment-boundary course steps glide instead of snapping.
// The slew cap eases toward COURSE_MIN_DPS at low speed, and a hovering
// aircraft (hold flag) keeps its previous nose direction outright.
const rawCourse = flightState._drCourseDeg != null ? flightState._drCourseDeg : ((info && info.true_track) || 0);
const prevCourse = flightState._displayCourse.get(icao24);
const course = (flightState._drCourseHold && prevCourse != null)
? prevCourse
: limitCourseStep(
prevCourse, rawCourse,
courseSlewCapDps(flightState._drSpeedMps != null ? flightState._drSpeedMps : ((info && info.velocity) ?? NaN), COURSE_MAX_DPS),
tickDtSec,
);
flightState._displayCourse.set(icao24, course);
// 3D model takes over from the billboard for in-view planes (modelEligible). GAP-PROOF: the
// billboard stays shown until the model is actually READY to render, so a plane is never both
// iconless AND modelless (the "planes vanish when 3D turns on" bug). Position the model every
// tick regardless so it's framed the instant it becomes ready.
// Converted TR-3Bs stay 2D on purpose: the Easter egg IS the triangle, and
// there is no GLB for it, so the model handoff is suppressed rather than
// fed a stand-in mesh. The billboard keeps rendering (and keeps satisfying
// the getNearby/getDetectableObjects `bb.show` visibility guards), so a
// converted contact still works in Contacts and Cockpit.
if (useModels && dr && modelEligible.has(icao24) && !isTr3b(icao24)) {
_ensureModel(icao24);
const model = flightState._models.get(icao24);
const ownsVisual = _driveFleetModelHandoff(
icao24,
model,
bb,
dr,
course,
() => {
applyAircraftModelTreatment({
model,
// IR boost must survive the per-tick treatment write — otherwise
// any alpha change would repaint the ordinary tint over the hot
// white. Boosted models also skip the recession fade: hot targets
// stay full-strength at any range (billboards keep their normal
// fade — full-opacity glyph walls read as overwhelming).
baseColor: flightState._irBoost ? Cesium.Color.WHITE : _modelColor(icao24),
alpha: flightState._irBoost ? 1 : treatment.alpha,
});
},
);
if (ownsVisual) continue; // skip billboard rotation
}
if ((!flightState._cockpitContactMode || isCockpitNear) && (doRotations || revealed)) {
const rot = screenProjectedRotation(scene, bb.position, course, bb.rotation);
if (rot !== null && Math.abs(rot - bb.rotation) > 0.002) {
bb.rotation = rot;
}
}
}
}
return { _fleetBillboardColor, _fleetBillboardScale, _groundDepthDistance, _modelSpec, _normalBillboardScaleByDistance, _cockpitBillboardScaleByDistance, _iconKind, _applyFleetBillboardPresentation, _modelColor, _modelRegimeActive, _modelCap, _modelAddDistM, _modelKeepDistM, _modelMatrix, _groundSampleExclusions, _modelDisplayPosition, _driveFleetModelHandoff, _modelOwnsVisual, _modelIsRendering, _specKeyFor, _syncModelToClass, _reloadModelsForIrBoost, _drainIrReloadQueue, _ensureModel, _releaseModel, _cleanupModelGen, _releaseModels, _releaseTrackedModel, _updateTrackedModel, _fleetTick };
}
+548
View File
@@ -0,0 +1,548 @@
import * as Cesium from 'cesium';
import { ENRICH_AMBIENT_BUDGET_CEIL } from './policy.js';
export function createFlightState({ source, services }) {
const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() };
flightState._source = source;
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs
* vertex-baked to real-world METERS in the airplane.glb axis convention, so
* they render at scale 1 with their own measured belly lift and bounding
* radius. Every other class keeps the shared-airplane.glb formula
* (MODEL_SCALE × CLASS_SCALE_3D). nativeRadiusM is PER SCALE UNIT — pixel-cap
* math multiplies it by `scale`, so world radius = nativeRadiusM × scale in
* both branches. The code-side MIX tint dominates every existing asset so
* class silhouettes stay light without modifying third-party GLBs/textures. */
/* Specs are static per class, and the detection weld now asks for one per
* MODELED contact per frame (up to the fleet cap) on top of the 12 Hz fleet
* pass — so this is memoized, as militaryFlights.js already does. */
flightState._specCache = new Map();
/** One-shot cached tile-skin heights for MODELED grounded planes (see groundSnap.js). */
flightState._groundSnap = createGroundSnap();
flightState._scratchGroundCarto = new Cesium.Cartographic();
flightState._scratchGroundPos = new Cesium.Cartesian3();
/** @type {Cesium.PrimitiveCollection|null} */
flightState._modelCollection = null;
/** @type {Map<string, Cesium.Model>} icao24 → model */
flightState._models = new Map();
/** @type {Set<string>} icao24 currently loading (async) */
flightState._modelPending = new Set();
/** @type {Map<string, number>} icao24 → load generation; bumped on release to invalidate
* an in-flight load (so a track/untrack/remove during fromGltfAsync can't add a stale model). */
flightState._modelGen = new Map();
/** Lifecycle epoch; bumped on destroy so an in-flight load from a PREVIOUS init can't settle
* against a new lifecycle's globals (which destroy cleared). Captured by _ensureModel. */
flightState._modelEpoch = 0;
/** DEFAULT-ON in PROXIMITY (owner directive 2026-08-22). A fresh boot never runs
* layer-state restoration, so this initializer — not the codec — is what the app
* actually starts with; it must stay in lockstep with the `models3d` default in
* `layerState.js` and `this._models3dEnabled` in ui.js, or the DISPLAY rail would
* light a button the layer has not armed. */
flightState._models3dEnabled = true;
flightState._models3dMode = 'proximity';
// 'proximity' = nearest MODEL_MAX in view; 'all' = every in-view plane (≤ MODEL_MAX_ALL)
flightState._lastModelCapWarnMs = 0;
// throttle the "more planes in view than the cap" console notice
// The tracked entity's billboard goes transparent (rather than hidden) once the model takes
// over, so it keeps supplying a bounding sphere for follow-camera framing. We only drop its
// alpha after the GLB is preloaded so the model is ready to render the instant the billboard
// fades — no gap, no double-image. Preloaded once at init; the instance is retained (not
// destroyed) purely to keep Cesium's glTF cache warm for fast tracked-model instantiation.
flightState._planeModelLoaded = false;
/** @type {Cesium.Model|null} retained preload that keeps the glTF cache warm */
flightState._preloadModel = null;
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
flightState._scratchModelMtx = new Cesium.Matrix4();
flightState._scratchModelBS = new Cesium.BoundingSphere(new Cesium.Cartesian3(), 1.0);
// frustum-visibility test
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
flightState._billboardLimbScale = new WeakMap();
// keep last N positions per aircraft
// ---------------------------------------------------------------------------
// Module-level state: billboard collection and per-aircraft lookup maps
// ---------------------------------------------------------------------------
/** @type {Cesium.BillboardCollection|null} */
flightState._billboardCollection = null;
/** @type {Map<string, Cesium.Billboard>} icao24 -> billboard primitive */
flightState._billboards = new Map();
/** Stable lightweight records reused by the detection overlay between polls. */
flightState._detectionObjects = new Map();
/** @type {Map<string, {callsign:string, altitude:number, velocity:number, true_track:number}>} */
flightState._flightData = new Map();
/** DEV-only explicit-position contacts used by qa-focus-evidence.mjs. */
flightState._focusEvidenceIds = new Set();
/** @type {Map<string, Array<{time:Cesium.JulianDate, position:Cesium.Cartesian3}>>} */
flightState._positionHistory = new Map();
/** @type {boolean} True once ensureGeoidReady() has resolved (awaited once at enable()) */
flightState._geoidReady = false;
/** @type {Map<string, number>} icao24 -> geoid undulation N (m), cached (negligible drift per-aircraft). */
flightState._geoidNCache = new Map();
/** @type {number} Current number of visible aircraft */
flightState._count = 0;
/** @type {number|null} Epoch ms of last successful API update */
flightState._lastUpdate = null;
/** @type {boolean} True while in a backoff/cooldown window */
flightState._backoff = false;
/** @type {number} Epoch ms — earliest time the next fetch is allowed */
flightState._retryAt = 0;
/** @type {string|null} Human-readable error string shown in stats chip */
flightState._lastError = null;
flightState._activeUpdateControllers = new Set();
/** @type {number|null} HTTP status of the most recent API response */
flightState._lastStatus = null;
/** @type {string} Source used by the latest successful snapshot. */
flightState._lastSource = source?.label || 'Aircraft';
/** @type {string} Completeness boundary for the latest successful snapshot. */
flightState._lastCoverage = 'worldwide upstream snapshot';
// ---------------------------------------------------------------------------
// Click-to-track state
// ---------------------------------------------------------------------------
/** @type {string|null} ICAO24 of the currently tracked aircraft */
flightState._trackedIcao = null;
flightState._pendingTrackingRestore = null;
flightState._trackingIntentGeneration = 0;
flightState._trackingRefreshEpoch = 0;
flightState._lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
ids: new Set(),
source: flightState._lastSource,
coverage: null,
};
/** @type {Cesium.Entity|null} Entity used for camera tracking */
flightState._trackedEntity = null;
/** Disposes the single active tracked-camera framing owner. */
flightState._trackedCameraFrameStop = null;
/** @type {Cesium.Model|null} Standalone 3D model for the tracked aircraft. Deliberately NOT a
* graphic on _trackedEntity: viewer.trackedEntity derives the follow-camera from the entity's
* bounding sphere, and a model graphic reports PENDING until its glTF loads — which stalls (or, on
* 3D-toggle, freezes) the centering. A pure-billboard entity always supplies a ready sphere; the
* model rides in _modelCollection and is driven per-frame, fully decoupled from the camera. */
flightState._trackedModel = null;
/** Bumped on untrack/teardown so an in-flight tracked-model load resolves into a no-op. */
flightState._trackedModelGen = 0;
flightState._trackedModelLoading = false;
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler on the scene canvas */
flightState._clickHandler = null;
/** @type {Cesium.Viewer|null} Cached viewer reference */
flightState._viewer = null;
/** Cockpit presentation switches ambient AIR contacts between near aircraft and far dots. */
flightState._cockpitContactMode = false;
/** AIR contacts inside the selected Display range; independent from model admission/load/cap. */
flightState._cockpitNearContacts = new Set();
/** Normalized ICAO24 of the active Cockpit subject, omitted from detection candidates. */
flightState._cockpitSubjectId = null;
/** @type {((event: CustomEvent) => void)|null} */
flightState._cockpitModeListener = null;
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Shared fading-trail renderer */
flightState._trail = null;
/** @type {Cesium.Entity|null} Cheap 2-point head segment bridging the last fix to the LIVE
* dead-reckoned icon, updated per frame via a CallbackProperty — so the trail head stays
* glued to the 12 Hz icon without rebuilding the 400-point trail primitive every frame. */
flightState._trailHeadEntity = null;
/** @type {number} Uniquifier for head-segment entity ids (Cesium requires unique ids). */
flightState._trailHeadSeq = 0;
/** @type {Cesium.Cartesian3[]} Chronological tracked-aircraft fixes (oldest first) */
flightState._trailPositions = [];
/** @type {number} Monotonic token — invalidates in-flight backfill responses */
flightState._trailBackfillToken = 0;
/** @type {Map<string, number>} icao24 -> consecutive missed polls */
flightState._missingPolls = new Map();
/** @type {number} Epoch ms of the last fleet dead-reckoning pass */
flightState._lastFleetTickMs = 0;
/** @type {string} Camera pose signature at the last rotation pass */
flightState._lastCamPoseSig = '';
/** @type {number} Epoch ms of the last full rotation pass */
flightState._lastRotPassMs = 0;
/** @type {number} Last computed rotation for the tracked entity (radians) */
flightState._lastTrackedRotation = 0;
/** @type {Cesium.Event.RemoveCallback|null} preRender listener disposer */
flightState._preRenderRemove = null;
flightState._trackedModelPreUpdateRemove = null;
/** @type {Cesium.Event.RemoveCallback|null} camera.moveEnd listener disposer (arrival rotation pass) */
flightState._moveEndRemove = null;
/** @type {Cesium.Event.RemoveCallback|null} trackedEntityChanged listener disposer (cross-layer untrack) */
flightState._trackedEntityChangedRemove = null;
/** @type {(() => void)|null} militaryRegistry active-transition unsubscribe (M2 handoff sweep) */
flightState._milActiveChangeUnsub = null;
// ---------------------------------------------------------------------------
// Scratch (reusable) variables — avoid per-frame heap allocation
// ---------------------------------------------------------------------------
flightState._scratchOffset = new Cesium.Cartesian3();
flightState._scratchCarto = new Cesium.Cartographic();
flightState._scratchEnu = new Cesium.Matrix4();
flightState._scratchArc = { east: 0, north: 0, endCourseDeg: 0 };
flightState._scratchRenderTime = new Cesium.JulianDate();
flightState._scratchFleetPos = new Cesium.Cartesian3();
flightState._scratchDrRaw = new Cesium.Cartesian3();
flightState._scratchWarmupTime = new Cesium.JulianDate();
flightState._trackedPosHolder = new Cesium.Cartesian3();
// ---------------------------------------------------------------------------
// Per-frame cache for the tracked entity's dead-reckoned position.
// The position, alignedAxis, and rotation CallbackProperties all fire each
// render frame; caching avoids running _deadReckon three times.
// ---------------------------------------------------------------------------
/** @type {Cesium.Cartesian3|null} */
flightState._cachedDRPosition = null;
/** @type {number} Frame number for which _cachedDRPosition is valid */
flightState._cachedDRFrame = -1;
/** Course (deg) of the position `_deadReckon` most recently returned — set on
* every branch of `_deadReckon`, read IMMEDIATELY by the caller (same
* synchronous flow; module-scratch idiom, like the Cartesian scratches). */
flightState._drCourseDeg = null;
/** Sibling scratches of _drCourseDeg: the displayed ground speed of the motion
* `_deadReckon` just returned, and whether that motion is too slow for ANY
* course source to be trusted (hover/GPS drift — consumers HOLD their
* previous display course instead of chasing noise). */
flightState._drSpeedMps = null;
flightState._drCourseHold = false;
/** Sibling scratch: whether the position `_deadReckon` just returned came from
* EXTRAPOLATION (coasting past the newest fix, or the pre-history warm-up)
* rather than interpolation between two known fixes. The display-floor
* corridor needs it — an interpolating contact is walking TOWARD its newest
* fix, a coasting one is walking AWAY from it along its course, and warming
* the wrong end leaves a coaster permanently ahead of its floor data. */
flightState._drExtrapolating = false;
/** Frame-cached course for the tracked aircraft (sibling of _cachedDRPosition). */
flightState._cachedDRCourse = null;
/** Frame-cached siblings of _cachedDRCourse (same discipline). */
flightState._cachedDRSpeedMps = null;
flightState._cachedDRHold = false;
/** Wall-clock of the tracked course limiter's last advance (dt source only —
* the course VALUE lives in the shared per-icao _displayCourse map below). */
flightState._trackedCourseMs = 0;
/** Per-aircraft smoothed display course — the SINGLE source of truth for the
* nose direction an aircraft displays. The fleet pass reads/writes it at
* tick cadence for untracked planes; _trackedDisplayCourse reads/writes the
* SAME entry per frame for the tracked plane (the fleet pass skips the
* tracked icao, so exactly one writer owns an entry at a time). Sharing the
* entry — including its slew state — is what makes the tracked↔fleet
* handoff seamless (2026-07-03 field fix: separate states froze the fleet
* entry while tracked, so clicking / un-clicking a 65 kt helicopter FLIPPED
* its nose — "looks like it's going in reverse"). */
flightState._displayCourse = new Map();
flightState._enrichActive = 0;
flightState._enrichLastDispatchMs = 0;
/** @type {ReturnType<typeof setTimeout>|null} pending drip wake-up */
flightState._enrichDripTimer = null;
flightState._enrichQueue = [];
flightState._enrichSeen = new Set();
flightState._enrichAmbientBudget = ENRICH_AMBIENT_BUDGET_CEIL;
/** Epoch ms the bucket last accounted a refill window from (0 = unset). */
flightState._enrichAmbientRefillAnchorMs = 0;
flightState._drCorrection = new Cesium.Cartesian3(0, 0, 0);
flightState._drCorrectionStartMs = 0;
flightState._drPrevRaw = new Cesium.Cartesian3();
flightState._drPrevDisplay = new Cesium.Cartesian3();
flightState._drPrevMs = 0;
flightState._drReconcileValid = false;
/** @type {string|null} icao the reconciliation state currently belongs to */
flightState._drReconcileIcao = null;
/** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */
flightState._trackedVisualPos = new Cesium.Cartesian3();
flightState._trackedTrailPos = new Cesium.Cartesian3();
/** Scratch for the tracked model's world-space origin (the envelope centre). */
flightState._scratchTrailClip = new Cesium.Cartesian3();
/** Scratch for the shortened trail-head start. Owned by the head-segment
* callback alone, so nothing else can overwrite it mid-frame. */
flightState._scratchTrailHead = new Cesium.Cartesian3();
/** Hysteresis latch for the tracked contact's zoom regime, plus the selection it
* belongs to. Scoped per selection so a NEW target re-evaluates against the ENTER
* ceiling instead of inheriting the previous target's looser EXIT band. */
flightState._trackedZoomLatched = false;
flightState._trackedZoomLatchIcao = null;
flightState._trackedModelFailIcao = null;
flightState._trackedModelFailCount = 0;
flightState._trackedModelRetryAtMs = 0;
/** @type {Cesium.Cartographic} Scratch for the grounded display-floor read. */
flightState._scratchDisplayCarto = new Cesium.Cartographic();
/** @type {Map<string, {cell: {lat: number, lon: number}, effectiveM: number|null,
* in: Cesium.Cartesian3, out: Cesium.Cartesian3|null, heldM: number|null,
* heldCell: {lat: number, lon: number}|null,
* heldTier: 'own'|'neighbor'|null, heldActive: boolean, seeded: boolean,
* probeMs: number|null, retiredMs: number|null,
* easedM: number|null, easeMs: number|null}>} Per-grounded-contact
* display-floor state: the cell it is currently reading (boundary hysteresis),
* the last input position and the effective floor that produced the cached
* output (rebuild skip), the last floor that actually RESOLVED for it plus the
* tier it came from (the hold, below), whether that floor is a REHYDRATED SEED
* rather than something measured while the contact stood here (`seeded` — it
* ranks below live evidence), and the value the downward ease is currently
* displaying while it approaches a lower floor.
* Dropped with the contact, and whenever it stops being a grounded billboard. */
flightState._displayFloorState = new Map();
/** @type {number} Poll counter handed to the corridor allocator: it rotates
* runs of EQUALLY needy contacts so a tie larger than the budget cycles across
* polls instead of the same prefix winning forever. */
flightState._corridorEpoch = 0;
/** @type {Cesium.Cartographic} Scratch for the corridor's display-end read. */
flightState._scratchCorridorCarto = new Cesium.Cartographic();
/** @type {Cesium.Cartesian3} Scratch for the corridor's dead-reckon probe. */
flightState._scratchCorridorPos = new Cesium.Cartesian3();
/** IR hot-target mode (owner playtest 2026-08-16): the NVG/FLIR post-styles
* map LUMINANCE, so mid-gray textured models read cold and vanish into
* terrain. While a boost style is active every model renders flat white
* (hottest); per-spec color/tint restores on style exit. Driven by ui.js
* setStyle via the `irBoost` layer param. */
flightState._irBoost = false;
/** Boosted models render UNLIT (owner cockpit-FLIR field rounds, 2026-08-16):
* the white tint alone is applied to the MATERIAL, so Cesium still
* sun-shades it — near-horizon viewing shows a plane's SIDE, ~90° to a high
* sun, so it rendered near-BLACK in FLIR/NVG while sun-lit neighbors glowed.
* LightingModel.UNLIT emits the flat white directly, orientation be damned.
* CRITICAL (field-verified via scene.pick): assigning customShader to an
* already-READY model is a silent no-op — the property sets but the shader
* program never rebuilds. The boost therefore flips by RELEASE-AND-RELOAD
* (see setParams), so every boosted model gets the shader AT CREATION.
* One shared shader instance — stateless, safe across models. */
flightState._IR_UNLIT_SHADER = new Cesium.CustomShader({ lightingModel: Cesium.LightingModel.UNLIT });
flightState._irReloadQueue = null;
return flightState;
}
+191
View File
@@ -0,0 +1,191 @@
import * as Cesium from 'cesium';
import { CYAN_TRANSPARENT } from './policy.js';
export function createTesting({ flightState, services, parts, layer, resolveAsset }) {
/** Test seam for the display-floor clamp (unit-tested against real Cesium math
* with seeded mesh cells — the drift mechanism is otherwise only reachable
* through a live poll + render loop). */
function _floorGroundedDisplayPositionForTest(
info, pos, modelOwnsVisual, icao24 = '__test__', nowMs = Date.now(),
) {
return parts.motion._floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs);
}
/** Test hook: drops the per-contact display-floor state (hysteresis + rebuild
* cache) so each case starts clean. There is no cross-contact state to reset —
* the clamp is per-contact all the way down. */
function _clearDisplayFloorStateForTest() {
flightState._displayFloorState.clear();
}
/**
* Seed only the mutable state needed to exercise tracked-card refreshes through
* the production poll reconciler. Tests still call `flightsLayer.update()`;
* this seam avoids constructing the browser-only Cesium layer lifecycle.
* @param {object} state
* @param {string} state.icao24
* @param {object} state.entity
* @param {object} state.meta
* @param {object} state.billboard
* @param {object} state.billboardCollection
* @param {object} state.viewer
* @param {Array<Object>} [state.history=[]]
* @param {boolean} [state.tracked=true]
* @param {Iterable<[string, object]>} [state.models=[]] - Fleet 3D models keyed
* by icao24, for the billboard-hidden/model-shown handoff state.
*/
function _setTrackedFlightRefreshStateForTest({
icao24,
entity,
meta,
billboard,
billboardCollection,
viewer,
history = [],
tracked = true,
models = [],
modelCollection = null,
}) {
flightState._viewer = viewer;
flightState._modelCollection = modelCollection;
flightState._billboardCollection = billboardCollection;
flightState._billboards = new Map([[icao24, billboard]]);
flightState._models.clear();
for (const [key, model] of models) flightState._models.set(key, model);
flightState._detectionObjects = new Map();
flightState._flightData = new Map([[icao24, meta]]);
flightState._positionHistory = new Map([[icao24, history]]);
flightState._missingPolls = new Map();
flightState._displayCourse.clear();
flightState._geoidNCache.clear();
flightState._trackedIcao = tracked ? icao24 : null;
flightState._trackedEntity = tracked ? entity : null;
flightState._trackedModel = null;
parts.tracking._cancelPendingTrackingRestore();
flightState._trackedModelLoading = false;
// NOTE: deliberately does NOT reset the per-selection latches. They are
// production state owned by the tracking lifecycle (_resetTrackedSelectionState),
// and clearing them here would mask exactly the deselect→re-track hole this
// seam is used to test.
flightState._backoff = false;
flightState._retryAt = 0;
}
/** Seed the authoritative snapshot outcome used by share-Follow tests. */
function _setFlightTrackingRefreshOutcomeForTest({
status = 'accepted',
ids = [],
source = 'OpenSky Network',
coverage = 'test',
} = {}) {
const epoch = ++flightState._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = {
epoch,
status,
ids: new Set(ids.map((id) => String(id).trim().toLowerCase())),
source,
coverage,
};
}
/** Add a cached contact so tests can model a target arriving on a later feed. */
function _addFlightTrackingCandidateForTest({ icao24, meta, billboard, history = [] }) {
flightState._billboards.set(icao24, billboard);
flightState._flightData.set(icao24, meta);
flightState._positionHistory.set(icao24, history);
}
/** Expose the military-suppression decision for the civil duplicate. */
function _militaryLayerSuppressesForTest(icao24) {
return parts.tracking._militaryLayerSuppresses(icao24);
}
/** Arm the deferred restore latch directly, without a full setParams turn. */
function _armFlightTrackingRestoreForTest(id, origin = 'share-restore') {
flightState._pendingTrackingRestore = id === null
? null
: { id, generation: flightState._trackingIntentGeneration, origin };
}
/** Return the deferred restore target held by the production tracker. */
function _pendingFlightTrackingRestoreForTest() {
return flightState._pendingTrackingRestore?.id ?? null;
}
/** Exercise the production deferred-restore retry after a simulated feed refresh. */
function _applyPendingFlightTrackingRestoreForTest() {
return parts.tracking._applyPendingTrackingRestore();
}
/** Set the exact Cockpit subject through the production state transition for focused tests. */
function _setCockpitDetectionSubjectForTest(active, subjectId = null) {
parts.tracking._applyCockpitState({ active, subjectId });
}
/** Evaluate the TRACKED contact's zoom regime through the production predicate.
* The decision is latch-bearing (default-on, hysteretic, cockpit/TR-3B-suppressed)
* and otherwise only observable through a live scene, so tests drive it here. */
function _trackedModelRegimeActiveForTest() {
return parts.tracking._trackedModelRegimeActive();
}
/** Run one frame of the production tracked-model driver (normally a
* `scene.preUpdate` listener) so tests can pin its bounded load retries. */
function _updateTrackedModelForTest() {
return parts.rendering._updateTrackedModel();
}
/** Evaluate the production tracked-billboard handoff colour for focused tests. */
function _trackedBillboardColorForTest() {
return parts.rendering._modelOwnsVisual(flightState._trackedIcao) ? CYAN_TRANSPARENT : Cesium.Color.CYAN;
}
/** Drive the exact fleet billboard-to-model handoff used by `_fleetTick`. */
function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
return parts.rendering._driveFleetModelHandoff(
icao24,
flightState._models.get(icao24),
flightState._billboards.get(icao24),
position,
course,
);
}
/** Exercise the exact asynchronous fleet loader and return its admitted model. */
async function _ensureFleetModelForTest(icao24) {
await parts.rendering._ensureModel(icao24);
return flightState._models.get(icao24) || null;
}
return { _floorGroundedDisplayPositionForTest, _clearDisplayFloorStateForTest, _setTrackedFlightRefreshStateForTest, _setFlightTrackingRefreshOutcomeForTest, _addFlightTrackingCandidateForTest, _militaryLayerSuppressesForTest, _armFlightTrackingRestoreForTest, _pendingFlightTrackingRestoreForTest, _applyPendingFlightTrackingRestoreForTest, _setCockpitDetectionSubjectForTest, _trackedModelRegimeActiveForTest, _updateTrackedModelForTest, _trackedBillboardColorForTest, _driveFleetModelHandoffForTest, _ensureFleetModelForTest };
}
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -1,3 +1,4 @@
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
@@ -24,10 +25,10 @@ function sweepLayerParamKeys() {
const dataDir = new URL('../data/', import.meta.url);
for (const entry of fs.readdirSync(dataDir)) {
if (!entry.endsWith('.js')) continue;
const source = fs.readFileSync(new URL(entry, dataDir), 'utf8');
const source = readLayerSource(new URL(entry, dataDir));
// Every getParams() in this codebase is a plain object return; take the
// body up to its closing brace and read the keys it publishes.
const body = source.match(/\n {2}getParams\(\)\s*\{[\s\S]*?\n {2}\},/);
const body = source.match(/\n\s+getParams\(\)\s*\{[\s\S]*?\n\s+\},/);
if (!body) continue;
// A key always follows `{` or `,` — which matches both the multi-line
// returns and the single-line `return { passive: … }` form, while a
+12
View File
@@ -0,0 +1,12 @@
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
/** Read actual component owners for structural regression assertions. */
export function readLayerSource(file) {
const path = file instanceof URL ? fileURLToPath(file) : file;
if (basename(path) !== 'flights.js') return readFileSync(path, 'utf8');
const directory = join(dirname(path), '../layers/flights');
return readdirSync(directory).filter((name) => name.endsWith('.js'))
.sort().map((name) => readFileSync(join(directory, name), 'utf8')).join('\n');
}