Merge pull request #498 from bilawalsidhu/refactor/civil-flight-records

Separate civil-flight records and rendering
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-09-14 16:04:58 -07:00
commit d01b69118e
27 files changed
+1113 -819

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# 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 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;
those methods delegate to the state owner. Source-based regression checks inspect
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
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,
visual settings and panel chrome. Navigation owns request generations and follow
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/share-restoration": "./src/ui/shareRestoration.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/panelChrome.js",
"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/state.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"]
},
@@ -1539,7 +1542,10 @@
"src/voice/realtimeController.js",
"src/voice/voiceCost.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": [
"@mapbox/vector-tile",
@@ -1863,7 +1869,10 @@
"src/services/requests.js",
"src/ui/panelRailGeometry.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": [
"cesium",
@@ -1977,5 +1986,24 @@
"src/ui/uiLifetime.js"
],
"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]]) {
assert.match(
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`,
);
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) || [];
assert.equal(callSites.length >= 4, true, `${name}: expected the known aircraftIcon call sites`);
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`);
}
+2 -2
View File
@@ -8,9 +8,9 @@ export function createController({
resolveAsset,
}) {
function _abortActiveUpdates() {
for (const controller of flightState._activeUpdateControllers)
for (const controller of flightState.feed._activeUpdateControllers)
controller.abort();
flightState._activeUpdateControllers.clear();
flightState.feed._activeUpdateControllers.clear();
}
function _flightQuery(viewer) {
+5 -5
View File
@@ -57,7 +57,7 @@ export function createEnrichment({
Promise.resolve()
.then(() => {
lifetime.signal.throwIfAborted();
return flightState._source.getEnrichment?.(job.query, {
return flightState.feed._source.getEnrichment?.(job.query, {
signal: lifetime.signal,
});
})
@@ -81,7 +81,7 @@ export function createEnrichment({
`t:${icao24}`,
{ kind: 'type', id: icao24.toLowerCase() },
(data) => {
const meta = flightState._flightData.get(icao24);
const meta = flightState.records.data.get(icao24);
if (!meta) return; // evicted while the lookup was in flight
meta.typeCode = data.typeCode || meta.typeCode;
meta.typeName = data.typeName || meta.typeName;
@@ -109,7 +109,7 @@ export function createEnrichment({
}
function _requestRouteEnrichment(icao24) {
const cs = String(flightState._flightData.get(icao24)?.callsign || '')
const cs = String(flightState.records.data.get(icao24)?.callsign || '')
.trim()
.toUpperCase();
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}`,
{ kind: 'route', id: cs },
(data) => {
const meta = flightState._flightData.get(icao24);
const meta = flightState.records.data.get(icao24);
if (!meta) return;
meta.airline = data.airline || meta.airline;
if (data.origin && data.destination)
@@ -198,7 +198,7 @@ export function createEnrichment({
for (const [icao24, bb] of flightState._billboards) {
if (flightState._enrichSeen.has(`t:${icao24}`)) continue; // answered / queued / negative this session
if (!/^[0-9a-f]{6}$/i.test(icao24)) continue; // adsbdb keys are 6-char hex only
if (flightState._flightData.get(icao24)?.onGround) continue; // ground traffic never spends ambient budget (click-to-enrich still works)
if (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
Cesium.Cartesian3.clone(
bb.position,
+7 -7
View File
@@ -50,10 +50,10 @@ export function createEvidence({
for (const bb of flightState._billboards.values())
flightState._billboardCollection.remove(bb);
flightState._billboards.clear();
flightState._flightData.clear();
flightState.records.data.clear();
flightState._positionHistory.clear();
flightState._displayCourse.clear();
flightState._missingPolls.clear();
flightState.records.missingPolls.clear();
flightState._focusEvidenceIds.clear();
for (const record of Array.isArray(records) ? records : []) {
@@ -79,9 +79,9 @@ export function createEvidence({
lastContactEpochMs: Date.now(),
rawLat: record.latitude ?? 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);
const bb = flightState._billboardCollection.add({
position,
@@ -100,10 +100,10 @@ export function createEvidence({
});
flightState._billboards.set(id, bb);
}
flightState._count = flightState._billboards.size;
flightState.feed._count = flightState._billboards.size;
flightState._lastFleetTickMs = 0;
flightState._viewer.scene.requestRender?.();
return { ok: true, count: flightState._count };
return { ok: true, count: flightState.feed._count };
}
/** Update explicit evidence positions without rebuilding billboards. */
@@ -120,7 +120,7 @@ export function createEvidence({
const bb = flightState._billboards.get(id);
if (!position || !bb) continue;
bb.position = position;
const meta = flightState._flightData.get(id);
const meta = flightState.records.data.get(id);
if (meta) {
if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg;
if (Number.isFinite(record.velocityMps))
+25 -1
View File
@@ -1,3 +1,4 @@
import { createFlightSnapshotRenderer } from './snapshotRenderer.js';
import { createFlightState } from './state.js';
import { createRendering } from './rendering.js';
import { createMotion } from './motion.js';
@@ -24,11 +25,34 @@ export function createCivilFlightLayer({
parts.tracking = createTracking(context);
parts.controller = createController(context);
parts.enrichment = createEnrichment(context);
parts.ingestion = createIngestion(context);
parts.lifecycle = createLifecycle(context);
parts.evidence = createEvidence(context);
parts.testing = createTesting(context);
parts.queries = createQueries(context);
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(
layer,
parts.queries.methods,
+64 -613
View File
@@ -1,661 +1,112 @@
import * as Cesium from 'cesium';
import { stickyNumber, stickyText } from '../../data/aircraftMeta.js';
import {
geoidSurfaceLastResortM,
pickRenderAltitudeM,
} from '../../data/renderAltitude.js';
import { classifyAircraft } from '../../data/aircraftClass.js';
import {
turnRateFromFixHistory,
liftRepeatedGroundFix,
synthesizeForwardKinematicsFix,
} from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import {
GROUND_FLOOR_CLAMP_RADIUS_KM,
GROUND_FLOOR_WARM_MAX_ALT_M,
POSITION_HISTORY_LIMIT,
LANDED_MISSING_POLL_LIMIT,
MISSING_POLL_LIMIT,
ERROR_BACKOFF_INTERVAL,
} from './policy.js';
import { ERROR_BACKOFF_INTERVAL } from './recordPolicy.js';
/** Own acquisition, cancellation, freshness and backoff independently of rendering. */
export function createIngestion({
flightState,
services,
parts,
layer,
resolveAsset,
feed,
getQuery,
applySnapshot,
setSourceLabel,
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 = {
/**
* Fetch the latest aircraft state vectors from the OpenSky proxy and
* reconcile them with the billboard collection.
*
* Handles HTTP 429 (rate-limit), 401/403 (auth), and transient errors
* with exponential-ish backoff. On success, adds/updates/removes
* billboards and position history, triggers lerp blending for the
* tracked aircraft, and updates its label text.
*
* @param {Cesium.Viewer} viewer
* @returns {Promise<void>}
*/
async update(viewer, { signal = null } = {}) {
const nowMs = Date.now();
const trackingRefreshEpoch = ++flightState._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = {
const trackingRefreshEpoch = ++feed._trackingRefreshEpoch;
feed._lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch,
status: 'source-unavailable',
ids: new Set(),
source: flightState._lastSource,
coverage: flightState._lastCoverage,
source: feed._lastSource,
coverage: feed._lastCoverage,
};
if (flightState._retryAt && nowMs < flightState._retryAt) {
flightState._backoff = true;
if (feed._retryAt && nowMs < feed._retryAt) {
feed._backoff = true;
return;
}
const resourceController = new AbortController();
flightState._activeUpdateControllers.add(resourceController);
feed._activeUpdateControllers.add(resourceController);
const updateSignal = signal
? AbortSignal.any([signal, resourceController.signal])
: resourceController.signal;
try {
updateSignal.throwIfAborted();
const snapshot = await flightState._source.getSnapshot(
parts.controller._flightQuery(viewer || flightState._viewer),
{ signal: updateSignal },
);
const snapshot = await feed._source.getSnapshot(getQuery(viewer), {
signal: updateSignal,
});
updateSignal.throwIfAborted();
flightState._lastStatus = snapshot.status ?? 200;
const usableStates = snapshot.records;
feed._lastStatus = snapshot.status ?? 200;
const sourceEpochMs = snapshot.observedAtMs;
const sourceAgeMs = snapshot.ageMs;
const sourceStale = snapshot.stale || snapshot.freshness === 'unknown';
flightState._backoff = sourceStale;
flightState._retryAt = 0;
flightState._lastError = sourceStale
feed._backoff = sourceStale;
feed._retryAt = 0;
feed._lastError = sourceStale
? sourceAgeMs == null
? 'Source snapshot time unavailable'
: `Source snapshot ${Math.max(2, Math.round(sourceAgeMs / 60_000))} min old`
: null;
flightState._lastSource = snapshot.source;
flightState._lastCoverage = snapshot.coverage;
flightsLayer.source = flightState._lastSource;
const currentIcaos = new Set();
const acceptedSnapshotIcaos = new Set();
const now = Cesium.JulianDate.now();
// Field-test round 3 (2026-07-06, Austin fleet-underground): viewer
// subpoint + collected floor cells for the viewer-proximate low-contact
// clamp below — one carto read per poll, one batch warm after the loop.
const viewerCarto =
(viewer || flightState._viewer)?.camera?.positionCartographic || null;
const viewerLatDeg = viewerCarto
? Cesium.Math.toDegrees(viewerCarto.latitude)
: null;
const viewerLonDeg = viewerCarto
? Cesium.Math.toDegrees(viewerCarto.longitude)
: null;
const floorWarmPoints = [];
// Classification and display policy consume source-independent observations.
refreshMilitaryRegistryIfStale();
for (const observation of usableStates) {
const {
id: icao24,
callsign,
originCountry: origin_country,
longitude: lon,
latitude: lat,
baroAltitudeM: baro_alt,
onGround: on_ground,
speedMps: velocity,
courseDeg: true_track,
ellipsoidAltitudeM: geo_alt,
category,
verticalRateMps: vertical_rate,
} = observation;
acceptedSnapshotIcaos.add(icao24);
const onGround = on_ground === true;
// Known-military aircraft: the dedicated military layer wins
// (icon + track + click) while it is enabled — suppress the
// OpenSky duplicate entirely (except a currently tracked one,
// which hands off on untrack).
const isMil = isMilitaryIcao(icao24);
if (isMil && parts.tracking._militaryLayerSuppresses(icao24)) {
const dupe = flightState._billboards.get(icao24);
if (dupe) {
flightState._billboardCollection.remove(dupe);
flightState._billboards.delete(icao24);
parts.rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
flightState._flightData.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
flightState._missingPolls.delete(icao24);
flightState._geoidNCache.delete(icao24);
}
continue;
}
currentIcaos.add(icao24);
flightState._missingPolls.delete(icao24);
// Sticky merge: OpenSky intermittently drops callsign/velocity/track for
// aircraft it still positions — hold last-known-good instead of
// regressing to the ICAO hex / a 0° (north) heading. Bounded by the
// MISSING_POLL_LIMIT eviction below, which deletes the whole entry.
const prevMeta = flightState._flightData.get(icao24);
// Grounded planes with no baro reading sit at 0 m, not the 10 km
// airborne default (a parked plane must never float).
// NOTE (height-datum fix): `alt` stays the AVIATION field — the sticky
// barometric/MSL altitude read by labels (FL/altitude readout),
// route-plausibility, and follow-camera range heuristics. It is
// NEVER overwritten or renamed. Where the aircraft actually RENDERS
// on the ellipsoidal globe is a SEPARATE value (renderAltitudeM,
// below) — geo_altitude when OpenSky reports it (already WGS84
// ellipsoidal), else baro+geoid as a visual fallback, else ground
// surface when parked. `Cartesian3.fromDegrees` gets renderAltitudeM,
// never `alt` directly.
const alt = stickyNumber(
baro_alt,
prevMeta?.altitude,
onGround ? 0 : 10000,
);
// geoid undulation N: cached per-aircraft (negligible drift — see
// task brief) once the geoid grid has loaded; unavailable pre-load
// just means the baro fallback branch below adds N=0 for a beat.
let geoidN = flightState._geoidNCache.get(icao24);
if (geoidN === undefined && flightState._geoidReady) {
geoidN = geoidHeight(lat, lon);
flightState._geoidNCache.set(icao24, geoidN);
}
// on_ground surface prior: ONLY synchronous warm-cache reads here —
// never a per-aircraft network fetch inside the poll loop (see the
// batch resolve call below, which fills this cache for NEXT poll). A
// Round 5 SIMPLIFICATION (owner directive: one floor, evenly applied):
// the grounded surface is the round-4 choke point and nothing else —
// rendered-mesh cell first, real (never fallback-poisoned) DEM cell
// second. The old exact-5-decimal warm chain is GONE: it minted a new
// key per parked-jitter poll for every grounded contact ON EARTH,
// hammering Re:Earth into the very failures that poisoned the cache.
let surfaceM = null;
if (onGround) {
surfaceM = cachedGroundFloor(lat, lon); // mesh ?? real DEM (coarse cell)
// Taxiing crosses into a fresh cold cell every poll — always one
// step ahead of the warm batch — so fall back to LAST poll's cell
// (warmed by last poll's batch; aprons are flat across adjacent
// 111 m cells). Round-5 verify caught taxiing contacts stuck at
// the geoid without this (round 2's lesson, at cell granularity).
if (
surfaceM == null &&
Number.isFinite(prevMeta?.rawLat) &&
Number.isFinite(prevMeta?.rawLon)
) {
surfaceM = cachedGroundFloor(prevMeta.rawLat, prevMeta.rawLon);
}
// Grounded contacts near the viewer feed the floor warm/sampler
// (the only ones whose exact height is visible; far contacts are
// subpixel and always-on-top anyway).
if (
viewerLatDeg != null &&
parts.queries._approxDistanceKm(
viewerLatDeg,
viewerLonDeg,
lat,
lon,
) <= GROUND_FLOOR_CLAMP_RADIUS_KM
) {
floorWarmPoints.push({ lat, lon });
}
// Last synchronous resort for a BRAND-NEW grounded contact with NO
// altitude data at all (nothing warm yet, not even the coarse
// cell): the geoid surface. At the sea-level airports where most
// grounded traffic sits, geoidN IS the local ellipsoidal ground to
// within metres — instantly right — and at elevated fields it is
// far less wrong than the raw 0 m ellipsoid default for the one
// poll until the coarse cell warms. STRICTLY gated on "no geo, no
// baro": a reported baro already reflects the field elevation, and
// pickRenderAltitudeM's surfaceM branch would let this crude guess
// outrank it (caught by the ground-3d track regression).
//
// 2026-08-21: the rule moved to geoidSurfaceLastResortM, which adds
// one more gate — a contact that already HAS a render height keeps
// it. Leaving surfaceM null routes it through the sentinel path
// below, which holds that height.
if (surfaceM == null) {
surfaceM = geoidSurfaceLastResortM({
geoAltM: geo_alt,
baroAltM: baro_alt,
priorRenderM: prevMeta?.renderAltitudeM,
geoidN,
});
}
}
const geoAltitudeM = Number.isFinite(geo_alt) ? geo_alt : null;
const pickedAltM = pickRenderAltitudeM({
geoAltM: geoAltitudeM,
baroAltM: Number.isFinite(baro_alt) ? baro_alt : null,
onGround,
surfaceM,
geoidN,
});
// pickRenderAltitudeM returns the sentinel `null` only when NEITHER
// geo_altitude nor baro_altitude was reported THIS poll. Two fallbacks,
// in priority order:
// (1) hold the previous geoid-corrected render height if we have one —
// a one-poll baro dropout must NOT snap the plane down by the geoid
// undulation N (~46 m in London) and back up next poll. `alt` stays
// sticky for labels, so holding the last render height keeps the two
// layers consistent through the gap.
// (2) otherwise the SAME default policy `alt` already uses, so the two
// never disagree on the genuine "no data yet" case (a never-reported
// aircraft has no prior render height, so it lands here unchanged).
let renderAltitudeM;
if (pickedAltM != null) {
renderAltitudeM = pickedAltM;
} else if (Number.isFinite(prevMeta?.renderAltitudeM)) {
renderAltitudeM = prevMeta.renderAltitudeM;
} else {
renderAltitudeM = alt;
}
// Field-test fix (WAKE01/RS46 class, 2026-07-06; widened round 3):
// floor a low airborne contact's render height at the local coarse
// ground so it can never dive below the mesh. Round 3 (Austin
// fleet-underground): baro can read BELOW an elevated field — SWA696
// showed 450 ft at Austin's 542 ft field elevation — and rollout/taxi
// traffic that OpenSky hasn't flagged on_ground yet renders from that
// baro, so the whole fleet sat buried at AUS in 2D. Clamping every
// global contact would need unbounded terrain resolution; instead the
// clamp covers the TRACKED contact (always) plus every low contact
// within GROUND_FLOOR_CLAMP_RADIUS_KM of the viewer — the only ones
// whose burial is visible. Cells warm in one batch after the loop.
// Airborne only (grounded planes keep the surface-cache path above).
if (
!onGround &&
renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M &&
(icao24 === flightState._trackedIcao ||
(viewerLatDeg != null &&
parts.queries._approxDistanceKm(
viewerLatDeg,
viewerLonDeg,
lat,
lon,
) <= GROUND_FLOOR_CLAMP_RADIUS_KM))
) {
renderAltitudeM = floorAltitudeM(
renderAltitudeM,
cachedGroundFloor(lat, lon),
);
floorWarmPoints.push({ lat, lon });
}
const position = Cesium.Cartesian3.fromDegrees(
lon,
lat,
renderAltitudeM,
);
// Landing/takeoff transition: the on_ground flip restyles IN PLACE.
const groundFlipped =
!!prevMeta && (prevMeta.onGround === true) !== onGround;
// Either flip direction retires the model's ground snap: a departing plane
// flies free of it, a landing plane earns a fresh sample where it rolls out.
if (groundFlipped) flightState._groundSnap.forget(icao24);
// Store flight metadata for click-to-track labels
const cat = stickyNumber(category, prevMeta?.category, null);
const meta = {
sourceReference: observation.reference,
observedReceiptMs: Date.now(),
callsign: stickyText(callsign, prevMeta?.callsign),
altitude: alt,
// geoAltitudeM/renderAltitudeM are ADDITIVE fields alongside the
// untouched aviation `altitude` — never rename/replace it (labels,
// FL readout, route-plausibility, and follow-camera range math all
// still read `altitude`/baro).
geoAltitudeM,
renderAltitudeM,
onGround,
// Round 7: sticky airborne history — the landed fast-cull only
// applies to contacts that actually flew this session.
wasAirborne: prevMeta?.wasAirborne === true || !onGround,
// Round 6: lifted occlusion-test point for contacts rendering
// at/below the ellipsoid (fleet pass reads it — see the occluder
// note there). Null for the overwhelmingly common airborne case.
cullPosition:
renderAltitudeM < 10
? Cesium.Cartesian3.fromDegrees(lon, lat, 12)
: null,
velocity: stickyNumber(velocity, prevMeta?.velocity, 0),
true_track: stickyNumber(true_track, prevMeta?.true_track, 0),
category: cat,
// An adsbdb-enriched type code outranks the coarse OpenSky category.
klass: classifyAircraft({
typeCode: prevMeta?.typeCode ?? null,
category: cat,
}),
turnRateDps: prevMeta?.turnRateDps || 0,
verticalRate: stickyNumber(
vertical_rate,
prevMeta?.verticalRate,
null,
),
// Analyst seam: OpenSky origin_country (state[2]) — additive, sticky
// like callsign so a transient blank row doesn't blank the field.
originCountry:
stickyText(origin_country, prevMeta?.originCountry) || null,
// OpenSky distinguishes the last position epoch from the last
// transponder message. The fleet coast horizon uses this actual
// contact time so a temporarily old position does not hard-freeze
// while fresh velocity/track messages are still arriving.
lastContactEpochMs: stickyNumber(
observation.contactTimeMs,
prevMeta?.lastContactEpochMs,
null,
),
// adsbdb enrichment — written by the enrichment callbacks, carried across polls:
typeCode: prevMeta?.typeCode ?? null,
typeName: prevMeta?.typeName ?? null,
registration: prevMeta?.registration ?? null,
airline: prevMeta?.airline ?? null,
route: prevMeta?.route ?? null,
// The RAW poll fix lat/lon (this tick's OpenSky state-vector
// coords, pre-dead-reckon) — kept distinct from the continuously
// dead-reckoned billboard position for any consumer that needs the
// actual reported fix.
rawLat: lat,
rawLon: lon,
};
flightState._flightData.set(icao24, meta);
const isTracked = icao24 === flightState._trackedIcao;
// Append to position history stamped with the FEED's fix epoch
// (time_position), not client receipt time — OpenSky positions arrive
// 5-15s stale and receipt-time stamping is what caused the
// back/forward oscillation. Only append when the fix actually
// advances, so stale repeats don't create zero-dt segments.
const fixEpochMs =
Number.isFinite(observation.positionTimeMs) &&
observation.positionTimeMs > 0
? observation.positionTimeMs
: Date.now();
const fixTime = Cesium.JulianDate.fromDate(new Date(fixEpochMs));
if (!flightState._positionHistory.has(icao24)) {
flightState._positionHistory.set(icao24, []);
}
const history = flightState._positionHistory.get(icao24);
const newest = history[history.length - 1];
if (!newest || Cesium.JulianDate.greaterThan(fixTime, newest.time)) {
// Per-fix kinematics: the fix's own velocity/track ride along so the
// extrapolation paths use the values that BELONG to the fix they
// extend, not whatever the latest poll reported.
history.push({
time: fixTime,
epochMs: fixEpochMs,
position: position.clone(),
velocity: meta.velocity,
track: meta.true_track,
});
if (history.length > POSITION_HISTORY_LIMIT) {
history.shift();
}
// Turn rate from the fix-track history — computed once per new fix
// (≤5 samples), consumed by the extrapolation paths at tick rate.
meta.turnRateDps = turnRateFromFixHistory(history);
// Trail accumulation is separate from the 5-fix DR history (PRD F1)
// so the visible trail keeps growing while tracked. Ground traffic
// appends nothing — a touchdown freezes the existing trail.
// Round 2 (owner): ground traffic appends too — taxi history stays
// live after touchdown (grounded flights positions are already
// surface-clamped via the surfaceM chain, so the ground leg drapes).
if (isTracked) parts.tracking._appendTrailFix(position.clone());
} else {
const modelOwnsGroundVisual =
parts.rendering._modelOwnsVisual(icao24);
if (!modelOwnsGroundVisual) {
liftRepeatedGroundFix(newest, position, meta.onGround);
}
// Apply fresh kinematics only from a forward synthetic fix. Mutating
// the historical fix reprojects the entire stale interval and snaps
// the rendered aircraft when course or speed changes late.
const kinematicsChanged =
newest.velocity !== meta.velocity ||
newest.track !== meta.true_track;
if (kinematicsChanged) {
const synthetic = synthesizeForwardKinematicsFix(newest, {
epochMs: Date.now(),
velocity: meta.velocity,
track: meta.true_track,
turnRateDps: meta.turnRateDps,
});
if (synthetic) {
history.push(synthetic);
if (history.length > POSITION_HISTORY_LIMIT) history.shift();
meta.turnRateDps = turnRateFromFixHistory(history);
if (isTracked)
parts.tracking._appendTrailFix(synthetic.position.clone());
}
}
}
if (flightState._billboards.has(icao24)) {
const bb = flightState._billboards.get(icao24);
// Reclassify if the category resolved/changed (first extended poll);
// a ground flip (landing/takeoff) re-scales the SAME billboard in
// place — the transition is a restyle, never a removal.
// Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff.
// Position AND rotation are owned by the fleet pass (_fleetTick);
// course changes land on the next rotation pass (forced below).
if (!isTracked) {
// Refresh affiliation hue without clobbering the tick-owned
// freshness × focus × horizon alpha composition.
bb.color = parts.rendering
._fleetBillboardColor(icao24)
.withAlpha(bb.color?.alpha ?? 1);
}
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Poll-path class change (category updates): same model resync rule
// as the enrichment path — the class's GLB/scale may have changed.
if (prevMeta?.klass !== meta.klass)
parts.rendering._syncModelToClass(icao24);
} else {
const bb = flightState._billboardCollection.add({
position,
image: aircraftIcon(
parts.rendering._iconKind(icao24, meta.klass),
),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale: parts.rendering._fleetBillboardScale(icao24, meta.klass),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: isTracked
? Cesium.Color.CYAN
: parts.rendering._fleetBillboardColor(icao24),
sizeInMeters: false,
scaleByDistance:
parts.rendering._normalBillboardScaleByDistance(),
// Grounded/near-surface planes sit at/below the photoreal tile
// surface — render them depth-test-free so they never vanish up
// close (_groundDepthDistance).
disableDepthTestDistance: parts.rendering._groundDepthDistance(),
id: icao24,
show: !isTracked, // hidden if currently tracked (entity replaces it)
});
flightState._billboards.set(icao24, bb);
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Takeoff while TRACKED: ground traffic drew no trail, so start one
// from the fresh airborne history (touchdown needs no action — the
// append gate above simply freezes the existing trail).
if (isTracked && groundFlipped && !meta.onGround)
parts.tracking._startTrail(icao24);
// If this is the tracked aircraft, update label text
// (position updates automatically via dead-reckoning CallbackProperty)
if (isTracked && flightState._trackedEntity) {
parts.tracking._updateTrackedLabelModel(icao24);
}
}
// Remove aircraft only after MISSING_POLL_LIMIT consecutive absences.
// OpenSky routinely drops aircraft for a single poll; immediate removal
// made planes blink and yanked the camera off actively tracked flights.
// EXCEPTION: likely-landed planes (last fix low + slow) get only
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
// feed gap, and the full grace left phantom planes parked at airports.
for (const [icao24, bb] of flightState._billboards) {
if (currentIcaos.has(icao24)) continue;
// Partial admissions do not prove absence, but stale retention is bounded.
if (
!snapshot.complete &&
Date.now() -
(flightState._flightData.get(icao24)?.observedReceiptMs ?? 0) <
300000
)
continue;
const misses = (flightState._missingPolls.get(icao24) || 0) + 1;
const limit = parts.queries._likelyLanded(icao24)
? LANDED_MISSING_POLL_LIMIT
: MISSING_POLL_LIMIT;
if (misses < limit) {
flightState._missingPolls.set(icao24, misses);
if (
icao24 === flightState._trackedIcao &&
flightState._trackedEntity
) {
// Honest readout: the tracked plane has no faded billboard (its
// entity owns the visual), so refresh the label — with icao24 now
// in _missingPolls, _trackedLabelText appends the STALE cue so
// last-known velocity/altitude aren't presented as live.
parts.tracking._updateTrackedLabelModel(icao24);
}
continue;
}
flightState._missingPolls.delete(icao24);
// If the tracked flight is truly gone, clear tracking BEFORE deleting
// its state (M3 ordering, keep it): teardown reads the maps this loop
// is about to delete (billboard restore, DR cache reset), and we must
// never leave the camera mid-follow with stale tracking state. The
// camera is then RELEASED IN PLACE — it stays where the follow left
// it, fully free (owner decision 2026-07-02: no overview flyTo).
if (icao24 === flightState._trackedIcao) {
parts.tracking._clearTracking(false, { evicted: true });
}
flightState._billboardCollection.remove(bb);
flightState._billboards.delete(icao24);
parts.rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
flightState._flightData.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
flightState._geoidNCache.delete(icao24);
flightState._displayFloorState.delete(icao24);
}
// Fresh courses arrived — force a rotation pass on the next fleet tick
flightState._lastCamPoseSig = '';
// Ambient type enrichment: give ON-SCREEN planes real types (bounded
// sweep — see _sweepAmbientEnrichment; internally fail-silent).
parts.enrichment._sweepAmbientEnrichment();
// 2026-08-19: the loop above only ever collects FIX cells, but a grounded
// contact renders across every cell its dead-reckoned position drifts
// through. Add those too, so the display clamp has data where the contact
// actually is instead of silently passing.
parts.motion._collectDisplayCorridorCells(
floorWarmPoints,
viewerLatDeg,
viewerLonDeg,
);
// Field-test round 3: one batch warm of the viewer-proximate low-contact
// floor cells collected in the loop (fire-and-forget, single-flight;
// read synchronously by NEXT poll's clamp — the military-layer pattern).
warmGroundFloor(floorWarmPoints);
// Round 4: sample the RENDERED mesh for those same cells (one-shot per
// cell, budget-capped, viewer-proximate, google-3d regime only). Own
// billboards/models are excluded so a vertical probe can't land on an
// aircraft instead of the pavement.
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
excludeObjects: [
...flightState._billboards.values(),
...flightState._models.values(),
flightState._trackedModel,
].filter(Boolean),
viewerLat: viewerLatDeg,
viewerLon: viewerLonDeg,
});
// Round 5: the grounded exact-key warm that used to live here is gone —
// see the note where _warmGroundedAircraftSurfaceCache was removed. The
// viewer-proximate coarse warm + mesh sampler above cover everything
// whose height is actually visible.
// Round 6: re-floor STALE grounded contacts. A parked plane whose
// transponder went quiet stops receiving poll updates, so a floor that
// warms AFTER its last fix never applied — it sat frozen at the geoid
// (ATL verify: FFT4347 at −30.7 m, 305 m under the apron, forever).
// Grounded contacts are static, so lifting the stored fix + billboard
// in place is safe (the DR extrapolates a zero-velocity fix).
parts.motion._refloorStaleGroundedContacts(currentIcaos);
flightState._count = flightState._billboards.size;
feed._lastSource = snapshot.source;
feed._lastCoverage = snapshot.coverage;
setSourceLabel(feed._lastSource);
const accepted = applySnapshot(snapshot, viewer);
feed._count = accepted.count;
// Freshness belongs to the source snapshot, not the moment this browser
// received a cached 200 response.
flightState._lastUpdate = sourceEpochMs;
flightState._lastTrackingRefreshOutcome = {
feed._lastUpdate = sourceEpochMs;
feed._lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch,
status: 'accepted',
ids: acceptedSnapshotIcaos,
source: flightState._lastSource,
coverage: flightState._lastCoverage,
ids: accepted.ids,
source: feed._lastSource,
coverage: feed._lastCoverage,
};
console.log(`[Data:Flights] Updated: ${flightState._count} aircraft`);
parts.tracking._applyPendingTrackingRestore();
console.log(`[Data:Flights] Updated: ${feed._count} aircraft`);
applyPendingTrackingRestore();
} catch (e) {
if (updateSignal.aborted || e?.name === 'AbortError') {
throw new DOMException('Flights update aborted', 'AbortError');
}
console.warn('[Data:Flights] Fetch error:', e);
flightState._backoff = true;
flightState._retryAt =
feed._backoff = true;
feed._retryAt =
Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
flightState._lastStatus = e?.status ?? null;
feed._lastStatus = e?.status ?? null;
if (e?.source) {
flightState._lastSource = e.source;
this.source = flightState._lastSource;
feed._lastSource = e.source;
setSourceLabel(feed._lastSource);
}
flightState._lastError =
feed._lastError =
e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
} finally {
flightState._activeUpdateControllers.delete(resourceController);
feed._activeUpdateControllers.delete(resourceController);
}
},
};
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');
if (typeof source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required');
flightState._source = source;
flightState._lastSource = source.label || flightState._lastSource;
this.source = flightState._lastSource;
flightState.feed._source = source;
flightState.feed._lastSource =
source.label || flightState.feed._lastSource;
this.source = flightState.feed._lastSource;
},
/**
@@ -40,7 +41,7 @@ export function createLifecycle({
init(viewer) {
if (flightState._viewer)
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');
if (flightState.lifetime.signal.aborted)
flightState.lifetime = new AbortController();
@@ -78,19 +79,21 @@ export function createLifecycle({
});
}
flightState._billboards = new Map();
flightState._cullPositions.clear();
flightState._detectionObjects = new Map();
flightState._flightData = new Map();
flightState.records.data = new Map();
flightState._positionHistory = new Map();
flightState._displayCourse.clear();
flightState._groundSnap.clear();
flightState._count = 0;
flightState._lastUpdate = null;
flightState._backoff = false;
flightState._retryAt = 0;
flightState._lastError = null;
flightState._lastStatus = null;
flightState._lastSource = flightState._source.label || 'Aircraft';
flightState._lastCoverage = 'worldwide upstream snapshot';
flightState.feed._count = 0;
flightState.feed._lastUpdate = null;
flightState.feed._backoff = false;
flightState.feed._retryAt = 0;
flightState.feed._lastError = null;
flightState.feed._lastStatus = null;
flightState.feed._lastSource =
flightState.feed._source.label || 'Aircraft';
flightState.feed._lastCoverage = 'worldwide upstream snapshot';
flightState._trackedIcao = null;
parts.tracking._resetTrackedSelectionState();
flightState._trackedEntity = null;
@@ -142,11 +145,11 @@ export function createLifecycle({
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
// only reads geoidHeight() synchronously after this resolves (guarded by
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
if (!flightState._geoidReady) {
if (!flightState.records.geoidReady) {
const lifetime = flightState.lifetime;
ensureGeoidReady()
.then(() => {
if (!lifetime.signal.aborted) flightState._geoidReady = true;
if (!lifetime.signal.aborted) flightState.records.geoidReady = true;
})
.catch(() => {
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
@@ -300,8 +303,9 @@ export function createLifecycle({
}
flightState._planeModelLoaded = false;
flightState._billboards.clear();
flightState._cullPositions.clear();
flightState._detectionObjects.clear();
flightState._flightData.clear();
flightState.records.data.clear();
flightState._positionHistory.clear();
flightState._displayCourse.clear();
flightState._groundSnap.clear();
@@ -312,20 +316,20 @@ export function createLifecycle({
clearTimeout(flightState._enrichDripTimer);
flightState._enrichDripTimer = null;
}
flightState._missingPolls.clear();
flightState.records.missingPolls.clear();
flightState._focusEvidenceIds.clear();
flightState._count = 0;
flightState._lastUpdate = null;
flightState.feed._count = 0;
flightState.feed._lastUpdate = null;
flightState._cockpitContactMode = false;
flightState._cockpitNearContacts = new Set();
flightState._cockpitSubjectId = null;
flightState._trackingRefreshEpoch += 1;
flightState._lastTrackingRefreshOutcome = {
epoch: flightState._trackingRefreshEpoch,
flightState.feed._trackingRefreshEpoch += 1;
flightState.feed._lastTrackingRefreshOutcome = {
epoch: flightState.feed._trackingRefreshEpoch,
status: 'destroyed',
ids: new Set(),
source: flightState._lastSource,
coverage: flightState._lastCoverage,
source: flightState.feed._lastSource,
coverage: flightState.feed._lastCoverage,
};
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
flightState._viewer = null;
+11 -11
View File
@@ -81,7 +81,7 @@ export function createMotion({
*/
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 (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon))
continue;
@@ -94,7 +94,7 @@ export function createMotion({
)
continue;
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(
info.rawLon,
info.rawLat,
@@ -125,7 +125,7 @@ export function createMotion({
*/
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)) {
const position = flightState._billboards.get(icao24)?.position;
flightState._drCourseDeg = info?.true_track || 0;
@@ -323,7 +323,7 @@ export function createMotion({
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
if (parts.rendering._modelOwnsVisual(icao24)) {
const spec = parts.rendering._modelSpec(
flightState._flightData.get(icao24)?.klass,
flightState.records.data.get(icao24)?.klass,
);
const scale = trackedModelScaleForPixelCap({
baseScale: spec.scale,
@@ -349,7 +349,7 @@ export function createMotion({
const height = billboard?.height?.getValue(time) ?? 28;
const scale =
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 =
billboard?.scaleByDistance?.getValue(time) ??
TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
@@ -392,7 +392,7 @@ export function createMotion({
}
const nowMs = Date.now();
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
if (sameTrack) {
const dtSec = Math.max(0.001, (nowMs - flightState._drPrevMs) / 1000);
const speed = (info && info.velocity) || 0;
@@ -499,7 +499,7 @@ export function createMotion({
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) {
const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
return modelVisualAnchor(
flightState._trackedModel.modelMatrix,
@@ -544,7 +544,7 @@ export function createMotion({
)
return 0;
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)
? flightState._trackedModel.computedScale
@@ -558,7 +558,7 @@ export function createMotion({
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) {
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
// 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. */
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 cacheValid =
flightState._drReconcileValid &&
@@ -1018,7 +1018,7 @@ export function createMotion({
}
const candidates = [];
for (const [icao24, info] of flightState._flightData) {
for (const [icao24, info] of flightState.records.data) {
if (!info?.onGround) continue;
// T7: a contact whose 3D model is the visual never reads a display floor.
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 second = createFlightState({ services: services() });
for (const key of [
'_flightData',
'records',
'feed',
'_billboards',
'_positionHistory',
'_displayFloorState',
'_groundSnap',
'_scratchCarto',
'_models',
'_activeUpdateControllers',
'lifetime',
]) {
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 () => {
+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';
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);
/** @constant {number} ERROR_BACKOFF_INTERVAL - Cooldown (ms) after transient errors */
export const ERROR_BACKOFF_INTERVAL = 20000;
// transient error retry
/** @constant {number} POSITION_HISTORY_LIMIT - Max position samples kept per aircraft for dead reckoning */
export const POSITION_HISTORY_LIMIT = 5;
export const COCKPIT_CONTACT_SIZE_PX = 6;
export const COCKPIT_CIVILIAN_COLOR =
@@ -151,8 +151,6 @@ export const RENDER_DELAY_SEC = 30;
/** @constant {number} Polls an aircraft may miss before removal (transient OpenSky dropouts). */
export const MISSING_POLL_LIMIT = 3;
// --- Landed-plane fast cull (owner field report 2026-07-02: "phantom" planes
// lingered ~2 min at airports after touchdown). OpenSky's on_ground flag LAGS
// the actual landing, so a landed plane's last airborne-classified fixes show
@@ -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). */
export const LANDED_MISSING_POLL_LIMIT = 1;
// Field-test rounds 1+3 (2026-07-06): below-ground floor clamp scope. Only
// contacts rendering below the alt ceiling are ever clamped/warmed (terrain
// tops out well under it outside the extreme Himalaya; cruise traffic can't
@@ -184,12 +180,8 @@ export const LANDED_MISSING_POLL_LIMIT = 1;
// need unbounded terrain resolution (the tracked contact clamps regardless).
/** @constant {number} Max render altitude (m, ellipsoidal) eligible for the ground-floor clamp. */
export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500;
/** @constant {number} Max viewer distance (km) for the fleet ground-floor clamp. */
export const GROUND_FLOOR_CLAMP_RADIUS_KM = 150;
// ---------------------------------------------------------------------------
// Icon orientation (2026-06-10 playtest fix): rotation is computed by
// projecting each aircraft's course vector into WINDOW coordinates
+26 -28
View File
@@ -1,3 +1,4 @@
import { approxDistanceKm as _approxDistanceKm } from './recordPolicy.js';
import { displayedKinematics } from '../../data/motionModel.js';
import * as Cesium from 'cesium';
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
@@ -43,13 +44,6 @@ export function createQueries({
* @returns {number} Approximate great-circle distance in km.
*/
function _approxDistanceKm(lat1, lon1, lat2, lon2) {
const dLat = (lat2 - lat1) * 111.32;
const dLon =
(lon2 - lon1) * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
return Math.hypot(dLat, dLon);
}
/**
* True when the aircraft's latest metadata reads "on or about the runway"
* (low + slow) — see the landed fast-cull rationale above. Both gates must
@@ -59,7 +53,7 @@ export function createQueries({
*/
function _likelyLanded(icao24) {
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
if (!info) return false;
// Round 7 (owner: "fewer planes than OpenSky's own map" + "parked planes
// never heal"): the fast cull only applies to contacts that were AIRBORNE
@@ -136,7 +130,7 @@ export function createQueries({
// user-visible surface renders `position` as the plane's on-screen location —
// every visual consumer reads the floored per-frame cache instead (see
// `_trackedDisplayPosition`). If that ever changes, floor this path too.
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24);
const basePos =
parts.motion._deadReckon(icao24) || (bb ? bb.position : null);
@@ -170,7 +164,8 @@ export function createQueries({
velocityMps: displayed.speedMps,
track: displayed.trackDeg,
stale: Boolean(
flightState._missingPolls.get(icao24) || flightState._backoff,
flightState.records.missingPolls.get(icao24) ||
flightState.feed._backoff,
),
airline: info?.airline ?? null,
// CLASS label follows the TR-3B conversion so every downstream card
@@ -271,7 +266,7 @@ export function createQueries({
icon: '✈️',
source: flightState._lastSource,
source: flightState.feed._lastSource,
// Browser-harness seam: isolates synthetic display-floor scenarios without
// changing any production lifecycle or cache policy.
@@ -429,7 +424,7 @@ export function createQueries({
const model = flightState._models.get(icao24);
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24);
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);
if (!object) {
object = {
@@ -496,7 +491,7 @@ export function createQueries({
* Best match with a cloned, dead-reckoned position, or null if none.
*/
findByQuery(query) {
if (!flightState._flightData || flightState._flightData.size === 0)
if (!flightState.records.data || flightState.records.data.size === 0)
return null;
const q = String(query || '')
.trim()
@@ -513,7 +508,7 @@ export function createQueries({
// strictly tiered so a registration can never out-rank a real callsign on
// feed order alone.
let best = null;
for (const [icao24, info] of flightState._flightData) {
for (const [icao24, info] of flightState.records.data) {
const candidate = {
tier: rankContactMatch({
query: q,
@@ -580,7 +575,7 @@ export function createQueries({
const distance = Cesium.Cartesian3.distance(center, pos);
if (distance > maxRange) continue;
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
const callsign = info?.callsign?.trim() || null;
nearby.push({
// Label. Callers that need identity read `icao24` (Context cohorts do).
@@ -664,7 +659,7 @@ export function createQueries({
flightState._scratchCarto,
);
if (!carto) continue;
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
result.push({
id: icao24, // identity (trackById resolves this)
label: _contactLabel(icao24, info),
@@ -703,14 +698,14 @@ export function createQueries({
if (
!flightState._billboardCollection ||
!flightState._billboardCollection.show ||
flightState._flightData.size === 0
flightState.records.data.size === 0
)
return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 2000;
const result = [];
for (const [icao24, info] of flightState._flightData) {
for (const [icao24, info] of flightState.records.data) {
const routeOk =
!!info?.route && parts.tracking._routeIsPlausible(icao24, info.route);
result.push(
@@ -754,7 +749,7 @@ export function createQueries({
};
const id = _normalizeTrackedIcao(icao24);
if (!id) return { status: 'missing', reason: 'invalid-target' };
const outcome = flightState._lastTrackingRefreshOutcome;
const outcome = flightState.feed._lastTrackingRefreshOutcome;
if (outcome.status !== 'accepted') {
return {
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}}
*/
getStats() {
const retryInSec = flightState._retryAt
? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000))
const retryInSec = flightState.feed._retryAt
? Math.max(
0,
Math.ceil((flightState.feed._retryAt - Date.now()) / 1000),
)
: 0;
return {
count: flightState._count,
lastUpdate: flightState._lastUpdate,
stale: flightState._backoff,
error: flightState._lastError,
status: flightState._lastStatus,
count: flightState.feed._count,
lastUpdate: flightState.feed._lastUpdate,
stale: flightState.feed._backoff,
error: flightState.feed._lastError,
status: flightState.feed._lastStatus,
retryInSec,
source: flightState._lastSource,
coverage: flightState._lastCoverage,
source: flightState.feed._lastSource,
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) {
return (
(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;
}
const meta = flightState._flightData.get(icao24);
const meta = flightState.records.data.get(icao24);
bb.image = aircraftIcon(
_iconKind(icao24, meta?.klass),
bb._gevIconLarge ? TRACKED_ICON_PX : undefined,
@@ -303,7 +303,7 @@ export function createRendering({
* because a resample is mid-backoff. */
function _modelDisplayPosition(icao24, pos, result) {
const meta = flightState._flightData.get(icao24);
const meta = flightState.records.data.get(icao24);
if (!meta || !meta.onGround) return pos;
const h = flightState._groundSnap.heightFor(
flightState._viewer,
@@ -438,7 +438,7 @@ export function createRendering({
* the same rule (its billboard entity is always the fallback visual). */
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);
if (
(current && current._gevSpecKey !== key) ||
@@ -510,10 +510,10 @@ export function createRendering({
// aircraft mid-load, the post-await admission below rejects the stale asset.
// Boost state likewise: the creation options bake it in, so a mid-load
// toggle must reject too (the reload queue only covers ADMITTED models).
const specKey = _specKeyFor(flightState._flightData.get(icao24)?.klass);
const specKey = _specKeyFor(flightState.records.data.get(icao24)?.klass);
const loadIrBoost = flightState._irBoost;
try {
const spec = _modelSpec(flightState._flightData.get(icao24)?.klass);
const spec = _modelSpec(flightState.records.data.get(icao24)?.klass);
model = await Cesium.Model.fromGltfAsync({
url: resolveAsset(spec.url),
asynchronous: false,
@@ -559,12 +559,12 @@ export function createRendering({
!_modelRegimeActive() ||
!flightState._modelCollection ||
flightState._modelCollection.isDestroyed() ||
!flightState._flightData.has(icao24) ||
!flightState.records.data.has(icao24) ||
icao24 === flightState._trackedIcao ||
flightState._models.has(icao24) ||
flightState._models.size >= _modelCap() ||
// Class reclassified mid-load → this GLB/scale is for the OLD class.
_specKeyFor(flightState._flightData.get(icao24)?.klass) !== specKey ||
_specKeyFor(flightState.records.data.get(icao24)?.klass) !== specKey ||
// IR boost flipped mid-load → this model baked the wrong shader/tint.
flightState._irBoost !== loadIrBoost;
if (stale) {
@@ -714,10 +714,10 @@ export function createRendering({
flightState._trackedModelLoading = true;
const gen = flightState._trackedModelGen;
const trackedSpec = _modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
const trackedKey = _specKeyFor(
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
const trackedIrBoost = flightState._irBoost;
Cesium.Model.fromGltfAsync({
@@ -756,7 +756,7 @@ export function createRendering({
// _trackedModel was still null): drop the stale asset; driver reloads.
if (
_specKeyFor(
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
) !== trackedKey ||
flightState._irBoost !== trackedIrBoost
) {
@@ -829,7 +829,7 @@ export function createRendering({
);
if (!flightState._trackedModel.ready) return;
const spec = _modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
flightState._trackedModel.scale = trackedModelScaleForPixelCap({
baseScale: spec.scale,
@@ -989,7 +989,7 @@ export function createRendering({
for (const [icao24, bb] of flightState._billboards) {
if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
const dr = parts.motion._deadReckon(icao24, flightState._scratchFleetPos);
// 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
// waiting for its floor to warm (ATL grounded contacts at geoid −31 m).
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)
// gets its rotation refreshed THIS tick even without a pose change —
@@ -1074,7 +1074,7 @@ export function createRendering({
flightState._cockpitContactMode && !isCockpitNear
? 1
: _fleetBillboardScale(icao24, info?.klass),
baseAlpha: flightState._missingPolls.get(icao24) ? 0.45 : 1,
baseAlpha: flightState.records.missingPolls.get(icao24) ? 0.45 : 1,
baseColor,
focusFactor: focus.factor,
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 { ENRICH_AMBIENT_BUDGET_CEIL } from './policy.js';
export function createFlightState({ source, services }) {
const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() };
flightState._source = source;
flightState.feed = createFlightFeed(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
* 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();
/** @type {Map<string, {callsign:string, altitude:number, velocity:number, true_track:number}>} */
flightState._flightData = new Map();
/** DEV-only explicit-position contacts used by qa-focus-evidence.mjs. */
flightState._focusEvidenceIds = new Set();
@@ -118,48 +120,6 @@ export function createFlightState({ source, services }) {
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
// ---------------------------------------------------------------------------
@@ -172,16 +132,6 @@ export function createFlightState({ source, services }) {
flightState._trackingIntentGeneration = 0;
flightState._trackingRefreshEpoch = 0;
flightState._lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
ids: new Set(),
source: flightState._lastSource,
coverage: null,
};
/** @type {Cesium.Entity|null} Entity used for camera tracking */
flightState._trackedEntity = null;
@@ -250,10 +200,6 @@ export function createFlightState({ source, services }) {
flightState._trailBackfillToken = 0;
/** @type {Map<string, number>} icao24 -> consecutive missed polls */
flightState._missingPolls = new Map();
/** @type {number} Epoch ms of the last fleet dead-reckoning pass */
flightState._lastFleetTickMs = 0;
+9 -8
View File
@@ -69,14 +69,15 @@ export function createTesting({
flightState._modelCollection = modelCollection;
flightState._billboardCollection = billboardCollection;
flightState._billboards = new Map([[icao24, billboard]]);
flightState._cullPositions.clear();
flightState._models.clear();
for (const [key, model] of models) flightState._models.set(key, model);
flightState._detectionObjects = new Map();
flightState._flightData = new Map([[icao24, meta]]);
flightState.records.data = new Map([[icao24, meta]]);
flightState._positionHistory = new Map([[icao24, history]]);
flightState._missingPolls = new Map();
flightState.records.missingPolls = new Map();
flightState._displayCourse.clear();
flightState._geoidNCache.clear();
flightState.records.geoidNCache.clear();
flightState._trackedIcao = tracked ? icao24 : null;
flightState._trackedEntity = tracked ? entity : null;
flightState._trackedModel = null;
@@ -86,8 +87,8 @@ export function createTesting({
// production state owned by the tracking lifecycle (_resetTrackedSelectionState),
// and clearing them here would mask exactly the deselect→re-track hole this
// seam is used to test.
flightState._backoff = false;
flightState._retryAt = 0;
flightState.feed._backoff = false;
flightState.feed._retryAt = 0;
}
/** Seed the authoritative snapshot outcome used by share-Follow tests. */
@@ -98,8 +99,8 @@ export function createTesting({
source = 'OpenSky Network',
coverage = 'test',
} = {}) {
const epoch = ++flightState._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = {
const epoch = ++flightState.feed._trackingRefreshEpoch;
flightState.feed._lastTrackingRefreshOutcome = {
epoch,
status,
ids: new Set(ids.map((id) => String(id).trim().toLowerCase())),
@@ -117,7 +118,7 @@ export function createTesting({
history = [],
}) {
flightState._billboards.set(icao24, billboard);
flightState._flightData.set(icao24, meta);
flightState.records.data.set(icao24, meta);
flightState._positionHistory.set(icao24, history);
}
+21 -18
View File
@@ -60,7 +60,7 @@ export function createTracking({
function _publishTrackedSelection(icao24, origin = 'programmatic') {
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 (flightState._trackedEntity)
flightState._trackedEntity.gevSelectionOrigin = origin;
@@ -99,10 +99,10 @@ export function createTracking({
id: icao24,
layerId: 'flights',
layerName: 'Live Flights',
source: flightState._lastSource,
source: flightState.feed._lastSource,
label: parts.queries._contactLabel(
icao24,
flightState._flightData.get(icao24),
flightState.records.data.get(icao24),
),
latitude: described.latitude,
longitude: described.longitude,
@@ -111,7 +111,7 @@ export function createTracking({
properties: {
name: parts.queries._contactLabel(
icao24,
flightState._flightData.get(icao24),
flightState.records.data.get(icao24),
),
operator: described.airline || '',
callsign: described.callsign || '',
@@ -477,8 +477,8 @@ export function createTracking({
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
let path = null;
try {
const track = await flightState._source.getTrack?.(
flightState._flightData.get(icao24)?.sourceReference ?? icao24,
const track = await flightState.feed._source.getTrack?.(
flightState.records.data.get(icao24)?.sourceReference ?? icao24,
{
signal: AbortSignal.any([
flightState.lifetime.signal,
@@ -645,7 +645,7 @@ export function createTracking({
bb.color = parts.rendering._fleetBillboardColor(flightState._trackedIcao);
bb.scale = parts.rendering._fleetBillboardScale(
flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
bb.rotation = flightState._lastTrackedRotation;
}
@@ -742,7 +742,7 @@ export function createTracking({
* velocity/altitude as live. */
function _trackedLabelText(icao24) {
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
if (!info) return icao24;
// A whitespace-only callsign (" ") is truthy, so `(info.callsign || icao24)`
// 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 spd = info.velocity ? `${Math.round(info.velocity * 1.944)} kts` : '';
const stale =
flightState._missingPolls.get(icao24) || flightState._backoff
flightState.records.missingPolls.get(icao24) || flightState.feed._backoff
? 'STALE'
: '';
const lines = [[cs, fl, spd, stale].filter(Boolean).join(' · ')];
@@ -795,7 +795,7 @@ export function createTracking({
flightState._trackedEntity.billboard.image = aircraftIcon(
parts.rendering._iconKind(
flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
),
TRACKED_ICON_PX,
);
@@ -850,7 +850,7 @@ export function createTracking({
* cache). Missing data → true (never hide what we can't judge). */
function _routeIsPlausible(icao24, route) {
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24);
if (!info || !bb || !bb.position) return true;
const carto = Cesium.Cartographic.fromCartesian(
@@ -890,7 +890,7 @@ export function createTracking({
_clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
const bb = flightState._billboards.get(icao24);
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
if (!bb || !info) return;
flightState._trackedIcao = icao24;
@@ -945,7 +945,7 @@ export function createTracking({
image: aircraftIcon(
parts.rendering._iconKind(
flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
),
TRACKED_ICON_PX,
),
@@ -953,7 +953,7 @@ export function createTracking({
height: 28,
scale:
CLASS_SCALE_2D[
flightState._flightData.get(flightState._trackedIcao)?.klass
flightState.records.data.get(flightState._trackedIcao)?.klass
] || 1,
// 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).
@@ -975,7 +975,9 @@ export function createTracking({
// Screen-projected rotation, evaluated per frame: exact in tracked-orbit
// mode where camera.heading lives in the entity's reference frame.
rotation: new Cesium.CallbackProperty(() => {
const tracked = flightState._flightData.get(flightState._trackedIcao);
const tracked = flightState.records.data.get(
flightState._trackedIcao,
);
const pos = getTrackedPosition();
if (!tracked || !pos || !flightState._viewer)
return flightState._lastTrackedRotation;
@@ -1086,13 +1088,14 @@ export function createTracking({
flightState._billboardCollection.remove(bb);
flightState._billboards.delete(icao24);
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._displayCourse.delete(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) {
// Fire-and-forget refresh; only while the layer is actually enabled.
void flightsLayer.update(flightState._viewer);