refactor: separate civil-flight records and rendering

This commit is contained in:
Sameh Khamis committed 2026-09-14 15:55:37 -07:00
1 parent a658e63fed
commit 4cb7334da5
27 files changed
+1113 -819

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# Changelog # Changelog
- Separate civil-flight acquisition and record reconciliation from Cesium resource updates, preserving source timing, ground placement and tracking behavior.
- Separate navigation, share restoration, visual settings and panel state into focused UI owners with explicit dependencies and terminal cleanup. - Separate navigation, share restoration, visual settings and panel state into focused UI owners with explicit dependencies and terminal cleanup.
- Separate layer lifecycle transactions from panel construction and render/detection reactions; retain existing transition and refresh behavior. - Separate layer lifecycle transactions from panel construction and render/detection reactions; retain existing transition and refresh behavior.
+14
View File
@@ -477,3 +477,17 @@ These package groups contain their own dependencies and exclude application
assembly. `applicationShell` retains composition and compatibility methods; assembly. `applicationShell` retains composition and compatibility methods;
those methods delegate to the state owner. Source-based regression checks inspect those methods delegate to the state owner. Source-based regression checks inspect
that implementation owner, and browser acceptance exercises the assembled UI. that implementation owner, and browser acceptance exercises the assembled UI.
## Civil-flight records and acquisition
`layers/flights/records` owns metadata, sticky observations, geoid values and
missing-poll admission. `layers/flights/ingestion` owns the source and request
lifetime, backoff and freshness state. Both are portable package groups with no
Cesium, viewer, billboard, model or application imports.
The layer composes these with `snapshotRenderer`, which applies record changes
to Cesium history and primitives and coordinates existing follow operations.
Occlusion points live with rendering state. Eviction still releases tracking
before deleting records; incomplete snapshots retain recent contacts for the
existing bounded interval. The renderer retains its existing per-frame scratch
objects; reconciliation occurs on source refresh, not on each frame.
+7
View File
@@ -1,5 +1,12 @@
# God's Eye View Current State # God's Eye View Current State
Civil flights separate source acquisition, portable record reconciliation and
scene application. The record owner keeps sticky metadata, geoid cache and
bounded missing-poll state; ingestion owns cancellation, source freshness and
backoff. The snapshot renderer owns Cartesian history, billboard/model changes
and follow transitions. Aviation altitude remains separate from render height;
partial retention, military suppression and ground-sampling policies are unchanged.
The UI facade composes separate owners for navigation authority, share restoration, The UI facade composes separate owners for navigation authority, share restoration,
visual settings and panel chrome. Navigation owns request generations and follow visual settings and panel chrome. Navigation owns request generations and follow
handoff; restoration owns its coordinator, deferred notices, gestures and timer; handoff; restoration owns its coordinator, deferred notices, gestures and timer;
+3 -1
View File
@@ -200,6 +200,8 @@
"./ui/navigation": "./src/ui/navigationController.js", "./ui/navigation": "./src/ui/navigationController.js",
"./ui/share-restoration": "./src/ui/shareRestoration.js", "./ui/share-restoration": "./src/ui/shareRestoration.js",
"./ui/visual-settings": "./src/ui/visualSettings.js", "./ui/visual-settings": "./src/ui/visualSettings.js",
"./ui/panel-chrome": "./src/ui/panelChrome.js" "./ui/panel-chrome": "./src/ui/panelChrome.js",
"./layers/flights/records": "./src/layers/flights/records.js",
"./layers/flights/ingestion": "./src/layers/flights/ingestion.js"
} }
} }
+6 -1
View File
@@ -595,5 +595,10 @@
"src/ui/visualSettings.js", "src/ui/visualSettings.js",
"src/ui/panelChrome.js", "src/ui/panelChrome.js",
"src/testSupport/readShellSource.mjs", "src/testSupport/readShellSource.mjs",
"src/ui/stateOwners.test.mjs" "src/ui/stateOwners.test.mjs",
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js",
"src/layers/flights/records.test.mjs",
"src/layers/flights/ingestion.test.mjs"
] ]
+31 -3
View File
@@ -623,7 +623,10 @@
"src/layers/flights/rendering.js", "src/layers/flights/rendering.js",
"src/layers/flights/state.js", "src/layers/flights/state.js",
"src/layers/flights/testing.js", "src/layers/flights/testing.js",
"src/layers/flights/tracking.js" "src/layers/flights/tracking.js",
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1539,7 +1542,10 @@
"src/voice/realtimeController.js", "src/voice/realtimeController.js",
"src/voice/voiceCost.js", "src/voice/voiceCost.js",
"src/weatherEffectsMath.js", "src/weatherEffectsMath.js",
"src/worldFocus.js" "src/worldFocus.js",
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js"
], ],
"external": [ "external": [
"@mapbox/vector-tile", "@mapbox/vector-tile",
@@ -1863,7 +1869,10 @@
"src/services/requests.js", "src/services/requests.js",
"src/ui/panelRailGeometry.js", "src/ui/panelRailGeometry.js",
"src/ui/radioTunerModel.js", "src/ui/radioTunerModel.js",
"src/worldFocus.js" "src/worldFocus.js",
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js"
], ],
"external": [ "external": [
"cesium", "cesium",
@@ -1977,5 +1986,24 @@
"src/ui/uiLifetime.js" "src/ui/uiLifetime.js"
], ],
"external": [] "external": []
},
"civil-flight-records": {
"exports": ["./layers/flights/records"],
"modules": [
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/data/aircraftMeta.js",
"src/data/renderAltitude.js",
"src/data/aircraftClass.js"
],
"external": []
},
"civil-flight-ingestion": {
"exports": ["./layers/flights/ingestion"],
"modules": [
"src/layers/flights/ingestion.js",
"src/layers/flights/recordPolicy.js"
],
"external": []
} }
} }
+1 -1
View File
@@ -1535,7 +1535,7 @@ test('production eviction sites actually tag their clears', () => {
for (const [name, source] of [['flights', flightsSource], ['militaryFlights', militarySource]]) { for (const [name, source] of [['flights', flightsSource], ['militaryFlights', militarySource]]) {
assert.match( assert.match(
source, source,
/if\s*\(\s*icao24\s*===\s*(?:flightState\.)?_trackedIcao,?\s*\)\s*\{\s*\n\s*(?:parts\.\w+\.)?_clearTracking\(\s*false,\s*\{\s*evicted:\s*true\s*\},?\s*\);/, /if\s*\(\s*icao24\s*===\s*(?:flightState\.)?_trackedIcao,?\s*\)\s*\{\s*\n\s*(?:(?:parts\.)?tracking\.)?_clearTracking\(\s*false,\s*\{\s*evicted:\s*true\s*\},?\s*\);/,
`${name} must mark its aged-out cull as an eviction`, `${name} must mark its aged-out cull as an eviction`,
); );
assert.match( assert.match(
+1 -1
View File
@@ -254,7 +254,7 @@ test('both flight layers keep a converted contact 2D and visible (render invaria
const callSites = code.match(/aircraftIcon\(\s*[^;]*?\)/g) || []; const callSites = code.match(/aircraftIcon\(\s*[^;]*?\)/g) || [];
assert.equal(callSites.length >= 4, true, `${name}: expected the known aircraftIcon call sites`); assert.equal(callSites.length >= 4, true, `${name}: expected the known aircraftIcon call sites`);
for (const call of callSites) { for (const call of callSites) {
assert.match(call, /aircraftIcon\(\s*\s*(?:parts\.rendering\.)?_iconKind\(\s*/, assert.match(call, /aircraftIcon\(\s*\s*(?:(?:parts\.)?rendering\.)?_iconKind\(\s*/,
`${name}: ${call.replace(/\s+/g, ' ')} must resolve its sprite kind through _iconKind`); `${name}: ${call.replace(/\s+/g, ' ')} must resolve its sprite kind through _iconKind`);
} }
+2 -2
View File
@@ -8,9 +8,9 @@ export function createController({
resolveAsset, resolveAsset,
}) { }) {
function _abortActiveUpdates() { function _abortActiveUpdates() {
for (const controller of flightState._activeUpdateControllers) for (const controller of flightState.feed._activeUpdateControllers)
controller.abort(); controller.abort();
flightState._activeUpdateControllers.clear(); flightState.feed._activeUpdateControllers.clear();
} }
function _flightQuery(viewer) { function _flightQuery(viewer) {
+5 -5
View File
@@ -57,7 +57,7 @@ export function createEnrichment({
Promise.resolve() Promise.resolve()
.then(() => { .then(() => {
lifetime.signal.throwIfAborted(); lifetime.signal.throwIfAborted();
return flightState._source.getEnrichment?.(job.query, { return flightState.feed._source.getEnrichment?.(job.query, {
signal: lifetime.signal, signal: lifetime.signal,
}); });
}) })
@@ -81,7 +81,7 @@ export function createEnrichment({
`t:${icao24}`, `t:${icao24}`,
{ kind: 'type', id: icao24.toLowerCase() }, { kind: 'type', id: icao24.toLowerCase() },
(data) => { (data) => {
const meta = flightState._flightData.get(icao24); const meta = flightState.records.data.get(icao24);
if (!meta) return; // evicted while the lookup was in flight if (!meta) return; // evicted while the lookup was in flight
meta.typeCode = data.typeCode || meta.typeCode; meta.typeCode = data.typeCode || meta.typeCode;
meta.typeName = data.typeName || meta.typeName; meta.typeName = data.typeName || meta.typeName;
@@ -109,7 +109,7 @@ export function createEnrichment({
} }
function _requestRouteEnrichment(icao24) { function _requestRouteEnrichment(icao24) {
const cs = String(flightState._flightData.get(icao24)?.callsign || '') const cs = String(flightState.records.data.get(icao24)?.callsign || '')
.trim() .trim()
.toUpperCase(); .toUpperCase();
if (!/^[A-Z]{3}\d/.test(cs)) return; // airline-style callsigns only (LLL + digit); GA tails won't resolve if (!/^[A-Z]{3}\d/.test(cs)) return; // airline-style callsigns only (LLL + digit); GA tails won't resolve
@@ -117,7 +117,7 @@ export function createEnrichment({
`r:${cs}`, `r:${cs}`,
{ kind: 'route', id: cs }, { kind: 'route', id: cs },
(data) => { (data) => {
const meta = flightState._flightData.get(icao24); const meta = flightState.records.data.get(icao24);
if (!meta) return; if (!meta) return;
meta.airline = data.airline || meta.airline; meta.airline = data.airline || meta.airline;
if (data.origin && data.destination) if (data.origin && data.destination)
@@ -198,7 +198,7 @@ export function createEnrichment({
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
if (flightState._enrichSeen.has(`t:${icao24}`)) continue; // answered / queued / negative this session 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 (!/^[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 (flightState.records.data.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 if (!bb.position || !occluder.isPointVisible(bb.position)) continue; // beyond the limb
Cesium.Cartesian3.clone( Cesium.Cartesian3.clone(
bb.position, bb.position,
+7 -7
View File
@@ -50,10 +50,10 @@ export function createEvidence({
for (const bb of flightState._billboards.values()) for (const bb of flightState._billboards.values())
flightState._billboardCollection.remove(bb); flightState._billboardCollection.remove(bb);
flightState._billboards.clear(); flightState._billboards.clear();
flightState._flightData.clear(); flightState.records.data.clear();
flightState._positionHistory.clear(); flightState._positionHistory.clear();
flightState._displayCourse.clear(); flightState._displayCourse.clear();
flightState._missingPolls.clear(); flightState.records.missingPolls.clear();
flightState._focusEvidenceIds.clear(); flightState._focusEvidenceIds.clear();
for (const record of Array.isArray(records) ? records : []) { for (const record of Array.isArray(records) ? records : []) {
@@ -79,9 +79,9 @@ export function createEvidence({
lastContactEpochMs: Date.now(), lastContactEpochMs: Date.now(),
rawLat: record.latitude ?? null, rawLat: record.latitude ?? null,
rawLon: record.longitude ?? null, rawLon: record.longitude ?? null,
cullPosition: null,
}; };
flightState._flightData.set(id, meta); flightState.records.data.set(id, meta);
flightState._cullPositions.delete(id);
flightState._focusEvidenceIds.add(id); flightState._focusEvidenceIds.add(id);
const bb = flightState._billboardCollection.add({ const bb = flightState._billboardCollection.add({
position, position,
@@ -100,10 +100,10 @@ export function createEvidence({
}); });
flightState._billboards.set(id, bb); flightState._billboards.set(id, bb);
} }
flightState._count = flightState._billboards.size; flightState.feed._count = flightState._billboards.size;
flightState._lastFleetTickMs = 0; flightState._lastFleetTickMs = 0;
flightState._viewer.scene.requestRender?.(); flightState._viewer.scene.requestRender?.();
return { ok: true, count: flightState._count }; return { ok: true, count: flightState.feed._count };
} }
/** Update explicit evidence positions without rebuilding billboards. */ /** Update explicit evidence positions without rebuilding billboards. */
@@ -120,7 +120,7 @@ export function createEvidence({
const bb = flightState._billboards.get(id); const bb = flightState._billboards.get(id);
if (!position || !bb) continue; if (!position || !bb) continue;
bb.position = position; bb.position = position;
const meta = flightState._flightData.get(id); const meta = flightState.records.data.get(id);
if (meta) { if (meta) {
if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg; if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg;
if (Number.isFinite(record.velocityMps)) if (Number.isFinite(record.velocityMps))
+25 -1
View File
@@ -1,3 +1,4 @@
import { createFlightSnapshotRenderer } from './snapshotRenderer.js';
import { createFlightState } from './state.js'; import { createFlightState } from './state.js';
import { createRendering } from './rendering.js'; import { createRendering } from './rendering.js';
import { createMotion } from './motion.js'; import { createMotion } from './motion.js';
@@ -24,11 +25,34 @@ export function createCivilFlightLayer({
parts.tracking = createTracking(context); parts.tracking = createTracking(context);
parts.controller = createController(context); parts.controller = createController(context);
parts.enrichment = createEnrichment(context); parts.enrichment = createEnrichment(context);
parts.ingestion = createIngestion(context);
parts.lifecycle = createLifecycle(context); parts.lifecycle = createLifecycle(context);
parts.evidence = createEvidence(context); parts.evidence = createEvidence(context);
parts.testing = createTesting(context); parts.testing = createTesting(context);
parts.queries = createQueries(context); parts.queries = createQueries(context);
const applySnapshot = createFlightSnapshotRenderer({
flightState,
records: flightState.records,
militaryRegistry: services.militaryRegistry,
groundFloor: services.groundFloor,
meshFloor: services.meshFloor,
rendering: parts.rendering,
tracking: parts.tracking,
motion: parts.motion,
enrichment: parts.enrichment,
queries: parts.queries,
});
parts.ingestion = createIngestion({
feed: flightState.feed,
getQuery: (viewer) =>
parts.controller._flightQuery(viewer || flightState._viewer),
applySnapshot,
setSourceLabel: (source) => {
layer.source = source;
},
applyPendingTrackingRestore: () =>
parts.tracking._applyPendingTrackingRestore(),
});
Object.assign( Object.assign(
layer, layer,
parts.queries.methods, parts.queries.methods,
+64 -613
View File
@@ -1,661 +1,112 @@
import * as Cesium from 'cesium'; import { ERROR_BACKOFF_INTERVAL } from './recordPolicy.js';
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';
/** Own acquisition, cancellation, freshness and backoff independently of rendering. */
export function createIngestion({ export function createIngestion({
flightState, feed,
services, getQuery,
parts, applySnapshot,
layer, setSourceLabel,
resolveAsset, applyPendingTrackingRestore,
}) { }) {
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } =
services.militaryRegistry;
const { geoidHeight } = services.geoid;
const { cachedGroundFloor, floorAltitudeM, warmGroundFloor } =
services.groundFloor;
const { sampleMeshFloorCells } = services.meshFloor;
const flightsLayer = layer;
const methods = { 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 } = {}) { async update(viewer, { signal = null } = {}) {
const nowMs = Date.now(); const nowMs = Date.now();
const trackingRefreshEpoch = ++flightState._trackingRefreshEpoch; const trackingRefreshEpoch = ++feed._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = { feed._lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch, epoch: trackingRefreshEpoch,
status: 'source-unavailable', status: 'source-unavailable',
ids: new Set(), ids: new Set(),
source: flightState._lastSource, source: feed._lastSource,
coverage: flightState._lastCoverage, coverage: feed._lastCoverage,
}; };
if (flightState._retryAt && nowMs < flightState._retryAt) { if (feed._retryAt && nowMs < feed._retryAt) {
flightState._backoff = true; feed._backoff = true;
return; return;
} }
const resourceController = new AbortController(); const resourceController = new AbortController();
flightState._activeUpdateControllers.add(resourceController); feed._activeUpdateControllers.add(resourceController);
const updateSignal = signal const updateSignal = signal
? AbortSignal.any([signal, resourceController.signal]) ? AbortSignal.any([signal, resourceController.signal])
: resourceController.signal; : resourceController.signal;
try { try {
updateSignal.throwIfAborted(); updateSignal.throwIfAborted();
const snapshot = await flightState._source.getSnapshot( const snapshot = await feed._source.getSnapshot(getQuery(viewer), {
parts.controller._flightQuery(viewer || flightState._viewer), signal: updateSignal,
{ signal: updateSignal }, });
);
updateSignal.throwIfAborted(); updateSignal.throwIfAborted();
flightState._lastStatus = snapshot.status ?? 200; feed._lastStatus = snapshot.status ?? 200;
const usableStates = snapshot.records;
const sourceEpochMs = snapshot.observedAtMs; const sourceEpochMs = snapshot.observedAtMs;
const sourceAgeMs = snapshot.ageMs; const sourceAgeMs = snapshot.ageMs;
const sourceStale = snapshot.stale || snapshot.freshness === 'unknown'; const sourceStale = snapshot.stale || snapshot.freshness === 'unknown';
flightState._backoff = sourceStale; feed._backoff = sourceStale;
flightState._retryAt = 0; feed._retryAt = 0;
flightState._lastError = sourceStale feed._lastError = sourceStale
? sourceAgeMs == null ? sourceAgeMs == null
? 'Source snapshot time unavailable' ? 'Source snapshot time unavailable'
: `Source snapshot ${Math.max(2, Math.round(sourceAgeMs / 60_000))} min old` : `Source snapshot ${Math.max(2, Math.round(sourceAgeMs / 60_000))} min old`
: null; : null;
flightState._lastSource = snapshot.source; feed._lastSource = snapshot.source;
flightState._lastCoverage = snapshot.coverage; feed._lastCoverage = snapshot.coverage;
flightsLayer.source = flightState._lastSource; setSourceLabel(feed._lastSource);
const currentIcaos = new Set(); const accepted = applySnapshot(snapshot, viewer);
const acceptedSnapshotIcaos = new Set(); feed._count = accepted.count;
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 // Freshness belongs to the source snapshot, not the moment this browser
// received a cached 200 response. // received a cached 200 response.
flightState._lastUpdate = sourceEpochMs; feed._lastUpdate = sourceEpochMs;
flightState._lastTrackingRefreshOutcome = { feed._lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch, epoch: trackingRefreshEpoch,
status: 'accepted', status: 'accepted',
ids: acceptedSnapshotIcaos, ids: accepted.ids,
source: flightState._lastSource, source: feed._lastSource,
coverage: flightState._lastCoverage, coverage: feed._lastCoverage,
}; };
console.log(`[Data:Flights] Updated: ${flightState._count} aircraft`); console.log(`[Data:Flights] Updated: ${feed._count} aircraft`);
parts.tracking._applyPendingTrackingRestore(); applyPendingTrackingRestore();
} catch (e) { } catch (e) {
if (updateSignal.aborted || e?.name === 'AbortError') { if (updateSignal.aborted || e?.name === 'AbortError') {
throw new DOMException('Flights update aborted', 'AbortError'); throw new DOMException('Flights update aborted', 'AbortError');
} }
console.warn('[Data:Flights] Fetch error:', e); console.warn('[Data:Flights] Fetch error:', e);
flightState._backoff = true; feed._backoff = true;
flightState._retryAt = feed._retryAt =
Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL); Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
flightState._lastStatus = e?.status ?? null; feed._lastStatus = e?.status ?? null;
if (e?.source) { if (e?.source) {
flightState._lastSource = e.source; feed._lastSource = e.source;
this.source = flightState._lastSource; setSourceLabel(feed._lastSource);
} }
flightState._lastError = feed._lastError =
e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable'; e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
} finally { } finally {
flightState._activeUpdateControllers.delete(resourceController); feed._activeUpdateControllers.delete(resourceController);
} }
}, },
}; };
return { methods }; return { methods };
} }
/** Construct a fresh source lifetime and its refresh status. */
export function createFlightFeed(source) {
const feed = {};
feed._source = source;
feed._count = 0;
feed._lastUpdate = null;
feed._backoff = false;
feed._retryAt = 0;
feed._lastError = null;
feed._activeUpdateControllers = new Set();
feed._lastStatus = null;
feed._lastSource = source?.label || 'Aircraft';
feed._lastCoverage = 'worldwide upstream snapshot';
feed._trackingRefreshEpoch = 0;
feed._lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
ids: new Set(),
source: feed._lastSource,
coverage: null,
};
return feed;
}
+85
View File
@@ -0,0 +1,85 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createIngestion } from './ingestion.js';
function setup(source) {
const feed = {
_source: source,
_activeUpdateControllers: new Set(),
_trackingRefreshEpoch: 0,
_lastSource: 'Fixture',
_lastCoverage: 'local',
_retryAt: 0,
};
const accepted = [];
const labels = [];
const restores = [];
const { methods } = createIngestion({
feed,
getQuery: () => ({ latitude: 39 }),
applySnapshot: (snapshot) => {
accepted.push(snapshot);
return { count: 2, ids: new Set(['abc123']) };
},
setSourceLabel: (label) => labels.push(label),
applyPendingTrackingRestore: () => restores.push(true),
});
return { feed, accepted, labels, restores, update: methods.update };
}
test('civil acquisition rejects late completion after cancellation without publishing records', async () => {
let release;
const probe = setup({
getSnapshot: () =>
new Promise((resolve) => {
release = resolve;
}),
});
const request = probe.update(null);
assert.equal(probe.feed._activeUpdateControllers.size, 1);
for (const controller of probe.feed._activeUpdateControllers)
controller.abort();
release({ records: [] });
await assert.rejects(request, { name: 'AbortError' });
assert.equal(probe.accepted.length, 0);
assert.equal(probe.restores.length, 0);
assert.equal(probe.feed._activeUpdateControllers.size, 0);
});
test('civil acquisition publishes source time and uses a replaced source on the next request', async () => {
let calls = 0;
const probe = setup({
async getSnapshot(query) {
assert.equal(query.latitude, 39);
calls += 1;
return {
records: [],
source: 'First',
observedAtMs: 1234,
freshness: 'fresh',
coverage: 'worldwide',
};
},
});
await probe.update(null);
assert.equal(probe.feed._lastUpdate, 1234);
assert.equal(probe.feed._count, 2);
assert.deepEqual([...probe.feed._lastTrackingRefreshOutcome.ids], ['abc123']);
probe.feed._source = {
async getSnapshot() {
return {
records: [],
source: 'Second',
observedAtMs: 1250,
freshness: 'unknown',
};
},
};
await probe.update(null);
assert.equal(calls, 1);
assert.equal(probe.feed._lastUpdate, 1250);
assert.equal(probe.feed._backoff, true);
assert.equal(probe.feed._lastError, 'Source snapshot time unavailable');
assert.deepEqual(probe.labels, ['First', 'Second']);
assert.equal(probe.restores.length, 2);
});
+28 -24
View File
@@ -26,9 +26,10 @@ export function createLifecycle({
throw new Error('Configure the source before layer initialization'); throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') if (typeof source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
flightState._source = source; flightState.feed._source = source;
flightState._lastSource = source.label || flightState._lastSource; flightState.feed._lastSource =
this.source = flightState._lastSource; source.label || flightState.feed._lastSource;
this.source = flightState.feed._lastSource;
}, },
/** /**
@@ -40,7 +41,7 @@ export function createLifecycle({
init(viewer) { init(viewer) {
if (flightState._viewer) if (flightState._viewer)
throw new Error('Flight layer is already initialized'); throw new Error('Flight layer is already initialized');
if (typeof flightState._source?.getSnapshot !== 'function') if (typeof flightState.feed._source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted) if (flightState.lifetime.signal.aborted)
flightState.lifetime = new AbortController(); flightState.lifetime = new AbortController();
@@ -78,19 +79,21 @@ export function createLifecycle({
}); });
} }
flightState._billboards = new Map(); flightState._billboards = new Map();
flightState._cullPositions.clear();
flightState._detectionObjects = new Map(); flightState._detectionObjects = new Map();
flightState._flightData = new Map(); flightState.records.data = new Map();
flightState._positionHistory = new Map(); flightState._positionHistory = new Map();
flightState._displayCourse.clear(); flightState._displayCourse.clear();
flightState._groundSnap.clear(); flightState._groundSnap.clear();
flightState._count = 0; flightState.feed._count = 0;
flightState._lastUpdate = null; flightState.feed._lastUpdate = null;
flightState._backoff = false; flightState.feed._backoff = false;
flightState._retryAt = 0; flightState.feed._retryAt = 0;
flightState._lastError = null; flightState.feed._lastError = null;
flightState._lastStatus = null; flightState.feed._lastStatus = null;
flightState._lastSource = flightState._source.label || 'Aircraft'; flightState.feed._lastSource =
flightState._lastCoverage = 'worldwide upstream snapshot'; flightState.feed._source.label || 'Aircraft';
flightState.feed._lastCoverage = 'worldwide upstream snapshot';
flightState._trackedIcao = null; flightState._trackedIcao = null;
parts.tracking._resetTrackedSelectionState(); parts.tracking._resetTrackedSelectionState();
flightState._trackedEntity = null; flightState._trackedEntity = null;
@@ -142,11 +145,11 @@ export function createLifecycle({
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop // Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
// only reads geoidHeight() synchronously after this resolves (guarded by // only reads geoidHeight() synchronously after this resolves (guarded by
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick. // _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
if (!flightState._geoidReady) { if (!flightState.records.geoidReady) {
const lifetime = flightState.lifetime; const lifetime = flightState.lifetime;
ensureGeoidReady() ensureGeoidReady()
.then(() => { .then(() => {
if (!lifetime.signal.aborted) flightState._geoidReady = true; if (!lifetime.signal.aborted) flightState.records.geoidReady = true;
}) })
.catch(() => { .catch(() => {
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */ /* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
@@ -300,8 +303,9 @@ export function createLifecycle({
} }
flightState._planeModelLoaded = false; flightState._planeModelLoaded = false;
flightState._billboards.clear(); flightState._billboards.clear();
flightState._cullPositions.clear();
flightState._detectionObjects.clear(); flightState._detectionObjects.clear();
flightState._flightData.clear(); flightState.records.data.clear();
flightState._positionHistory.clear(); flightState._positionHistory.clear();
flightState._displayCourse.clear(); flightState._displayCourse.clear();
flightState._groundSnap.clear(); flightState._groundSnap.clear();
@@ -312,20 +316,20 @@ export function createLifecycle({
clearTimeout(flightState._enrichDripTimer); clearTimeout(flightState._enrichDripTimer);
flightState._enrichDripTimer = null; flightState._enrichDripTimer = null;
} }
flightState._missingPolls.clear(); flightState.records.missingPolls.clear();
flightState._focusEvidenceIds.clear(); flightState._focusEvidenceIds.clear();
flightState._count = 0; flightState.feed._count = 0;
flightState._lastUpdate = null; flightState.feed._lastUpdate = null;
flightState._cockpitContactMode = false; flightState._cockpitContactMode = false;
flightState._cockpitNearContacts = new Set(); flightState._cockpitNearContacts = new Set();
flightState._cockpitSubjectId = null; flightState._cockpitSubjectId = null;
flightState._trackingRefreshEpoch += 1; flightState.feed._trackingRefreshEpoch += 1;
flightState._lastTrackingRefreshOutcome = { flightState.feed._lastTrackingRefreshOutcome = {
epoch: flightState._trackingRefreshEpoch, epoch: flightState.feed._trackingRefreshEpoch,
status: 'destroyed', status: 'destroyed',
ids: new Set(), ids: new Set(),
source: flightState._lastSource, source: flightState.feed._lastSource,
coverage: flightState._lastCoverage, coverage: flightState.feed._lastCoverage,
}; };
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
flightState._viewer = null; flightState._viewer = null;
+11 -11
View File
@@ -81,7 +81,7 @@ export function createMotion({
*/ */
function _refloorStaleGroundedContacts(currentIcaos) { function _refloorStaleGroundedContacts(currentIcaos) {
for (const [icao24, info] of flightState._flightData) { for (const [icao24, info] of flightState.records.data) {
if (!info?.onGround || currentIcaos.has(icao24)) continue; if (!info?.onGround || currentIcaos.has(icao24)) continue;
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon)) if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon))
continue; continue;
@@ -94,7 +94,7 @@ export function createMotion({
) )
continue; continue;
info.renderAltitudeM = lifted; info.renderAltitudeM = lifted;
info.cullPosition = null; // above the ellipsoid now (or floors say otherwise next poll) flightState._cullPositions.delete(icao24); // above the ellipsoid now (or floors say otherwise next poll)
const position = Cesium.Cartesian3.fromDegrees( const position = Cesium.Cartesian3.fromDegrees(
info.rawLon, info.rawLon,
info.rawLat, info.rawLat,
@@ -125,7 +125,7 @@ export function createMotion({
*/ */
function _deadReckon(icao24, result) { function _deadReckon(icao24, result) {
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) { if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) {
const position = flightState._billboards.get(icao24)?.position; const position = flightState._billboards.get(icao24)?.position;
flightState._drCourseDeg = info?.true_track || 0; flightState._drCourseDeg = info?.true_track || 0;
@@ -323,7 +323,7 @@ export function createMotion({
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position); const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
if (parts.rendering._modelOwnsVisual(icao24)) { if (parts.rendering._modelOwnsVisual(icao24)) {
const spec = parts.rendering._modelSpec( const spec = parts.rendering._modelSpec(
flightState._flightData.get(icao24)?.klass, flightState.records.data.get(icao24)?.klass,
); );
const scale = trackedModelScaleForPixelCap({ const scale = trackedModelScaleForPixelCap({
baseScale: spec.scale, baseScale: spec.scale,
@@ -349,7 +349,7 @@ export function createMotion({
const height = billboard?.height?.getValue(time) ?? 28; const height = billboard?.height?.getValue(time) ?? 28;
const scale = const scale =
billboard?.scale?.getValue(time) ?? billboard?.scale?.getValue(time) ??
(CLASS_SCALE_2D[flightState._flightData.get(icao24)?.klass] || 1); (CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1);
const scaleByDistance = const scaleByDistance =
billboard?.scaleByDistance?.getValue(time) ?? billboard?.scaleByDistance?.getValue(time) ??
TRACKED_BILLBOARD_SCALE_BY_DISTANCE; TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
@@ -392,7 +392,7 @@ export function createMotion({
} }
const nowMs = Date.now(); const nowMs = Date.now();
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (sameTrack) { if (sameTrack) {
const dtSec = Math.max(0.001, (nowMs - flightState._drPrevMs) / 1000); const dtSec = Math.max(0.001, (nowMs - flightState._drPrevMs) / 1000);
const speed = (info && info.velocity) || 0; const speed = (info && info.velocity) || 0;
@@ -499,7 +499,7 @@ export function createMotion({
parts.rendering._modelOwnsVisual(flightState._trackedIcao) parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) { ) {
const spec = parts.rendering._modelSpec( const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
return modelVisualAnchor( return modelVisualAnchor(
flightState._trackedModel.modelMatrix, flightState._trackedModel.modelMatrix,
@@ -544,7 +544,7 @@ export function createMotion({
) )
return 0; return 0;
const spec = parts.rendering._modelSpec( const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
const scale = Number.isFinite(flightState._trackedModel.computedScale) const scale = Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale ? flightState._trackedModel.computedScale
@@ -558,7 +558,7 @@ export function createMotion({
parts.rendering._modelOwnsVisual(flightState._trackedIcao) parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) { ) {
const spec = parts.rendering._modelSpec( const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled // Through the model's OWN render chain (modelVisualAnchor's hand-rolled
// half-correction put this offset on the lateral axis — see // half-correction put this offset on the lateral axis — see
@@ -584,7 +584,7 @@ export function createMotion({
* the same aircraft can never disagree across the handoff. */ * the same aircraft can never disagree across the handoff. */
function _trackedDisplayCourse() { function _trackedDisplayCourse() {
const info = flightState._flightData.get(flightState._trackedIcao); const info = flightState.records.data.get(flightState._trackedIcao);
const fallback = (info && info.true_track) || 0; const fallback = (info && info.true_track) || 0;
const cacheValid = const cacheValid =
flightState._drReconcileValid && flightState._drReconcileValid &&
@@ -1018,7 +1018,7 @@ export function createMotion({
} }
const candidates = []; const candidates = [];
for (const [icao24, info] of flightState._flightData) { for (const [icao24, info] of flightState.records.data) {
if (!info?.onGround) continue; if (!info?.onGround) continue;
// T7: a contact whose 3D model is the visual never reads a display floor. // T7: a contact whose 3D model is the visual never reads a display floor.
if (parts.rendering._modelOwnsVisual(icao24)) continue; if (parts.rendering._modelOwnsVisual(icao24)) continue;
+9 -2
View File
@@ -56,18 +56,25 @@ test('flight state owns distinct mutable records, scratch objects and floor samp
const first = createFlightState({ services: services() }); const first = createFlightState({ services: services() });
const second = createFlightState({ services: services() }); const second = createFlightState({ services: services() });
for (const key of [ for (const key of [
'_flightData', 'records',
'feed',
'_billboards', '_billboards',
'_positionHistory', '_positionHistory',
'_displayFloorState', '_displayFloorState',
'_groundSnap', '_groundSnap',
'_scratchCarto', '_scratchCarto',
'_models', '_models',
'_activeUpdateControllers',
'lifetime', 'lifetime',
]) { ]) {
assert.notEqual(first[key], second[key], key); assert.notEqual(first[key], second[key], key);
} }
assert.notEqual(first.records.data, second.records.data);
assert.notEqual(first.records.missingPolls, second.records.missingPolls);
assert.notEqual(first.records.geoidNCache, second.records.geoidNCache);
assert.notEqual(
first.feed._activeUpdateControllers,
second.feed._activeUpdateControllers,
);
}); });
test('aborted enrichment cannot write into a later lifecycle or decrement its active count', async () => { test('aborted enrichment cannot write into a later lifecycle or decrement its active count', async () => {
+8 -16
View File
@@ -1,3 +1,11 @@
export {
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 './recordPolicy.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
export const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true; export const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
@@ -101,14 +109,6 @@ export const MODEL_BELLY_OFFSET_NATIVE = 6.719;
export const CYAN_TRANSPARENT = Cesium.Color.CYAN.withAlpha(0); 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_CONTACT_SIZE_PX = 6;
export const COCKPIT_CIVILIAN_COLOR = export const COCKPIT_CIVILIAN_COLOR =
@@ -151,8 +151,6 @@ export const RENDER_DELAY_SEC = 30;
/** @constant {number} Polls an aircraft may miss before removal (transient OpenSky dropouts). */ /** @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 // --- 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 // 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 // the actual landing, so a landed plane's last airborne-classified fixes show
@@ -174,8 +172,6 @@ export const LANDED_SPEED_MAX_MPS = 23;
/** @constant {number} Missed-poll allowance for likely-landed planes (1 = removed on the first missed poll). */ /** @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 // 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 // contacts rendering below the alt ceiling are ever clamped/warmed (terrain
// tops out well under it outside the extreme Himalaya; cruise traffic can't // tops out well under it outside the extreme Himalaya; cruise traffic can't
@@ -184,12 +180,8 @@ export const LANDED_MISSING_POLL_LIMIT = 1;
// need unbounded terrain resolution (the tracked contact clamps regardless). // need unbounded terrain resolution (the tracked contact clamps regardless).
/** @constant {number} Max render altitude (m, ellipsoidal) eligible for the ground-floor clamp. */ /** @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. */ /** @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 // Icon orientation (2026-06-10 playtest fix): rotation is computed by
// projecting each aircraft's course vector into WINDOW coordinates // projecting each aircraft's course vector into WINDOW coordinates
+26 -28
View File
@@ -1,3 +1,4 @@
import { approxDistanceKm as _approxDistanceKm } from './recordPolicy.js';
import { displayedKinematics } from '../../data/motionModel.js'; import { displayedKinematics } from '../../data/motionModel.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { isExplicitLayerStateOrigin } from '../../data/layerState.js'; import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
@@ -43,13 +44,6 @@ export function createQueries({
* @returns {number} Approximate great-circle distance in km. * @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" * 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 * (low + slow) — see the landed fast-cull rationale above. Both gates must
@@ -59,7 +53,7 @@ export function createQueries({
*/ */
function _likelyLanded(icao24) { function _likelyLanded(icao24) {
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (!info) return false; if (!info) return false;
// Round 7 (owner: "fewer planes than OpenSky's own map" + "parked planes // Round 7 (owner: "fewer planes than OpenSky's own map" + "parked planes
// never heal"): the fast cull only applies to contacts that were AIRBORNE // never heal"): the fast cull only applies to contacts that were AIRBORNE
@@ -136,7 +130,7 @@ export function createQueries({
// user-visible surface renders `position` as the plane's on-screen location — // user-visible surface renders `position` as the plane's on-screen location —
// every visual consumer reads the floored per-frame cache instead (see // every visual consumer reads the floored per-frame cache instead (see
// `_trackedDisplayPosition`). If that ever changes, floor this path too. // `_trackedDisplayPosition`). If that ever changes, floor this path too.
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const basePos = const basePos =
parts.motion._deadReckon(icao24) || (bb ? bb.position : null); parts.motion._deadReckon(icao24) || (bb ? bb.position : null);
@@ -170,7 +164,8 @@ export function createQueries({
velocityMps: displayed.speedMps, velocityMps: displayed.speedMps,
track: displayed.trackDeg, track: displayed.trackDeg,
stale: Boolean( stale: Boolean(
flightState._missingPolls.get(icao24) || flightState._backoff, flightState.records.missingPolls.get(icao24) ||
flightState.feed._backoff,
), ),
airline: info?.airline ?? null, airline: info?.airline ?? null,
// CLASS label follows the TR-3B conversion so every downstream card // CLASS label follows the TR-3B conversion so every downstream card
@@ -271,7 +266,7 @@ export function createQueries({
icon: '✈️', icon: '✈️',
source: flightState._lastSource, source: flightState.feed._lastSource,
// Browser-harness seam: isolates synthetic display-floor scenarios without // Browser-harness seam: isolates synthetic display-floor scenarios without
// changing any production lifecycle or cache policy. // changing any production lifecycle or cache policy.
@@ -429,7 +424,7 @@ export function createQueries({
const model = flightState._models.get(icao24); const model = flightState._models.get(icao24);
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24); const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24);
if (!isTracked && !bb.show && !modelOwnsVisual) continue; if (!isTracked && !bb.show && !modelOwnsVisual) continue;
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
let object = flightState._detectionObjects.get(icao24); let object = flightState._detectionObjects.get(icao24);
if (!object) { if (!object) {
object = { object = {
@@ -496,7 +491,7 @@ export function createQueries({
* Best match with a cloned, dead-reckoned position, or null if none. * Best match with a cloned, dead-reckoned position, or null if none.
*/ */
findByQuery(query) { findByQuery(query) {
if (!flightState._flightData || flightState._flightData.size === 0) if (!flightState.records.data || flightState.records.data.size === 0)
return null; return null;
const q = String(query || '') const q = String(query || '')
.trim() .trim()
@@ -513,7 +508,7 @@ export function createQueries({
// strictly tiered so a registration can never out-rank a real callsign on // strictly tiered so a registration can never out-rank a real callsign on
// feed order alone. // feed order alone.
let best = null; let best = null;
for (const [icao24, info] of flightState._flightData) { for (const [icao24, info] of flightState.records.data) {
const candidate = { const candidate = {
tier: rankContactMatch({ tier: rankContactMatch({
query: q, query: q,
@@ -580,7 +575,7 @@ export function createQueries({
const distance = Cesium.Cartesian3.distance(center, pos); const distance = Cesium.Cartesian3.distance(center, pos);
if (distance > maxRange) continue; if (distance > maxRange) continue;
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
const callsign = info?.callsign?.trim() || null; const callsign = info?.callsign?.trim() || null;
nearby.push({ nearby.push({
// Label. Callers that need identity read `icao24` (Context cohorts do). // Label. Callers that need identity read `icao24` (Context cohorts do).
@@ -664,7 +659,7 @@ export function createQueries({
flightState._scratchCarto, flightState._scratchCarto,
); );
if (!carto) continue; if (!carto) continue;
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
result.push({ result.push({
id: icao24, // identity (trackById resolves this) id: icao24, // identity (trackById resolves this)
label: _contactLabel(icao24, info), label: _contactLabel(icao24, info),
@@ -703,14 +698,14 @@ export function createQueries({
if ( if (
!flightState._billboardCollection || !flightState._billboardCollection ||
!flightState._billboardCollection.show || !flightState._billboardCollection.show ||
flightState._flightData.size === 0 flightState.records.data.size === 0
) )
return []; return [];
const limit = Number.isFinite(maxCount) const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount)) ? Math.max(1, Math.floor(maxCount))
: 2000; : 2000;
const result = []; const result = [];
for (const [icao24, info] of flightState._flightData) { for (const [icao24, info] of flightState.records.data) {
const routeOk = const routeOk =
!!info?.route && parts.tracking._routeIsPlausible(icao24, info.route); !!info?.route && parts.tracking._routeIsPlausible(icao24, info.route);
result.push( result.push(
@@ -754,7 +749,7 @@ export function createQueries({
}; };
const id = _normalizeTrackedIcao(icao24); const id = _normalizeTrackedIcao(icao24);
if (!id) return { status: 'missing', reason: 'invalid-target' }; if (!id) return { status: 'missing', reason: 'invalid-target' };
const outcome = flightState._lastTrackingRefreshOutcome; const outcome = flightState.feed._lastTrackingRefreshOutcome;
if (outcome.status !== 'accepted') { if (outcome.status !== 'accepted') {
return { return {
status: 'source-unavailable', status: 'source-unavailable',
@@ -925,18 +920,21 @@ export function createQueries({
* @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}} * @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}}
*/ */
getStats() { getStats() {
const retryInSec = flightState._retryAt const retryInSec = flightState.feed._retryAt
? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000)) ? Math.max(
0,
Math.ceil((flightState.feed._retryAt - Date.now()) / 1000),
)
: 0; : 0;
return { return {
count: flightState._count, count: flightState.feed._count,
lastUpdate: flightState._lastUpdate, lastUpdate: flightState.feed._lastUpdate,
stale: flightState._backoff, stale: flightState.feed._backoff,
error: flightState._lastError, error: flightState.feed._lastError,
status: flightState._lastStatus, status: flightState.feed._lastStatus,
retryInSec, retryInSec,
source: flightState._lastSource, source: flightState.feed._lastSource,
coverage: flightState._lastCoverage, coverage: flightState.feed._lastCoverage,
}; };
}, },
}; };
+21
View File
@@ -0,0 +1,21 @@
export const GROUND_FLOOR_CLAMP_RADIUS_KM = 150;
export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500;
/** Maximum feed fixes retained for dead reckoning. */
export const POSITION_HISTORY_LIMIT = 5;
export const LANDED_MISSING_POLL_LIMIT = 1;
export const MISSING_POLL_LIMIT = 3;
/** Cooldown in milliseconds after a transient source error. */
export const ERROR_BACKOFF_INTERVAL = 20000;
/** Local equirectangular distance used for viewer-proximate floor sampling. */
export 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);
}
+283
View File
@@ -0,0 +1,283 @@
import { stickyNumber, stickyText } from '../../data/aircraftMeta.js';
import {
geoidSurfaceLastResortM,
pickRenderAltitudeM,
} from '../../data/renderAltitude.js';
import { classifyAircraft } from '../../data/aircraftClass.js';
import {
approxDistanceKm,
GROUND_FLOOR_CLAMP_RADIUS_KM,
GROUND_FLOOR_WARM_MAX_ALT_M,
LANDED_MISSING_POLL_LIMIT,
MISSING_POLL_LIMIT,
} from './recordPolicy.js';
/** Source-independent aircraft metadata and bounded missing-poll retention. */
export class FlightRecords {
constructor({ geoidHeight, cachedGroundFloor, floorAltitudeM }) {
this.services = {
geoidHeight,
cachedGroundFloor,
floorAltitudeM,
};
this.data = new Map();
this.missingPolls = new Map();
this.geoidNCache = new Map();
this.geoidReady = false;
}
receive(
observation,
{ viewerLatDeg, viewerLonDeg, trackedId, floorWarmPoints },
) {
const { geoidHeight, cachedGroundFloor, floorAltitudeM } = this.services;
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;
const onGround = on_ground === true;
this.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 = this.data.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 = this.geoidNCache.get(icao24);
if (geoidN === undefined && this.geoidReady) {
geoidN = geoidHeight(lat, lon);
this.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 &&
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 === trackedId ||
(viewerLatDeg != null &&
approxDistanceKm(viewerLatDeg, viewerLonDeg, lat, lon) <=
GROUND_FLOOR_CLAMP_RADIUS_KM))
) {
renderAltitudeM = floorAltitudeM(
renderAltitudeM,
cachedGroundFloor(lat, lon),
);
floorWarmPoints.push({ lat, lon });
}
// Landing/takeoff transition: the on_ground flip restyles IN PLACE.
const groundFlipped =
!!prevMeta && (prevMeta.onGround === true) !== onGround;
// 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,
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,
};
this.data.set(icao24, meta);
const fixEpochMs =
Number.isFinite(observation.positionTimeMs) &&
observation.positionTimeMs > 0
? observation.positionTimeMs
: Date.now();
return { icao24, prevMeta, meta, groundFlipped, fixEpochMs };
}
absence(id, { complete, likelyLanded }) {
// Partial admissions do not prove absence, but retention remains bounded.
if (
!complete &&
Date.now() - (this.data.get(id)?.observedReceiptMs ?? 0) < 300000
)
return 'retain';
const misses = (this.missingPolls.get(id) || 0) + 1;
const limit = likelyLanded ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
if (misses < limit) {
this.missingPolls.set(id, misses);
return 'stale';
}
this.missingPolls.delete(id);
return 'remove';
}
forget(id) {
this.data.delete(id);
this.missingPolls.delete(id);
this.geoidNCache.delete(id);
}
}
+108
View File
@@ -0,0 +1,108 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { FlightRecords } from './records.js';
function records() {
return new FlightRecords({
geoidHeight: () => 40,
cachedGroundFloor: () => null,
floorAltitudeM: (altitude) => altitude,
approxDistanceKm: () => 0,
});
}
const view = () => ({
viewerLatDeg: null,
viewerLonDeg: null,
trackedId: null,
floorWarmPoints: [],
});
const observation = (fields = {}) => ({
id: 'abc123',
reference: 'abc123',
longitude: -77,
latitude: 39,
callsign: 'TEST1',
baroAltitudeM: 1000,
ellipsoidAltitudeM: null,
onGround: false,
speedMps: 120,
courseDeg: 90,
positionTimeMs: 1700000000000,
...fields,
});
test('civil records keep aviation units separate from render height and survive missing fields', () => {
const store = records();
store.geoidReady = true;
const first = store.receive(observation(), view());
assert.equal(first.meta.altitude, 1000);
assert.equal(first.meta.renderAltitudeM, 1040);
assert.equal(first.fixEpochMs, 1700000000000);
first.meta.typeCode = 'B738';
const second = store.receive(
observation({
callsign: '',
baroAltitudeM: null,
speedMps: null,
courseDeg: null,
}),
view(),
);
assert.equal(second.prevMeta, first.meta);
assert.equal(second.meta.callsign, 'TEST1');
assert.equal(second.meta.altitude, 1000);
assert.equal(second.meta.renderAltitudeM, 1040);
assert.equal(second.meta.velocity, 120);
assert.equal(second.meta.true_track, 90);
assert.equal(second.meta.typeCode, 'B738');
assert.equal(Object.hasOwn(second.meta, 'cullPosition'), false);
assert.deepEqual(JSON.parse(JSON.stringify(second.meta)), second.meta);
});
test('civil record owners isolate state and preserve partial, missing-poll and landed retention', () => {
const store = records();
const other = records();
const { meta } = store.receive(observation(), view());
assert.equal(other.data.size, 0);
assert.equal(
store.absence('abc123', { complete: false, likelyLanded: false }),
'retain',
);
assert.equal(store.missingPolls.size, 0);
meta.observedReceiptMs = Date.now() - 300001;
assert.equal(
store.absence('abc123', { complete: false, likelyLanded: false }),
'stale',
);
assert.equal(
store.absence('abc123', { complete: true, likelyLanded: false }),
'stale',
);
assert.equal(
store.absence('abc123', { complete: true, likelyLanded: false }),
'remove',
);
// Admission decides removal; follow teardown can still read the record first.
assert.equal(store.data.get('abc123'), meta);
store.forget('abc123');
assert.equal(store.data.size, 0);
assert.equal(store.missingPolls.size, 0);
assert.equal(store.geoidNCache.size, 0);
store.receive(observation(), view());
assert.equal(
store.absence('abc123', { complete: true, likelyLanded: true }),
'remove',
);
});
test('ground transitions preserve last render height while a surface cell is cold', () => {
const store = records();
store.receive(observation({ ellipsoidAltitudeM: 1040 }), view());
const next = store.receive(
observation({ onGround: true, baroAltitudeM: null }),
view(),
);
assert.equal(next.groundFlipped, true);
assert.equal(next.meta.renderAltitudeM, 1040);
assert.equal(next.meta.wasAirborne, true);
});
+15 -15
View File
@@ -77,7 +77,7 @@ export function createRendering({
function _fleetBillboardScale(icao24, klass) { function _fleetBillboardScale(icao24, klass) {
return ( return (
(CLASS_SCALE_2D[klass] || 1) * (CLASS_SCALE_2D[klass] || 1) *
(flightState._flightData.get(icao24)?.onGround ? GROUND_SCALE : 1) (flightState.records.data.get(icao24)?.onGround ? GROUND_SCALE : 1)
); );
} }
@@ -171,7 +171,7 @@ export function createRendering({
return; return;
} }
const meta = flightState._flightData.get(icao24); const meta = flightState.records.data.get(icao24);
bb.image = aircraftIcon( bb.image = aircraftIcon(
_iconKind(icao24, meta?.klass), _iconKind(icao24, meta?.klass),
bb._gevIconLarge ? TRACKED_ICON_PX : undefined, bb._gevIconLarge ? TRACKED_ICON_PX : undefined,
@@ -303,7 +303,7 @@ export function createRendering({
* because a resample is mid-backoff. */ * because a resample is mid-backoff. */
function _modelDisplayPosition(icao24, pos, result) { function _modelDisplayPosition(icao24, pos, result) {
const meta = flightState._flightData.get(icao24); const meta = flightState.records.data.get(icao24);
if (!meta || !meta.onGround) return pos; if (!meta || !meta.onGround) return pos;
const h = flightState._groundSnap.heightFor( const h = flightState._groundSnap.heightFor(
flightState._viewer, flightState._viewer,
@@ -438,7 +438,7 @@ export function createRendering({
* the same rule (its billboard entity is always the fallback visual). */ * the same rule (its billboard entity is always the fallback visual). */
function _syncModelToClass(icao24) { function _syncModelToClass(icao24) {
const key = _specKeyFor(flightState._flightData.get(icao24)?.klass); const key = _specKeyFor(flightState.records.data.get(icao24)?.klass);
const current = flightState._models.get(icao24); const current = flightState._models.get(icao24);
if ( if (
(current && current._gevSpecKey !== key) || (current && current._gevSpecKey !== key) ||
@@ -510,10 +510,10 @@ export function createRendering({
// aircraft mid-load, the post-await admission below rejects the stale asset. // 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 // Boost state likewise: the creation options bake it in, so a mid-load
// toggle must reject too (the reload queue only covers ADMITTED models). // toggle must reject too (the reload queue only covers ADMITTED models).
const specKey = _specKeyFor(flightState._flightData.get(icao24)?.klass); const specKey = _specKeyFor(flightState.records.data.get(icao24)?.klass);
const loadIrBoost = flightState._irBoost; const loadIrBoost = flightState._irBoost;
try { try {
const spec = _modelSpec(flightState._flightData.get(icao24)?.klass); const spec = _modelSpec(flightState.records.data.get(icao24)?.klass);
model = await Cesium.Model.fromGltfAsync({ model = await Cesium.Model.fromGltfAsync({
url: resolveAsset(spec.url), url: resolveAsset(spec.url),
asynchronous: false, asynchronous: false,
@@ -559,12 +559,12 @@ export function createRendering({
!_modelRegimeActive() || !_modelRegimeActive() ||
!flightState._modelCollection || !flightState._modelCollection ||
flightState._modelCollection.isDestroyed() || flightState._modelCollection.isDestroyed() ||
!flightState._flightData.has(icao24) || !flightState.records.data.has(icao24) ||
icao24 === flightState._trackedIcao || icao24 === flightState._trackedIcao ||
flightState._models.has(icao24) || flightState._models.has(icao24) ||
flightState._models.size >= _modelCap() || flightState._models.size >= _modelCap() ||
// Class reclassified mid-load → this GLB/scale is for the OLD class. // Class reclassified mid-load → this GLB/scale is for the OLD class.
_specKeyFor(flightState._flightData.get(icao24)?.klass) !== specKey || _specKeyFor(flightState.records.data.get(icao24)?.klass) !== specKey ||
// IR boost flipped mid-load → this model baked the wrong shader/tint. // IR boost flipped mid-load → this model baked the wrong shader/tint.
flightState._irBoost !== loadIrBoost; flightState._irBoost !== loadIrBoost;
if (stale) { if (stale) {
@@ -714,10 +714,10 @@ export function createRendering({
flightState._trackedModelLoading = true; flightState._trackedModelLoading = true;
const gen = flightState._trackedModelGen; const gen = flightState._trackedModelGen;
const trackedSpec = _modelSpec( const trackedSpec = _modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
const trackedKey = _specKeyFor( const trackedKey = _specKeyFor(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
const trackedIrBoost = flightState._irBoost; const trackedIrBoost = flightState._irBoost;
Cesium.Model.fromGltfAsync({ Cesium.Model.fromGltfAsync({
@@ -756,7 +756,7 @@ export function createRendering({
// _trackedModel was still null): drop the stale asset; driver reloads. // _trackedModel was still null): drop the stale asset; driver reloads.
if ( if (
_specKeyFor( _specKeyFor(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
) !== trackedKey || ) !== trackedKey ||
flightState._irBoost !== trackedIrBoost flightState._irBoost !== trackedIrBoost
) { ) {
@@ -829,7 +829,7 @@ export function createRendering({
); );
if (!flightState._trackedModel.ready) return; if (!flightState._trackedModel.ready) return;
const spec = _modelSpec( const spec = _modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
flightState._trackedModel.scale = trackedModelScaleForPixelCap({ flightState._trackedModel.scale = trackedModelScaleForPixelCap({
baseScale: spec.scale, baseScale: spec.scale,
@@ -989,7 +989,7 @@ export function createRendering({
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
const dr = parts.motion._deadReckon(icao24, flightState._scratchFleetPos); const dr = parts.motion._deadReckon(icao24, flightState._scratchFleetPos);
// The dead-reckoned point drifts away from the fix whose cell supplied the // The dead-reckoned point drifts away from the fix whose cell supplied the
@@ -1018,7 +1018,7 @@ export function createRendering({
// pass would hide a plane that is really just low over high-N terrain // 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). // waiting for its floor to warm (ATL grounded contacts at geoid −31 m).
const beyondHorizon = !occluder.isPointVisible( const beyondHorizon = !occluder.isPointVisible(
info?.cullPosition || bb.position, flightState._cullPositions.get(icao24) || bb.position,
); );
// A billboard flipping INTO view (horizon reveal while the camera idles) // A billboard flipping INTO view (horizon reveal while the camera idles)
// gets its rotation refreshed THIS tick even without a pose change — // gets its rotation refreshed THIS tick even without a pose change —
@@ -1074,7 +1074,7 @@ export function createRendering({
flightState._cockpitContactMode && !isCockpitNear flightState._cockpitContactMode && !isCockpitNear
? 1 ? 1
: _fleetBillboardScale(icao24, info?.klass), : _fleetBillboardScale(icao24, info?.klass),
baseAlpha: flightState._missingPolls.get(icao24) ? 0.45 : 1, baseAlpha: flightState.records.missingPolls.get(icao24) ? 0.45 : 1,
baseColor, baseColor,
focusFactor: focus.factor, focusFactor: focus.factor,
cameraDistanceM, cameraDistanceM,
+313
View File
@@ -0,0 +1,313 @@
import * as Cesium from 'cesium';
import {
turnRateFromFixHistory,
liftRepeatedGroundFix,
synthesizeForwardKinematicsFix,
} from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import { POSITION_HISTORY_LIMIT } from './recordPolicy.js';
/** Apply reconciled aircraft records to Cesium resources and follow state. */
export function createFlightSnapshotRenderer({
flightState,
records,
militaryRegistry,
groundFloor,
meshFloor,
rendering,
tracking,
motion,
enrichment,
queries,
}) {
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } = militaryRegistry;
const { warmGroundFloor } = groundFloor;
const { sampleMeshFloorCells } = meshFloor;
return function applySnapshot(snapshot, viewer) {
const currentIcaos = new Set();
const acceptedSnapshotIcaos = new Set();
// 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 snapshot.records) {
const icao24 = observation.id;
acceptedSnapshotIcaos.add(icao24);
// 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 && tracking._militaryLayerSuppresses(icao24)) {
const dupe = flightState._billboards.get(icao24);
if (dupe) {
flightState._billboardCollection.remove(dupe);
flightState._billboards.delete(icao24);
rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
records.forget(icao24);
flightState._cullPositions.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
}
continue;
}
currentIcaos.add(icao24);
const { prevMeta, meta, groundFlipped, fixEpochMs } = records.receive(
observation,
{
viewerLatDeg,
viewerLonDeg,
trackedId: flightState._trackedIcao,
floorWarmPoints,
},
);
const position = Cesium.Cartesian3.fromDegrees(
meta.rawLon,
meta.rawLat,
meta.renderAltitudeM,
);
// Either flip retires the previous one-shot ground sample.
if (groundFlipped) flightState._groundSnap.forget(icao24);
flightState._cullPositions.set(
icao24,
meta.renderAltitudeM < 10
? Cesium.Cartesian3.fromDegrees(meta.rawLon, meta.rawLat, 12)
: null,
);
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 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) tracking._appendTrailFix(position.clone());
} else {
const modelOwnsGroundVisual = 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) 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 = rendering
._fleetBillboardColor(icao24)
.withAlpha(bb.color?.alpha ?? 1);
}
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
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) rendering._syncModelToClass(icao24);
} else {
const bb = flightState._billboardCollection.add({
position,
image: aircraftIcon(rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale: 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
: rendering._fleetBillboardColor(icao24),
sizeInMeters: false,
scaleByDistance: 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: rendering._groundDepthDistance(),
id: icao24,
show: !isTracked, // hidden if currently tracked (entity replaces it)
});
flightState._billboards.set(icao24, bb);
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)
tracking._startTrail(icao24);
// If this is the tracked aircraft, update label text
// (position updates automatically via dead-reckoning CallbackProperty)
if (isTracked && flightState._trackedEntity) {
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;
const absence = records.absence(icao24, {
complete: snapshot.complete,
likelyLanded: queries._likelyLanded(icao24),
});
if (absence === 'retain') continue;
if (absence === 'stale') {
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.
tracking._updateTrackedLabelModel(icao24);
}
continue;
}
// 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) {
tracking._clearTracking(false, { evicted: true });
}
flightState._billboardCollection.remove(bb);
flightState._billboards.delete(icao24);
rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
records.forget(icao24);
flightState._cullPositions.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(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).
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.
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).
motion._refloorStaleGroundedContacts(currentIcaos);
return { count: flightState._billboards.size, ids: acceptedSnapshotIcaos };
};
}
+8 -62
View File
@@ -1,11 +1,17 @@
import { FlightRecords } from './records.js';
import { createFlightFeed } from './ingestion.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { ENRICH_AMBIENT_BUDGET_CEIL } from './policy.js'; import { ENRICH_AMBIENT_BUDGET_CEIL } from './policy.js';
export function createFlightState({ source, services }) { export function createFlightState({ source, services }) {
const { createGroundSnap } = services.groundSnap; const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() }; const flightState = { lifetime: new AbortController() };
flightState.feed = createFlightFeed(source);
flightState._source = source; flightState.records = new FlightRecords({
...services.geoid,
...services.groundFloor,
});
flightState._cullPositions = new Map();
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs /** 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 * vertex-baked to real-world METERS in the airplane.glb axis convention, so
@@ -106,10 +112,6 @@ export function createFlightState({ source, services }) {
flightState._detectionObjects = new Map(); 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. */ /** DEV-only explicit-position contacts used by qa-focus-evidence.mjs. */
flightState._focusEvidenceIds = new Set(); flightState._focusEvidenceIds = new Set();
@@ -118,48 +120,6 @@ export function createFlightState({ source, services }) {
flightState._positionHistory = new Map(); 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 // Click-to-track state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -172,16 +132,6 @@ export function createFlightState({ source, services }) {
flightState._trackingIntentGeneration = 0; 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 */ /** @type {Cesium.Entity|null} Entity used for camera tracking */
flightState._trackedEntity = null; flightState._trackedEntity = null;
@@ -250,10 +200,6 @@ export function createFlightState({ source, services }) {
flightState._trailBackfillToken = 0; 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 */ /** @type {number} Epoch ms of the last fleet dead-reckoning pass */
flightState._lastFleetTickMs = 0; flightState._lastFleetTickMs = 0;
+9 -8
View File
@@ -69,14 +69,15 @@ export function createTesting({
flightState._modelCollection = modelCollection; flightState._modelCollection = modelCollection;
flightState._billboardCollection = billboardCollection; flightState._billboardCollection = billboardCollection;
flightState._billboards = new Map([[icao24, billboard]]); flightState._billboards = new Map([[icao24, billboard]]);
flightState._cullPositions.clear();
flightState._models.clear(); flightState._models.clear();
for (const [key, model] of models) flightState._models.set(key, model); for (const [key, model] of models) flightState._models.set(key, model);
flightState._detectionObjects = new Map(); flightState._detectionObjects = new Map();
flightState._flightData = new Map([[icao24, meta]]); flightState.records.data = new Map([[icao24, meta]]);
flightState._positionHistory = new Map([[icao24, history]]); flightState._positionHistory = new Map([[icao24, history]]);
flightState._missingPolls = new Map(); flightState.records.missingPolls = new Map();
flightState._displayCourse.clear(); flightState._displayCourse.clear();
flightState._geoidNCache.clear(); flightState.records.geoidNCache.clear();
flightState._trackedIcao = tracked ? icao24 : null; flightState._trackedIcao = tracked ? icao24 : null;
flightState._trackedEntity = tracked ? entity : null; flightState._trackedEntity = tracked ? entity : null;
flightState._trackedModel = null; flightState._trackedModel = null;
@@ -86,8 +87,8 @@ export function createTesting({
// production state owned by the tracking lifecycle (_resetTrackedSelectionState), // production state owned by the tracking lifecycle (_resetTrackedSelectionState),
// and clearing them here would mask exactly the deselect→re-track hole this // and clearing them here would mask exactly the deselect→re-track hole this
// seam is used to test. // seam is used to test.
flightState._backoff = false; flightState.feed._backoff = false;
flightState._retryAt = 0; flightState.feed._retryAt = 0;
} }
/** Seed the authoritative snapshot outcome used by share-Follow tests. */ /** Seed the authoritative snapshot outcome used by share-Follow tests. */
@@ -98,8 +99,8 @@ export function createTesting({
source = 'OpenSky Network', source = 'OpenSky Network',
coverage = 'test', coverage = 'test',
} = {}) { } = {}) {
const epoch = ++flightState._trackingRefreshEpoch; const epoch = ++flightState.feed._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = { flightState.feed._lastTrackingRefreshOutcome = {
epoch, epoch,
status, status,
ids: new Set(ids.map((id) => String(id).trim().toLowerCase())), ids: new Set(ids.map((id) => String(id).trim().toLowerCase())),
@@ -117,7 +118,7 @@ export function createTesting({
history = [], history = [],
}) { }) {
flightState._billboards.set(icao24, billboard); flightState._billboards.set(icao24, billboard);
flightState._flightData.set(icao24, meta); flightState.records.data.set(icao24, meta);
flightState._positionHistory.set(icao24, history); flightState._positionHistory.set(icao24, history);
} }
+21 -18
View File
@@ -60,7 +60,7 @@ export function createTracking({
function _publishTrackedSelection(icao24, origin = 'programmatic') { function _publishTrackedSelection(icao24, origin = 'programmatic') {
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (!bb?.position || !info) return false; if (!bb?.position || !info) return false;
if (flightState._trackedEntity) if (flightState._trackedEntity)
flightState._trackedEntity.gevSelectionOrigin = origin; flightState._trackedEntity.gevSelectionOrigin = origin;
@@ -99,10 +99,10 @@ export function createTracking({
id: icao24, id: icao24,
layerId: 'flights', layerId: 'flights',
layerName: 'Live Flights', layerName: 'Live Flights',
source: flightState._lastSource, source: flightState.feed._lastSource,
label: parts.queries._contactLabel( label: parts.queries._contactLabel(
icao24, icao24,
flightState._flightData.get(icao24), flightState.records.data.get(icao24),
), ),
latitude: described.latitude, latitude: described.latitude,
longitude: described.longitude, longitude: described.longitude,
@@ -111,7 +111,7 @@ export function createTracking({
properties: { properties: {
name: parts.queries._contactLabel( name: parts.queries._contactLabel(
icao24, icao24,
flightState._flightData.get(icao24), flightState.records.data.get(icao24),
), ),
operator: described.airline || '', operator: described.airline || '',
callsign: described.callsign || '', callsign: described.callsign || '',
@@ -477,8 +477,8 @@ export function createTracking({
async function _backfillTrail(icao24, token, oldestFixEpochSec) { async function _backfillTrail(icao24, token, oldestFixEpochSec) {
let path = null; let path = null;
try { try {
const track = await flightState._source.getTrack?.( const track = await flightState.feed._source.getTrack?.(
flightState._flightData.get(icao24)?.sourceReference ?? icao24, flightState.records.data.get(icao24)?.sourceReference ?? icao24,
{ {
signal: AbortSignal.any([ signal: AbortSignal.any([
flightState.lifetime.signal, flightState.lifetime.signal,
@@ -645,7 +645,7 @@ export function createTracking({
bb.color = parts.rendering._fleetBillboardColor(flightState._trackedIcao); bb.color = parts.rendering._fleetBillboardColor(flightState._trackedIcao);
bb.scale = parts.rendering._fleetBillboardScale( bb.scale = parts.rendering._fleetBillboardScale(
flightState._trackedIcao, flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
bb.rotation = flightState._lastTrackedRotation; bb.rotation = flightState._lastTrackedRotation;
} }
@@ -742,7 +742,7 @@ export function createTracking({
* velocity/altitude as live. */ * velocity/altitude as live. */
function _trackedLabelText(icao24) { function _trackedLabelText(icao24) {
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (!info) return icao24; if (!info) return icao24;
// A whitespace-only callsign (" ") is truthy, so `(info.callsign || icao24)` // A whitespace-only callsign (" ") is truthy, so `(info.callsign || icao24)`
// kept it, then .trim() emptied it → the callsign slot dropped out of the // kept it, then .trim() emptied it → the callsign slot dropped out of the
@@ -754,7 +754,7 @@ export function createTracking({
const fl = altFt >= 18000 ? `FL${Math.round(altFt / 100)}` : `${altFt} ft`; const fl = altFt >= 18000 ? `FL${Math.round(altFt / 100)}` : `${altFt} ft`;
const spd = info.velocity ? `${Math.round(info.velocity * 1.944)} kts` : ''; const spd = info.velocity ? `${Math.round(info.velocity * 1.944)} kts` : '';
const stale = const stale =
flightState._missingPolls.get(icao24) || flightState._backoff flightState.records.missingPolls.get(icao24) || flightState.feed._backoff
? 'STALE' ? 'STALE'
: ''; : '';
const lines = [[cs, fl, spd, stale].filter(Boolean).join(' · ')]; const lines = [[cs, fl, spd, stale].filter(Boolean).join(' · ')];
@@ -795,7 +795,7 @@ export function createTracking({
flightState._trackedEntity.billboard.image = aircraftIcon( flightState._trackedEntity.billboard.image = aircraftIcon(
parts.rendering._iconKind( parts.rendering._iconKind(
flightState._trackedIcao, flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
), ),
TRACKED_ICON_PX, TRACKED_ICON_PX,
); );
@@ -850,7 +850,7 @@ export function createTracking({
* cache). Missing data → true (never hide what we can't judge). */ * cache). Missing data → true (never hide what we can't judge). */
function _routeIsPlausible(icao24, route) { function _routeIsPlausible(icao24, route) {
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
if (!info || !bb || !bb.position) return true; if (!info || !bb || !bb.position) return true;
const carto = Cesium.Cartographic.fromCartesian( const carto = Cesium.Cartographic.fromCartesian(
@@ -890,7 +890,7 @@ export function createTracking({
_clearTracking(false, { origin }); // switching planes — the new follow-camera takes over _clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (!bb || !info) return; if (!bb || !info) return;
flightState._trackedIcao = icao24; flightState._trackedIcao = icao24;
@@ -945,7 +945,7 @@ export function createTracking({
image: aircraftIcon( image: aircraftIcon(
parts.rendering._iconKind( parts.rendering._iconKind(
flightState._trackedIcao, flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
), ),
TRACKED_ICON_PX, TRACKED_ICON_PX,
), ),
@@ -953,7 +953,7 @@ export function createTracking({
height: 28, height: 28,
scale: scale:
CLASS_SCALE_2D[ CLASS_SCALE_2D[
flightState._flightData.get(flightState._trackedIcao)?.klass flightState.records.data.get(flightState._trackedIcao)?.klass
] || 1, ] || 1,
// Solid cyan when the billboard is the visual (zoomed out, 3D off, or model still loading); // Solid cyan when the billboard is the visual (zoomed out, 3D off, or model still loading);
// transparent once the STANDALONE tracked model is actually up (ready + shown). // transparent once the STANDALONE tracked model is actually up (ready + shown).
@@ -975,7 +975,9 @@ export function createTracking({
// Screen-projected rotation, evaluated per frame: exact in tracked-orbit // Screen-projected rotation, evaluated per frame: exact in tracked-orbit
// mode where camera.heading lives in the entity's reference frame. // mode where camera.heading lives in the entity's reference frame.
rotation: new Cesium.CallbackProperty(() => { rotation: new Cesium.CallbackProperty(() => {
const tracked = flightState._flightData.get(flightState._trackedIcao); const tracked = flightState.records.data.get(
flightState._trackedIcao,
);
const pos = getTrackedPosition(); const pos = getTrackedPosition();
if (!tracked || !pos || !flightState._viewer) if (!tracked || !pos || !flightState._viewer)
return flightState._lastTrackedRotation; return flightState._lastTrackedRotation;
@@ -1086,13 +1088,14 @@ export function createTracking({
flightState._billboardCollection.remove(bb); flightState._billboardCollection.remove(bb);
flightState._billboards.delete(icao24); flightState._billboards.delete(icao24);
parts.rendering._releaseModel(icao24); // military-suppression: drop any 3D model too parts.rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
flightState._flightData.delete(icao24); flightState.records.data.delete(icao24);
flightState._cullPositions.delete(icao24);
flightState._positionHistory.delete(icao24); flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24); flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24); flightState._groundSnap.forget(icao24);
flightState._missingPolls.delete(icao24); flightState.records.missingPolls.delete(icao24);
} }
flightState._count = flightState._billboards.size; flightState.feed._count = flightState._billboards.size;
} else if (flightState._billboardCollection.show) { } else if (flightState._billboardCollection.show) {
// Fire-and-forget refresh; only while the layer is actually enabled. // Fire-and-forget refresh; only while the layer is actually enabled.
void flightsLayer.update(flightState._viewer); void flightsLayer.update(flightState._viewer);