Merge pull request #499 from bilawalsidhu/refactor/military-flight-records

Separate military-flight records and rendering
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-09-14 16:23:02 -07:00
commit 852008df4d
23 files changed
+928 -665

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# Changelog
- Separate military-flight records and acquisition from rendering while preserving ground-model ownership, source units and follow behavior.
- 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.
+6
View File
@@ -491,3 +491,9 @@ 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.
`layers/military/records` and `layers/military/ingestion` have the same portable
ownership boundary. Military observations retain their aviation-foot readout,
source-time fallback and model-owned ground policy. The renderer supplies that
ownership fact and handles stale-ground lifting, Cesium history and primitives;
record reconciliation does not import the engine or application.
+5
View File
@@ -1,5 +1,10 @@
# God's Eye View Current State
Military flights also separate portable records and source acquisition from
Cesium snapshot application. The metadata owner retains aviation feet, source
fix times, sticky fields and partial-feed retention. Rendering retains ground
model ownership, motion history, military registration and tracking cleanup.
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
+3 -1
View File
@@ -202,6 +202,8 @@
"./ui/visual-settings": "./src/ui/visualSettings.js",
"./ui/panel-chrome": "./src/ui/panelChrome.js",
"./layers/flights/records": "./src/layers/flights/records.js",
"./layers/flights/ingestion": "./src/layers/flights/ingestion.js"
"./layers/flights/ingestion": "./src/layers/flights/ingestion.js",
"./layers/military/records": "./src/layers/military/records.js",
"./layers/military/ingestion": "./src/layers/military/ingestion.js"
}
}
+6 -1
View File
@@ -600,5 +600,10 @@
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js",
"src/layers/flights/records.test.mjs",
"src/layers/flights/ingestion.test.mjs"
"src/layers/flights/ingestion.test.mjs",
"src/layers/military/records.js",
"src/layers/military/recordPolicy.js",
"src/layers/military/snapshotRenderer.js",
"src/layers/military/records.test.mjs",
"src/layers/military/ingestion.test.mjs"
]
+31 -3
View File
@@ -657,7 +657,10 @@
"src/layers/military/rendering.js",
"src/layers/military/state.js",
"src/layers/military/testing.js",
"src/layers/military/tracking.js"
"src/layers/military/tracking.js",
"src/layers/military/records.js",
"src/layers/military/recordPolicy.js",
"src/layers/military/snapshotRenderer.js"
],
"external": ["cesium"]
},
@@ -1545,7 +1548,10 @@
"src/worldFocus.js",
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js"
"src/layers/flights/snapshotRenderer.js",
"src/layers/military/records.js",
"src/layers/military/recordPolicy.js",
"src/layers/military/snapshotRenderer.js"
],
"external": [
"@mapbox/vector-tile",
@@ -1872,7 +1878,10 @@
"src/worldFocus.js",
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js"
"src/layers/flights/snapshotRenderer.js",
"src/layers/military/records.js",
"src/layers/military/recordPolicy.js",
"src/layers/military/snapshotRenderer.js"
],
"external": [
"cesium",
@@ -2005,5 +2014,24 @@
"src/layers/flights/recordPolicy.js"
],
"external": []
},
"military-flight-records": {
"exports": ["./layers/military/records"],
"modules": [
"src/layers/military/records.js",
"src/layers/military/recordPolicy.js",
"src/data/aircraftMeta.js",
"src/data/renderAltitude.js",
"src/data/aircraftClass.js"
],
"external": []
},
"military-flight-ingestion": {
"exports": ["./layers/military/ingestion"],
"modules": [
"src/layers/military/ingestion.js",
"src/layers/military/recordPolicy.js"
],
"external": []
}
}
+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;