refactor: separate military-flight records and rendering

This commit is contained in:
Sameh Khamis committed 2026-09-14 16:17:06 -07:00
1 parent d01b69118e
commit 9e84b03c5e
23 files changed
+928 -665

No files matched your search

+2 -2
View File
@@ -6,9 +6,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();
}
return { _abortActiveUpdates };
}
+21 -1
View File
@@ -1,3 +1,4 @@
import { createMilitarySnapshotRenderer } from './snapshotRenderer.js';
import { createFlightState } from './state.js';
import { createRendering } from './rendering.js';
import { createMotion } from './motion.js';
@@ -21,10 +22,29 @@ export function createMilitaryFlightLayer({
parts.motion = createMotion(context);
parts.tracking = createTracking(context);
parts.controller = createController(context);
parts.ingestion = createIngestion(context);
parts.lifecycle = createLifecycle(context);
parts.testing = createTesting(context);
parts.queries = createQueries(context);
const applySnapshot = createMilitarySnapshotRenderer({
flightState,
records: flightState.records,
groundFloor: services.groundFloor,
meshFloor: services.meshFloor,
militaryRegistry: services.militaryRegistry,
rendering: parts.rendering,
tracking: parts.tracking,
queries: parts.queries,
});
parts.ingestion = createIngestion({
feed: flightState.feed,
applySnapshot,
setSourceLabel: (source) => {
layer.source = source;
},
applyPendingTrackingRestore: () =>
parts.tracking._applyPendingTrackingRestore(),
});
Object.assign(
layer,
parts.queries.methods,
+56 -501
View File
@@ -1,547 +1,102 @@
import { pickRenderAltitudeM } from '../../data/renderAltitude.js';
import * as Cesium from 'cesium';
import { stickyText, stickyNumber } from '../../data/aircraftMeta.js';
import { classifyAircraft, CLASS_SCALE_2D } from '../../data/aircraftClass.js';
import {
turnRateFromFixHistory,
liftRepeatedGroundFix,
synthesizeForwardKinematicsFix,
} from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import {
GROUND_FLOOR_WARM_MAX_ALT_M,
POSITION_HISTORY_LIMIT,
BILLBOARD_SCALE,
GROUND_SCALE,
TRACKED_ICON_COLOR,
MIL_ICON_COLOR,
LANDED_MISSING_POLL_LIMIT,
MISSING_POLL_LIMIT,
ERROR_BACKOFF_INTERVAL,
} from './policy.js';
import { ERROR_BACKOFF_INTERVAL } from './recordPolicy.js';
/** Own military source acquisition, cancellation, freshness and error backoff. */
export function createIngestion({
flightState,
services,
parts,
layer,
resolveAsset,
feed,
applySnapshot,
setSourceLabel,
applyPendingTrackingRestore,
}) {
const { geoidHeight } = services.geoid;
const {
floorAltitudeM,
cachedGroundFloor,
warmGroundFloor,
GROUND_FLOOR_LIFT_M,
} = services.groundFloor;
const { sampleMeshFloorCells } = services.meshFloor;
const { registerMilitaryIcaos } = services.militaryRegistry;
const militaryFlightsLayer = layer;
const methods = {
/**
* Fetch the latest military aircraft positions from adsb.lol and reconcile
* with the billboard collection. Handles error backoff, feed-time-stamped
* position history updates, and grace-period removal of absent aircraft.
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
* @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,
source: feed._lastSource,
};
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(
const snapshot = await feed._source.getSnapshot(
{},
{ signal: updateSignal },
);
updateSignal.throwIfAborted();
flightState._lastStatus = snapshot.status ?? 200;
flightState._lastSource = snapshot.source;
militaryFlightsLayer.source = flightState._lastSource;
const usableAircraft = snapshot.records;
flightState._backoff =
snapshot.stale || snapshot.freshness === 'unknown';
flightState._retryAt = 0;
flightState._lastError =
feed._lastStatus = snapshot.status ?? 200;
feed._lastSource = snapshot.source;
setSourceLabel(feed._lastSource);
feed._backoff = snapshot.stale || snapshot.freshness === 'unknown';
feed._retryAt = 0;
feed._lastError =
snapshot.reason ||
(snapshot.freshness === 'unknown'
? 'Source snapshot time unavailable'
: null);
const currentIcaos = new Set();
const receiptNowMs = snapshot.observedAtMs;
// Field-test fix (RS46): coarse floor cells to warm for the below-ground
// clamp — collected during the loop (low airborne contacts only),
// batch-resolved once after it. Never a fetch inside the loop.
const _floorWarmPoints = [];
for (const aircraft of usableAircraft) {
const { id: icao24, longitude: lon, latitude: lat } = aircraft;
currentIcaos.add(icao24);
flightState._missingPolls.delete(icao24);
const prevMeta = flightState._flightData.get(icao24);
// adsb.lol/readsb reports GROUND traffic as alt_baro === "ground" (no
// separate boolean). Grounded planes fall back to their last known
// altitude (field elevation is unknowable here), else 0 m — never the
// 3 km airborne default (a parked plane must not float).
const onGround = aircraft.onGround;
const altitudeFt =
aircraft.baroAltitudeM == null
? null
: aircraft.baroAltitudeM / 0.3048;
const altitudeM =
aircraft.baroAltitudeM ??
(onGround
? Number.isFinite(prevMeta?.altitudeFt)
? prevMeta.altitudeFt * 0.3048
: 0
: 3048);
const track = aircraft.courseDeg || 0;
const speedMps = aircraft.speedMps ?? 0;
const verticalRateMps = aircraft.verticalRateMps;
const callsign = aircraft.callsign;
const type = aircraft.typeCode;
const registration = aircraft.registration;
const operator = aircraft.operator;
const geoAltitudeM = aircraft.ellipsoidAltitudeM;
const baroAltitudeM = aircraft.baroAltitudeM;
// 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);
}
// GROUND-SNAP INTERPLAY (brief item 3 — "don't double-correct"): a
// grounded plane's MODEL already rides groundSnap.js's one-shot tileset
// sample (_modelDisplayPosition), which is the visual on the ground, and
// its billboard is depth-test-free (_groundDepthDistance) so its exact
// height is cosmetic. Deliberately pass surfaceM=null so
// pickRenderAltitudeM's on-ground surface branch never fires here:
// 1. It would be the SECOND correction of the same grounded plane
// (model tileset-snap is the first) — the exact double-correct the
// brief forbids.
// 2. Military ground rows carry NO baro ("alt_baro":"ground"), so the
// grounded billboard sits at 0 m until a surface value warms; letting
// surfaceM then jump it 0 -> ~surface (often ~100 m) BETWEEN polls
// drags the model's ground-snap input past groundSnap's 50 m
// move-invalidation threshold and forces a re-sample every time the
// cache warms — breaking the ONE-SHOT-per-(camera,regime) invariant
// the track regression locks (qa: sampleHeight count must stay flat).
// Grounded planes therefore keep the pre-existing `altitudeM` default
// (last-known baro / 0). The datum fix (alt_geom -> baro+geoidN) is what
// matters for AIRBORNE military planes — the actual "renders at MSL" bug.
const pickedAltM = pickRenderAltitudeM({
geoAltM: geoAltitudeM,
baroAltM: baroAltitudeM,
onGround,
surfaceM: null,
geoidN,
});
// pickRenderAltitudeM returns the sentinel `null` only when NEITHER
// alt_geom nor alt_baro was ever reported for this aircraft (not even
// stickily) — fall back to the SAME existing default policy `altitudeM`
// already uses (which also carries the on-ground 0 m / last-known-baro
// case), so the two never disagree on the "no data yet" case.
let renderAltitudeM = pickedAltM != null ? pickedAltM : altitudeM;
// Field-test fix (RS46 heli-in-hillside, 2026-07-06): a baro-only
// AIRBORNE contact near steep terrain can compute a render height
// BELOW the local surface (no alt_geom; baro+N carries QNH error
// larger than the height above ground). Floor it at the coarse-grid
// ellipsoidal ground (warm-cache read only — the batch warm below
// fills cells for later polls). Grounded contacts are deliberately
// NOT touched: their model rides groundSnap's tileset sample and
// their billboard is depth-test-free (see the surfaceM:null block
// above — same one-shot-invariant reasoning).
if (!onGround && renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M) {
renderAltitudeM = floorAltitudeM(
renderAltitudeM,
cachedGroundFloor(lat, lon),
);
_floorWarmPoints.push({ lat, lon });
} else if (onGround) {
// Grounded contacts: warm the floor cell, and — round 4 — when NO
// 3D model owns this contact's visual, lift the billboard itself
// onto the floor (mesh-first): the R20053 heli sat "straight up in
// the ground" because grounded rows render at the legacy ~0 m.
// With a model present the billboard stays put (it hides behind
// the tileset-snapped model, and moving it would drag groundSnap's
// input past its move-invalidation threshold — the T7 one-shot
// invariant the track regression locks).
_floorWarmPoints.push({ lat, lon });
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24);
if (!modelOwnsVisual) {
const floor = cachedGroundFloor(lat, lon);
if (Number.isFinite(floor)) {
renderAltitudeM = floorAltitudeM(renderAltitudeM, floor);
}
}
}
const position = Cesium.Cartesian3.fromDegrees(
lon,
lat,
renderAltitudeM,
);
// Landing/takeoff transition: the 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);
// Sticky merge — adsb.lol intermittently drops flight/t/r/ownOp; hold
// last-known-good (bounded by the layer's eviction, which deletes the entry).
const stickyType = stickyText(type, prevMeta?.type);
const meta = {
sourceReference: aircraft.reference,
observedReceiptMs: Date.now(),
callsign: stickyText(callsign, prevMeta?.callsign),
type: stickyType,
// Type outranks category automatically inside classifyAircraft.
klass: classifyAircraft({
typeCode: stickyType,
category: aircraft?.category,
}),
registration: stickyText(registration, prevMeta?.registration),
operator: stickyText(operator, prevMeta?.operator),
altitudeFt: stickyNumber(altitudeFt, prevMeta?.altitudeFt, null),
// geoAltitudeM/renderAltitudeM are ADDITIVE fields alongside the
// untouched aviation `altitudeFt` — never rename/replace it (labels,
// the FL readout, and the landed-fast-cull heuristic all still read
// altitudeFt/baro).
geoAltitudeM,
renderAltitudeM,
speedMps: stickyNumber(speedMps, prevMeta?.speedMps, null),
track: stickyNumber(track, prevMeta?.track, null),
// Analyst seam (additive): sticky like the other kinematics.
verticalRateMps: stickyNumber(
verticalRateMps,
prevMeta?.verticalRateMps,
null,
),
lastContactEpochMs: stickyNumber(
aircraft.contactTimeMs,
prevMeta?.lastContactEpochMs,
null,
),
turnRateDps: prevMeta?.turnRateDps || 0,
onGround,
// Round 7: sticky airborne history (see _likelyLanded).
wasAirborne: prevMeta?.wasAirborne === true || !onGround,
// Round 6: lifted occlusion-test point for at/below-ellipsoid
// renders (mirror of flights.js).
cullPosition:
renderAltitudeM < 10
? Cesium.Cartesian3.fromDegrees(lon, lat, 12)
: null,
// Raw poll-fix coords (pre-dead-reckon) — the stale-grounded
// re-floor sweep keys floors off these (mirror of flights.js).
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.
// adsb.lol's seen_pos is the AGE in seconds of the last position
// report, so the fix epoch is receipt time minus that age. Only
// append when the fix actually advances, so stale repeats don't
// create zero-dt segments.
const fixEpochMs = aircraft.positionTimeMs ?? receiptNowMs;
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.speedMps,
track: meta.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 F2)
// so the visible trail keeps growing while tracked. Round 2
// (owner): ground traffic appends too — taxi history stays live
// after touchdown; _appendTrailFix floors grounded positions at
// the surface so the ground leg drapes instead of diving.
if (isTracked) parts.tracking._appendTrailFix(position.clone());
} else {
const modelOwnsGroundVisual =
parts.rendering._modelOwnsVisual(icao24);
if (!modelOwnsGroundVisual) {
liftRepeatedGroundFix(newest, position, meta.onGround);
}
const kinematicsChanged =
newest.velocity !== meta.speedMps || newest.track !== meta.track;
if (kinematicsChanged) {
const synthetic = synthesizeForwardKinematicsFix(newest, {
epochMs: Date.now(),
velocity: meta.speedMps,
track: meta.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);
// Position AND rotation are owned by the fleet pass (_fleetTick);
// course changes land on the next rotation pass (forced below).
// Ground flips (landing/takeoff) restyle this SAME billboard in
// place — the transition is never a removal.
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Per-class GLBs: a class change can mean a different asset OR scale —
// resync the live model, any in-flight load, and the tracked
// standalone (mirror of flights.js).
if (prevMeta?.klass !== meta.klass)
parts.rendering._syncModelToClass(icao24);
// Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff.
} else {
const bb = flightState._billboardCollection.add({
position,
image: aircraftIcon(
parts.rendering._iconKind(icao24, meta.klass),
),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale:
BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta.klass] || 1) *
(meta.onGround ? GROUND_SCALE : 1),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: isTracked ? TRACKED_ICON_COLOR : MIL_ICON_COLOR,
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) {
const info = flightState._flightData.get(icao24);
parts.tracking._updateTrackedLabelModel(icao24);
}
}
// Field-test fix (RS46): one batch warm of the low-airborne floor cells
// collected above — fire-and-forget, single-flight, results read
// synchronously by NEXT poll's clamp (same contract as flights.js's
// grounded-surface warm).
warmGroundFloor(_floorWarmPoints);
// Round 6: re-floor STALE grounded contacts (mirror of flights.js) —
// a parked contact whose feed went quiet froze at its pre-warm height.
// The model-ownership gate matches the live grounded-clamp path (a
// tileset-snapped model owns its visual; moving its billboard would
// disturb groundSnap's input — the T7 one-shot invariant).
for (const [icao24, info] of flightState._flightData) {
if (!info?.onGround || currentIcaos.has(icao24)) continue;
if (parts.rendering._modelOwnsVisual(icao24)) continue;
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon))
continue;
const floor = cachedGroundFloor(info.rawLat, info.rawLon);
if (!Number.isFinite(floor)) continue;
const lifted = floor + GROUND_FLOOR_LIFT_M;
if (
Number.isFinite(info.renderAltitudeM) &&
info.renderAltitudeM >= floor - 1
)
continue;
info.renderAltitudeM = lifted;
info.cullPosition = null;
const liftedPos = Cesium.Cartesian3.fromDegrees(
info.rawLon,
info.rawLat,
lifted,
);
const hist = flightState._positionHistory.get(icao24);
const newest = hist?.[hist.length - 1];
if (newest)
newest.position = Cesium.Cartesian3.clone(
liftedPos,
newest.position,
);
const bbStale = flightState._billboards.get(icao24);
if (bbStale) bbStale.position = liftedPos;
}
// Round 4: sample the RENDERED mesh for those cells (one-shot per cell,
// budget-capped, viewer-proximate, google-3d regime only), excluding
// this layer's own billboards/models from the probe.
{
const viewerCarto =
flightState._viewer?.camera?.positionCartographic || null;
sampleMeshFloorCells(flightState._viewer?.scene, _floorWarmPoints, {
excludeObjects: [
...flightState._billboards.values(),
...flightState._models.values(),
flightState._trackedModel,
].filter(Boolean),
viewerLat: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.latitude)
: undefined,
viewerLon: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.longitude)
: undefined,
});
}
// Remove aircraft only after MISSING_POLL_LIMIT consecutive absences.
// adsb.lol routinely drops aircraft for a single poll; immediate removal
// made icons 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, _buildTrackedLabel appends the STALE cue so
// last-known altitude/speed 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);
}
// Fresh courses arrived — force a rotation pass on the next fleet tick
flightState._lastCamPoseSig = '';
// Feed the shared registry so the flights layer can classify/suppress
registerMilitaryIcaos(currentIcaos);
flightState._count = flightState._billboards.size;
flightState._lastUpdate = receiptNowMs;
flightState._lastTrackingRefreshOutcome = {
const accepted = applySnapshot(snapshot, viewer);
feed._count = accepted.count;
feed._lastUpdate = snapshot.observedAtMs;
feed._lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch,
status: 'accepted',
ids: currentIcaos,
source: flightState._lastSource,
ids: accepted.ids,
source: feed._lastSource,
};
console.log(`[Data:Military] Updated: ${flightState._count} aircraft`);
parts.tracking._applyPendingTrackingRestore();
console.log(`[Data:Military] Updated: ${feed._count} aircraft`);
applyPendingTrackingRestore();
} catch (e) {
if (updateSignal.aborted || e?.name === 'AbortError') {
throw new DOMException('Military update aborted', 'AbortError');
}
console.warn('[Data:Military] 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 an independent military source lifetime and status. */
export function createMilitaryFeed(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._trackingRefreshEpoch = 0;
feed._lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
ids: new Set(),
source: feed._lastSource,
};
return feed;
}
+68
View File
@@ -0,0 +1,68 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createIngestion, createMilitaryFeed } from './ingestion.js';
function setup(source) {
const feed = createMilitaryFeed(source);
const accepted = [];
const labels = [];
const restores = [];
const { methods } = createIngestion({
feed,
applySnapshot: (snapshot) => {
accepted.push(snapshot);
return { count: 1, ids: new Set(['aaa001']) };
},
setSourceLabel: (label) => labels.push(label),
applyPendingTrackingRestore: () => restores.push(true),
});
return { feed, accepted, labels, restores, update: methods.update };
}
test('military acquisition cancels late replies without publishing or restoring tracking', 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('military acquisition keeps the degraded source reason and respects retry admission', async () => {
let calls = 0;
const probe = setup({
async getSnapshot() {
calls++;
return {
records: [],
source: 'Fixture',
observedAtMs: 1234,
freshness: 'stale',
stale: true,
reason: 'Upstream refreshing',
};
},
});
await probe.update(null);
assert.equal(probe.feed._lastUpdate, 1234);
assert.equal(probe.feed._lastError, 'Upstream refreshing');
assert.equal(probe.feed._backoff, true);
assert.deepEqual(probe.labels, ['Fixture']);
probe.feed._retryAt = Date.now() + 60000;
await probe.update(null);
assert.equal(calls, 1);
assert.equal(probe.restores.length, 1);
assert.equal(
probe.feed._lastTrackingRefreshOutcome.status,
'source-unavailable',
);
});
+24 -21
View File
@@ -22,9 +22,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;
},
/**
@@ -35,7 +36,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();
@@ -72,17 +73,18 @@ 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.feed._count = 0;
flightState.feed._lastUpdate = null;
flightState.feed._backoff = false;
flightState.feed._retryAt = 0;
flightState.feed._lastError = null;
flightState.feed._lastStatus = null;
flightState._trackedIcao = null;
parts.tracking._resetTrackedSelectionState();
flightState._trackedEntity = null;
@@ -122,11 +124,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 */
@@ -274,23 +276,24 @@ 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();
flightState._missingPolls.clear();
flightState._count = 0;
flightState._lastUpdate = null;
flightState.records.missingPolls.clear();
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,
source: flightState.feed._lastSource,
};
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
flightState._viewer = null;
+8 -8
View File
@@ -55,7 +55,7 @@ export function createMotion({
function _deadReckon(icao24, result) {
const history = flightState._positionHistory.get(icao24);
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
if (!history || history.length === 0) {
flightState._drCourseDeg = null;
flightState._drSpeedMps = null;
@@ -229,7 +229,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,
@@ -256,7 +256,7 @@ export function createMotion({
const scale =
billboard?.scale?.getValue(time) ??
BILLBOARD_SCALE *
(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;
@@ -299,7 +299,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.speedMps) || 0;
@@ -380,7 +380,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,
@@ -423,7 +423,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
@@ -440,7 +440,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
@@ -466,7 +466,7 @@ export function createMotion({
* same aircraft can never disagree across the handoff (mirror of flights.js). */
function _trackedDisplayCourse() {
const info = flightState._flightData.get(flightState._trackedIcao);
const info = flightState.records.data.get(flightState._trackedIcao);
const fallback = (info && info.track) || 0;
const cacheValid =
flightState._drReconcileValid &&
+9 -2
View File
@@ -61,17 +61,24 @@ test('military state owns separate contact maps, motion scratch and ground sampl
const first = createFlightState({ services: services() });
const second = createFlightState({ services: services() });
for (const key of [
'_flightData',
'records',
'feed',
'_billboards',
'_positionHistory',
'_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('a normalized source can retain its stale reason without changing standalone cache policy', async () => {
+7 -10
View File
@@ -1,3 +1,10 @@
export {
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';
/**
@@ -19,12 +26,8 @@ import * as Cesium from 'cesium';
/** @constant {number} Milliseconds to wait before retrying after a transient error */
export const ERROR_BACKOFF_INTERVAL = 20000;
/** @constant {number} Max position samples retained per aircraft for dead reckoning */
export const POSITION_HISTORY_LIMIT = 5;
/** @constant {number} Base billboard display scale */
export const BILLBOARD_SCALE = 0.7;
@@ -186,8 +189,6 @@ export const RENDER_DELAY_SEC = 15;
/** @constant {number} Polls an aircraft may miss before removal (transient adsb.lol dropouts). */
export const MISSING_POLL_LIMIT = 3;
// --- Landed-plane fast cull (mirror of flights.js; owner field report
// 2026-07-02: "phantom" planes lingered ~2 min at airports after touchdown).
// The feed's ground flag lags the actual landing, so a landed plane's last
@@ -207,8 +208,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 fix (RS46, 2026-07-06): only contacts rendering below this
// ellipsoidal height get the below-ground floor clamp + a coarse floor-cell
// warm. Terrain outside the extreme Himalaya tops out well under this, so
@@ -217,8 +216,6 @@ export const LANDED_MISSING_POLL_LIMIT = 1;
// gets the floor.
/** @constant {number} Max render altitude (m, ellipsoidal) eligible for the ground-floor clamp. */
export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500;
// ---------------------------------------------------------------------------
// Nadir-stable icon orientation (mirrors flights.js): surface-normal alignment
// degenerates when the camera looks straight down (the normal is parallel to
+25 -21
View File
@@ -67,7 +67,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 (mirror of flights.js): fast cull only for contacts seen
// AIRBORNE this session — parked contacts ride the normal grace so feed
@@ -91,7 +91,7 @@ export function createQueries({
*/
function _describeFlight(icao24) {
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);
@@ -130,7 +130,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,
),
};
}
@@ -199,7 +200,7 @@ export function createQueries({
icon: '🎖️',
source: flightState._lastSource,
source: flightState.feed._lastSource,
/** @type {number} Polling interval in ms between API fetches */
updateInterval: 15000,
@@ -362,7 +363,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);
nearby.push({
id: info?.callsign?.trim() || info?.registration?.trim() || icao24,
icao24,
@@ -434,7 +435,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);
const callsign = info?.callsign?.trim();
const registration = info?.registration?.trim();
let object = flightState._detectionObjects.get(icao24);
@@ -506,7 +507,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()
@@ -524,7 +525,7 @@ export function createQueries({
// cannot disagree, and it is strictly tiered so a registration can never
// out-rank a real callsign on feed order alone.
let best = null;
for (const [icao24, info] of flightState._flightData) {
for (const [icao24, info] of flightState.records.data) {
const candidate = {
tier: rankContactMatch({
query: q,
@@ -595,7 +596,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/Context resolve this)
// Last surface still reading as the raw hex for a callsign-less
@@ -626,14 +627,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) {
result.push(mapAnalystRecord(icao24, info));
if (result.length >= limit) break;
}
@@ -672,11 +673,11 @@ export function createQueries({
.trim()
.toLowerCase();
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',
reason: `${flightState._lastSource} snapshot unavailable`,
reason: `${flightState.feed._lastSource} snapshot unavailable`,
refreshEpoch: outcome.epoch,
source: outcome.source,
};
@@ -790,17 +791,20 @@ 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,
source: flightState.feed._lastSource,
fallback: false,
};
},
+9
View File
@@ -0,0 +1,9 @@
export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500;
export const POSITION_HISTORY_LIMIT = 5;
export const LANDED_MISSING_POLL_LIMIT = 1;
export const MISSING_POLL_LIMIT = 3;
export const ERROR_BACKOFF_INTERVAL = 20000;
+201
View File
@@ -0,0 +1,201 @@
import { pickRenderAltitudeM } from '../../data/renderAltitude.js';
import { stickyText, stickyNumber } from '../../data/aircraftMeta.js';
import { classifyAircraft } from '../../data/aircraftClass.js';
import {
GROUND_FLOOR_WARM_MAX_ALT_M,
LANDED_MISSING_POLL_LIMIT,
MISSING_POLL_LIMIT,
} from './recordPolicy.js';
/** Portable military aircraft metadata and bounded missing-poll retention. */
export class MilitaryFlightRecords {
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(
aircraft,
{ observedAtMs, floorWarmPoints: _floorWarmPoints, modelOwnsVisual },
) {
const { geoidHeight, cachedGroundFloor, floorAltitudeM } = this.services;
const { id: icao24, longitude: lon, latitude: lat } = aircraft;
this.missingPolls.delete(icao24);
const prevMeta = this.data.get(icao24);
// adsb.lol/readsb reports GROUND traffic as alt_baro === "ground" (no
// separate boolean). Grounded planes fall back to their last known
// altitude (field elevation is unknowable here), else 0 m — never the
// 3 km airborne default (a parked plane must not float).
const onGround = aircraft.onGround;
const altitudeFt =
aircraft.baroAltitudeM == null ? null : aircraft.baroAltitudeM / 0.3048;
const altitudeM =
aircraft.baroAltitudeM ??
(onGround
? Number.isFinite(prevMeta?.altitudeFt)
? prevMeta.altitudeFt * 0.3048
: 0
: 3048);
const track = aircraft.courseDeg || 0;
const speedMps = aircraft.speedMps ?? 0;
const verticalRateMps = aircraft.verticalRateMps;
const callsign = aircraft.callsign;
const type = aircraft.typeCode;
const registration = aircraft.registration;
const operator = aircraft.operator;
const geoAltitudeM = aircraft.ellipsoidAltitudeM;
const baroAltitudeM = aircraft.baroAltitudeM;
// 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);
}
// GROUND-SNAP INTERPLAY (brief item 3 — "don't double-correct"): a
// grounded plane's MODEL already rides groundSnap.js's one-shot tileset
// sample (_modelDisplayPosition), which is the visual on the ground, and
// its billboard is depth-test-free (_groundDepthDistance) so its exact
// height is cosmetic. Deliberately pass surfaceM=null so
// pickRenderAltitudeM's on-ground surface branch never fires here:
// 1. It would be the SECOND correction of the same grounded plane
// (model tileset-snap is the first) — the exact double-correct the
// brief forbids.
// 2. Military ground rows carry NO baro ("alt_baro":"ground"), so the
// grounded billboard sits at 0 m until a surface value warms; letting
// surfaceM then jump it 0 -> ~surface (often ~100 m) BETWEEN polls
// drags the model's ground-snap input past groundSnap's 50 m
// move-invalidation threshold and forces a re-sample every time the
// cache warms — breaking the ONE-SHOT-per-(camera,regime) invariant
// the track regression locks (qa: sampleHeight count must stay flat).
// Grounded planes therefore keep the pre-existing `altitudeM` default
// (last-known baro / 0). The datum fix (alt_geom -> baro+geoidN) is what
// matters for AIRBORNE military planes — the actual "renders at MSL" bug.
const pickedAltM = pickRenderAltitudeM({
geoAltM: geoAltitudeM,
baroAltM: baroAltitudeM,
onGround,
surfaceM: null,
geoidN,
});
// pickRenderAltitudeM returns the sentinel `null` only when NEITHER
// alt_geom nor alt_baro was ever reported for this aircraft (not even
// stickily) — fall back to the SAME existing default policy `altitudeM`
// already uses (which also carries the on-ground 0 m / last-known-baro
// case), so the two never disagree on the "no data yet" case.
let renderAltitudeM = pickedAltM != null ? pickedAltM : altitudeM;
// Field-test fix (RS46 heli-in-hillside, 2026-07-06): a baro-only
// AIRBORNE contact near steep terrain can compute a render height
// BELOW the local surface (no alt_geom; baro+N carries QNH error
// larger than the height above ground). Floor it at the coarse-grid
// ellipsoidal ground (warm-cache read only — the batch warm below
// fills cells for later polls). Grounded contacts are deliberately
// NOT touched: their model rides groundSnap's tileset sample and
// their billboard is depth-test-free (see the surfaceM:null block
// above — same one-shot-invariant reasoning).
if (!onGround && renderAltitudeM < GROUND_FLOOR_WARM_MAX_ALT_M) {
renderAltitudeM = floorAltitudeM(
renderAltitudeM,
cachedGroundFloor(lat, lon),
);
_floorWarmPoints.push({ lat, lon });
} else if (onGround) {
// Grounded contacts: warm the floor cell, and — round 4 — when NO
// 3D model owns this contact's visual, lift the billboard itself
// onto the floor (mesh-first): the R20053 heli sat "straight up in
// the ground" because grounded rows render at the legacy ~0 m.
// With a model present the billboard stays put (it hides behind
// the tileset-snapped model, and moving it would drag groundSnap's
// input past its move-invalidation threshold — the T7 one-shot
// invariant the track regression locks).
_floorWarmPoints.push({ lat, lon });
if (!modelOwnsVisual) {
const floor = cachedGroundFloor(lat, lon);
if (Number.isFinite(floor)) {
renderAltitudeM = floorAltitudeM(renderAltitudeM, floor);
}
}
}
// Landing/takeoff transition: the ground flip restyles IN PLACE.
const groundFlipped =
!!prevMeta && (prevMeta.onGround === true) !== onGround;
// Sticky merge — adsb.lol intermittently drops flight/t/r/ownOp; hold
// last-known-good (bounded by the layer's eviction, which deletes the entry).
const stickyType = stickyText(type, prevMeta?.type);
const meta = {
sourceReference: aircraft.reference,
observedReceiptMs: Date.now(),
callsign: stickyText(callsign, prevMeta?.callsign),
type: stickyType,
// Type outranks category automatically inside classifyAircraft.
klass: classifyAircraft({
typeCode: stickyType,
category: aircraft?.category,
}),
registration: stickyText(registration, prevMeta?.registration),
operator: stickyText(operator, prevMeta?.operator),
altitudeFt: stickyNumber(altitudeFt, prevMeta?.altitudeFt, null),
// geoAltitudeM/renderAltitudeM are ADDITIVE fields alongside the
// untouched aviation `altitudeFt` — never rename/replace it (labels,
// the FL readout, and the landed-fast-cull heuristic all still read
// altitudeFt/baro).
geoAltitudeM,
renderAltitudeM,
speedMps: stickyNumber(speedMps, prevMeta?.speedMps, null),
track: stickyNumber(track, prevMeta?.track, null),
// Analyst seam (additive): sticky like the other kinematics.
verticalRateMps: stickyNumber(
verticalRateMps,
prevMeta?.verticalRateMps,
null,
),
lastContactEpochMs: stickyNumber(
aircraft.contactTimeMs,
prevMeta?.lastContactEpochMs,
null,
),
turnRateDps: prevMeta?.turnRateDps || 0,
onGround,
// Round 7: sticky airborne history (see _likelyLanded).
wasAirborne: prevMeta?.wasAirborne === true || !onGround,
// Raw poll-fix coords (pre-dead-reckon) — the stale-grounded
// re-floor sweep keys floors off these (mirror of flights.js).
rawLat: lat,
rawLon: lon,
};
this.data.set(icao24, meta);
return {
prevMeta,
meta,
groundFlipped,
fixEpochMs: aircraft.positionTimeMs ?? observedAtMs,
};
}
absence(id, { complete, likelyLanded }) {
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);
}
}
+100
View File
@@ -0,0 +1,100 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { MilitaryFlightRecords } from './records.js';
const observation = (fields = {}) => ({
id: 'aaa001',
reference: 'aaa001',
longitude: -77,
latitude: 39,
baroAltitudeM: 1000,
ellipsoidAltitudeM: null,
onGround: false,
callsign: 'MIL1',
typeCode: 'C17',
speedMps: 100,
courseDeg: 90,
verticalRateMps: 1,
positionTimeMs: 1700000000000,
...fields,
});
const context = (fields = {}) => ({
observedAtMs: 1700000001000,
floorWarmPoints: [],
modelOwnsVisual: false,
...fields,
});
const store = () =>
new MilitaryFlightRecords({
geoidHeight: () => 40,
cachedGroundFloor: () => 200,
floorAltitudeM: (height, floor) => Math.max(height, floor + 1.5),
});
test('military records preserve feet for aviation, metres for rendering and source epochs', () => {
const records = store();
records.geoidReady = true;
const first = records.receive(observation(), context());
assert.equal(first.meta.altitudeFt, 1000 / 0.3048);
assert.equal(first.meta.renderAltitudeM, 1040);
assert.equal(first.fixEpochMs, 1700000000000);
const next = records.receive(
observation({ callsign: '', typeCode: '', positionTimeMs: null }),
context(),
);
assert.equal(next.meta.callsign, 'MIL1');
assert.equal(next.meta.type, 'C17');
assert.equal(next.fixEpochMs, 1700000001000);
assert.equal(Object.hasOwn(next.meta, 'cullPosition'), false);
});
test('military ground records leave model-owned placement alone and warm the same floor cell', () => {
const records = store();
const owned = context({ modelOwnsVisual: true });
const first = records.receive(
observation({ onGround: true, baroAltitudeM: null }),
owned,
);
assert.equal(first.meta.renderAltitudeM, 0);
assert.deepEqual(owned.floorWarmPoints, [{ lat: 39, lon: -77 }]);
const unowned = context();
const second = records.receive(
observation({ onGround: true, baroAltitudeM: null }),
unowned,
);
assert.equal(second.meta.renderAltitudeM, 201.5);
assert.deepEqual(unowned.floorWarmPoints, owned.floorWarmPoints);
});
test('military records keep bounded partial retention and release metadata only after removal admission', () => {
const first = store();
const second = store();
const { meta } = first.receive(observation(), context());
assert.equal(second.data.size, 0);
assert.equal(
first.absence('aaa001', { complete: false, likelyLanded: true }),
'retain',
);
meta.observedReceiptMs = Date.now() - 300001;
assert.equal(
first.absence('aaa001', { complete: false, likelyLanded: false }),
'stale',
);
assert.equal(
first.absence('aaa001', { complete: true, likelyLanded: false }),
'stale',
);
assert.equal(
first.absence('aaa001', { complete: true, likelyLanded: false }),
'remove',
);
assert.equal(first.data.get('aaa001'), meta);
first.forget('aaa001');
assert.equal(first.data.size, 0);
assert.equal(first.geoidNCache.size, 0);
assert.equal(first.missingPolls.size, 0);
first.receive(observation(), context());
assert.equal(
first.absence('aaa001', { complete: true, likelyLanded: true }),
'remove',
);
});
+17 -17
View File
@@ -133,7 +133,7 @@ export function createRendering({
}
function _militaryBillboardScale(icao24) {
const meta = flightState._flightData.get(icao24);
const meta = flightState.records.data.get(icao24);
return (
BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta?.klass] || 1) *
@@ -162,7 +162,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,
@@ -280,7 +280,7 @@ export function createRendering({
* outage inside groundSnap's drift bound, so taxiing does not pop it back to 2D. */
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,
@@ -332,7 +332,7 @@ export function createRendering({
displayPos,
course,
model.modelMatrix,
_modelSpec(flightState._flightData.get(icao24)?.klass).headingOffsetDeg,
_modelSpec(flightState.records.data.get(icao24)?.klass).headingOffsetDeg,
);
if (!model.ready) {
model.show = false; // not loaded yet → keep the 2D icon, no half-model flash
@@ -380,7 +380,7 @@ export function createRendering({
* fleet billboard FIRST (gap-proof) and covering the tracked standalone. */
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) ||
@@ -450,10 +450,10 @@ export function createRendering({
// Spec identity captured at load START (mirror of flights.js): a mid-load
// reclassification makes the post-await admission reject the stale asset.
// Boost state likewise — creation options bake it in.
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,
@@ -498,12 +498,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) {
@@ -651,10 +651,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({
@@ -691,7 +691,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
) {
@@ -760,7 +760,7 @@ export function createRendering({
return;
}
const spec = _modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass,
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
_modelMatrix(
displayPos,
@@ -933,7 +933,7 @@ export function createRendering({
// ellipsoid (mirror of flights.js — sub-ellipsoid points near the limb
// read "beyond the horizon" and would hide low contacts awaiting floors).
const beyondHorizon = !occluder.isPointVisible(
flightState._flightData.get(icao24)?.cullPosition || bb.position,
flightState.records.data.get(icao24)?.cullPosition || bb.position,
);
// A billboard flipping INTO view (horizon reveal while the camera idles)
// gets its rotation refreshed THIS tick even without a pose change —
@@ -950,7 +950,7 @@ export function createRendering({
continue;
}
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
// One sprite-owned write site composes freshness × focus × limb haze and
// base class/ground scale × limb taper. The locked NearFarScalar remains
@@ -983,7 +983,7 @@ export function createRendering({
flightState._cockpitContactMode && !isCockpitNear
? 1
: _militaryBillboardScale(icao24),
baseAlpha: flightState._missingPolls.get(icao24) ? 0.45 : 1,
baseAlpha: flightState.records.missingPolls.get(icao24) ? 0.45 : 1,
baseColor: MIL_ICON_COLOR,
focusFactor: focus.factor,
cameraDistanceM,
@@ -1003,7 +1003,7 @@ export function createRendering({
if (wantLarge !== !!bb._gevIconLarge) {
bb._gevIconLarge = wantLarge;
bb.image = aircraftIcon(
_iconKind(icao24, flightState._flightData.get(icao24)?.klass),
_iconKind(icao24, flightState.records.data.get(icao24)?.klass),
wantLarge ? TRACKED_ICON_PX : undefined,
);
}
+295
View File
@@ -0,0 +1,295 @@
import * as Cesium from 'cesium';
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
import {
turnRateFromFixHistory,
liftRepeatedGroundFix,
synthesizeForwardKinematicsFix,
} from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import { POSITION_HISTORY_LIMIT } from './recordPolicy.js';
import {
BILLBOARD_SCALE,
GROUND_SCALE,
TRACKED_ICON_COLOR,
MIL_ICON_COLOR,
} from './policy.js';
/** Apply military record changes to Cesium resources and existing follow operations. */
export function createMilitarySnapshotRenderer({
flightState,
records,
groundFloor,
meshFloor,
militaryRegistry,
rendering,
tracking,
queries,
}) {
const { warmGroundFloor, cachedGroundFloor, GROUND_FLOOR_LIFT_M } =
groundFloor;
const { sampleMeshFloorCells } = meshFloor;
const { registerMilitaryIcaos } = militaryRegistry;
return function applySnapshot(snapshot, viewer) {
const currentIcaos = new Set();
const receiptNowMs = snapshot.observedAtMs;
// Field-test fix (RS46): coarse floor cells to warm for the below-ground
// clamp — collected during the loop (low airborne contacts only),
// batch-resolved once after it. Never a fetch inside the loop.
const _floorWarmPoints = [];
for (const aircraft of snapshot.records) {
const icao24 = aircraft.id;
currentIcaos.add(icao24);
const { prevMeta, meta, groundFlipped, fixEpochMs } = records.receive(
aircraft,
{
observedAtMs: receiptNowMs,
floorWarmPoints: _floorWarmPoints,
modelOwnsVisual: aircraft.onGround
? rendering._modelOwnsVisual(icao24)
: false,
},
);
const position = Cesium.Cartesian3.fromDegrees(
meta.rawLon,
meta.rawLat,
meta.renderAltitudeM,
);
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.
// adsb.lol's seen_pos is the AGE in seconds of the last position
// report, so the fix epoch is receipt time minus that age. 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.speedMps,
track: meta.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 F2)
// so the visible trail keeps growing while tracked. Round 2
// (owner): ground traffic appends too — taxi history stays live
// after touchdown; _appendTrailFix floors grounded positions at
// the surface so the ground leg drapes instead of diving.
if (isTracked) tracking._appendTrailFix(position.clone());
} else {
const modelOwnsGroundVisual = rendering._modelOwnsVisual(icao24);
if (!modelOwnsGroundVisual) {
liftRepeatedGroundFix(newest, position, meta.onGround);
}
const kinematicsChanged =
newest.velocity !== meta.speedMps || newest.track !== meta.track;
if (kinematicsChanged) {
const synthetic = synthesizeForwardKinematicsFix(newest, {
epochMs: Date.now(),
velocity: meta.speedMps,
track: meta.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);
// Position AND rotation are owned by the fleet pass (_fleetTick);
// course changes land on the next rotation pass (forced below).
// Ground flips (landing/takeoff) restyle this SAME billboard in
// place — the transition is never a removal.
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Per-class GLBs: a class change can mean a different asset OR scale —
// resync the live model, any in-flight load, and the tracked
// standalone (mirror of flights.js).
if (prevMeta?.klass !== meta.klass) rendering._syncModelToClass(icao24);
// Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff.
} else {
const bb = flightState._billboardCollection.add({
position,
image: aircraftIcon(rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale:
BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta.klass] || 1) *
(meta.onGround ? GROUND_SCALE : 1),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: isTracked ? TRACKED_ICON_COLOR : MIL_ICON_COLOR,
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) {
const info = flightState.records.data.get(icao24);
tracking._updateTrackedLabelModel(icao24);
}
}
// Field-test fix (RS46): one batch warm of the low-airborne floor cells
// collected above — fire-and-forget, single-flight, results read
// synchronously by NEXT poll's clamp (same contract as flights.js's
// grounded-surface warm).
warmGroundFloor(_floorWarmPoints);
// Round 6: re-floor STALE grounded contacts (mirror of flights.js) —
// a parked contact whose feed went quiet froze at its pre-warm height.
// The model-ownership gate matches the live grounded-clamp path (a
// tileset-snapped model owns its visual; moving its billboard would
// disturb groundSnap's input — the T7 one-shot invariant).
for (const [icao24, info] of flightState.records.data) {
if (!info?.onGround || currentIcaos.has(icao24)) continue;
if (rendering._modelOwnsVisual(icao24)) continue;
if (!Number.isFinite(info.rawLat) || !Number.isFinite(info.rawLon))
continue;
const floor = cachedGroundFloor(info.rawLat, info.rawLon);
if (!Number.isFinite(floor)) continue;
const lifted = floor + GROUND_FLOOR_LIFT_M;
if (
Number.isFinite(info.renderAltitudeM) &&
info.renderAltitudeM >= floor - 1
)
continue;
info.renderAltitudeM = lifted;
flightState._cullPositions.delete(icao24);
const liftedPos = Cesium.Cartesian3.fromDegrees(
info.rawLon,
info.rawLat,
lifted,
);
const hist = flightState._positionHistory.get(icao24);
const newest = hist?.[hist.length - 1];
if (newest)
newest.position = Cesium.Cartesian3.clone(liftedPos, newest.position);
const bbStale = flightState._billboards.get(icao24);
if (bbStale) bbStale.position = liftedPos;
}
// Round 4: sample the RENDERED mesh for those cells (one-shot per cell,
// budget-capped, viewer-proximate, google-3d regime only), excluding
// this layer's own billboards/models from the probe.
{
const viewerCarto =
flightState._viewer?.camera?.positionCartographic || null;
sampleMeshFloorCells(flightState._viewer?.scene, _floorWarmPoints, {
excludeObjects: [
...flightState._billboards.values(),
...flightState._models.values(),
flightState._trackedModel,
].filter(Boolean),
viewerLat: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.latitude)
: undefined,
viewerLon: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.longitude)
: undefined,
});
}
// Remove aircraft only after MISSING_POLL_LIMIT consecutive absences.
// adsb.lol routinely drops aircraft for a single poll; immediate removal
// made icons 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, _buildTrackedLabel appends the STALE cue so
// last-known altitude/speed 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);
}
// Fresh courses arrived — force a rotation pass on the next fleet tick
flightState._lastCamPoseSig = '';
// Feed the shared registry so the flights layer can classify/suppress
registerMilitaryIcaos(currentIcaos);
return { count: flightState._billboards.size, ids: currentIcaos };
};
}
+8 -55
View File
@@ -1,10 +1,16 @@
import { MilitaryFlightRecords } from './records.js';
import { createMilitaryFeed } from './ingestion.js';
import * as Cesium from 'cesium';
export function createFlightState({ source, services }) {
const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() };
flightState._source = source;
flightState.feed = createMilitaryFeed(source);
flightState.records = new MilitaryFlightRecords({
...services.geoid,
...services.groundFloor,
});
flightState._cullPositions = new Map();
/** Per-class model spec for THIS layer (2026-08-16, owner playtest ask:
* military contacts should read as their WEIGHT CLASS, always in this layer's
@@ -102,50 +108,10 @@ export function createFlightState({ source, services }) {
flightState._detectionObjects = new Map();
/** @type {Map<string, Object>} ICAO hex -> flight metadata (callsign, type, registration, operator, altitudeFt, speedMps, track) */
flightState._flightData = new Map();
/** @type {Map<string, Array<{time: Cesium.JulianDate, position: Cesium.Cartesian3}>>} ICAO hex -> recent position samples for dead reckoning */
flightState._positionHistory = 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 when in error-backoff mode */
flightState._backoff = false;
/** @type {number} Epoch ms after which the next retry is allowed */
flightState._retryAt = 0;
/** @type {string|null} Human-readable description of the last error */
flightState._lastError = null;
flightState._activeUpdateControllers = new Set();
/** @type {number|null} HTTP status code from the last API response */
flightState._lastStatus = null;
flightState._lastSource = source?.label || 'Aircraft';
/** @type {boolean} True once ensureGeoidReady() has resolved (awaited once at enable()). Mirror of flights.js. */
flightState._geoidReady = false;
/** @type {Map<string, number>} icao24 -> geoid undulation N (m), cached (negligible drift per-aircraft). */
flightState._geoidNCache = new Map();
// -- Click-to-track state --
/** @type {string|null} ICAO hex of the currently tracked aircraft */
@@ -155,15 +121,6 @@ export function createFlightState({ source, services }) {
flightState._trackingIntentGeneration = 0;
flightState._trackingRefreshEpoch = 0;
flightState._lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
ids: new Set(),
source: flightState._lastSource,
};
/** @type {Cesium.Entity|null} Entity created for the tracked aircraft (camera follows this) */
flightState._trackedEntity = null;
@@ -226,10 +183,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;
+10 -9
View File
@@ -40,14 +40,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;
@@ -57,8 +58,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. */
@@ -66,10 +67,10 @@ export function createTesting({
function _setMilitaryTrackingRefreshOutcomeForTest({
status = 'accepted',
ids = [],
source = flightState._lastSource,
source = flightState.feed._lastSource,
} = {}) {
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())),
@@ -86,7 +87,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);
}
+15 -13
View File
@@ -61,7 +61,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;
@@ -90,13 +90,13 @@ export function createTracking({
function _contextSubjectMetadata(icao24) {
const described = parts.queries._describeFlight(icao24);
if (!described) return null;
const info = flightState._flightData.get(icao24);
const info = flightState.records.data.get(icao24);
const label = described.callsign || described.registration || icao24;
return {
id: icao24,
layerId: 'military',
layerName: 'Military Flights',
source: flightState._lastSource,
source: flightState.feed._lastSource,
label,
latitude: described.latitude,
longitude: described.longitude,
@@ -218,7 +218,7 @@ export function createTracking({
function _buildTrackedLabel(info, icao24) {
const stale =
flightState._missingPolls.get(icao24) || flightState._backoff
flightState.records.missingPolls.get(icao24) || flightState.feed._backoff
? ' · STALE'
: '';
const callsign =
@@ -251,7 +251,7 @@ export function createTracking({
if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao)
return;
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
_buildTrackedLabel(flightState._flightData.get(icao24), icao24),
_buildTrackedLabel(flightState.records.data.get(icao24), icao24),
'#ffd166',
);
refreshTrackedReadout(flightState._trackedEntity);
@@ -268,7 +268,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,
);
@@ -606,8 +606,8 @@ export function createTracking({
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
let trace = 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,
@@ -775,7 +775,7 @@ export function createTracking({
// ROTATION_REFRESH_MS of a wrong (possibly reversed) nose on release.
if (flightState._billboards.has(flightState._trackedIcao)) {
const bb = flightState._billboards.get(flightState._trackedIcao);
const meta = flightState._flightData.get(flightState._trackedIcao);
const meta = flightState.records.data.get(flightState._trackedIcao);
bb.show = true;
bb.width = 20;
bb.height = 20;
@@ -855,7 +855,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;
@@ -908,7 +908,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,
),
@@ -917,7 +917,7 @@ export function createTracking({
scale:
BILLBOARD_SCALE *
(CLASS_SCALE_2D[
flightState._flightData.get(flightState._trackedIcao)?.klass
flightState.records.data.get(flightState._trackedIcao)?.klass
] || 1),
// Solid amber 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).
@@ -939,7 +939,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;