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 # 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 civil-flight acquisition and record reconciliation from Cesium resource updates, preserving source timing, ground placement and tracking behavior.
- Separate navigation, share restoration, visual settings and panel state into focused UI owners with explicit dependencies and terminal cleanup. - Separate navigation, share restoration, visual settings and panel state into focused UI owners with explicit dependencies and terminal cleanup.
+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 before deleting records; incomplete snapshots retain recent contacts for the
existing bounded interval. The renderer retains its existing per-frame scratch existing bounded interval. The renderer retains its existing per-frame scratch
objects; reconciliation occurs on source refresh, not on each frame. 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 # 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 Civil flights separate source acquisition, portable record reconciliation and
scene application. The record owner keeps sticky metadata, geoid cache and scene application. The record owner keeps sticky metadata, geoid cache and
bounded missing-poll state; ingestion owns cancellation, source freshness 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/visual-settings": "./src/ui/visualSettings.js",
"./ui/panel-chrome": "./src/ui/panelChrome.js", "./ui/panel-chrome": "./src/ui/panelChrome.js",
"./layers/flights/records": "./src/layers/flights/records.js", "./layers/flights/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/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js", "src/layers/flights/snapshotRenderer.js",
"src/layers/flights/records.test.mjs", "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/rendering.js",
"src/layers/military/state.js", "src/layers/military/state.js",
"src/layers/military/testing.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"] "external": ["cesium"]
}, },
@@ -1545,7 +1548,10 @@
"src/worldFocus.js", "src/worldFocus.js",
"src/layers/flights/records.js", "src/layers/flights/records.js",
"src/layers/flights/recordPolicy.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": [ "external": [
"@mapbox/vector-tile", "@mapbox/vector-tile",
@@ -1872,7 +1878,10 @@
"src/worldFocus.js", "src/worldFocus.js",
"src/layers/flights/records.js", "src/layers/flights/records.js",
"src/layers/flights/recordPolicy.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": [ "external": [
"cesium", "cesium",
@@ -2005,5 +2014,24 @@
"src/layers/flights/recordPolicy.js" "src/layers/flights/recordPolicy.js"
], ],
"external": [] "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, resolveAsset,
}) { }) {
function _abortActiveUpdates() { function _abortActiveUpdates() {
for (const controller of flightState._activeUpdateControllers) for (const controller of flightState.feed._activeUpdateControllers)
controller.abort(); controller.abort();
flightState._activeUpdateControllers.clear(); flightState.feed._activeUpdateControllers.clear();
} }
return { _abortActiveUpdates }; return { _abortActiveUpdates };
} }
+21 -1
View File
@@ -1,3 +1,4 @@
import { createMilitarySnapshotRenderer } from './snapshotRenderer.js';
import { createFlightState } from './state.js'; import { createFlightState } from './state.js';
import { createRendering } from './rendering.js'; import { createRendering } from './rendering.js';
import { createMotion } from './motion.js'; import { createMotion } from './motion.js';
@@ -21,10 +22,29 @@ export function createMilitaryFlightLayer({
parts.motion = createMotion(context); parts.motion = createMotion(context);
parts.tracking = createTracking(context); parts.tracking = createTracking(context);
parts.controller = createController(context); parts.controller = createController(context);
parts.ingestion = createIngestion(context);
parts.lifecycle = createLifecycle(context); parts.lifecycle = createLifecycle(context);
parts.testing = createTesting(context); parts.testing = createTesting(context);
parts.queries = createQueries(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( Object.assign(
layer, layer,
parts.queries.methods, parts.queries.methods,
+56 -501
View File
@@ -1,547 +1,102 @@
import { pickRenderAltitudeM } from '../../data/renderAltitude.js'; import { ERROR_BACKOFF_INTERVAL } from './recordPolicy.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';
/** Own military source acquisition, cancellation, freshness and error backoff. */
export function createIngestion({ export function createIngestion({
flightState, feed,
services, applySnapshot,
parts, setSourceLabel,
layer, applyPendingTrackingRestore,
resolveAsset,
}) { }) {
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 = { 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 } = {}) { async update(viewer, { signal = null } = {}) {
const nowMs = Date.now(); const nowMs = Date.now();
const trackingRefreshEpoch = ++flightState._trackingRefreshEpoch; const trackingRefreshEpoch = ++feed._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = { feed._lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch, epoch: trackingRefreshEpoch,
status: 'source-unavailable', status: 'source-unavailable',
ids: new Set(), ids: new Set(),
source: flightState._lastSource, source: feed._lastSource,
}; };
if (flightState._retryAt && nowMs < flightState._retryAt) { if (feed._retryAt && nowMs < feed._retryAt) {
flightState._backoff = true; feed._backoff = true;
return; return;
} }
const resourceController = new AbortController(); const resourceController = new AbortController();
flightState._activeUpdateControllers.add(resourceController); feed._activeUpdateControllers.add(resourceController);
const updateSignal = signal const updateSignal = signal
? AbortSignal.any([signal, resourceController.signal]) ? AbortSignal.any([signal, resourceController.signal])
: resourceController.signal; : resourceController.signal;
try { try {
updateSignal.throwIfAborted(); updateSignal.throwIfAborted();
const snapshot = await flightState._source.getSnapshot( const snapshot = await feed._source.getSnapshot(
{}, {},
{ signal: updateSignal }, { signal: updateSignal },
); );
updateSignal.throwIfAborted(); updateSignal.throwIfAborted();
flightState._lastStatus = snapshot.status ?? 200; feed._lastStatus = snapshot.status ?? 200;
flightState._lastSource = snapshot.source; feed._lastSource = snapshot.source;
militaryFlightsLayer.source = flightState._lastSource; setSourceLabel(feed._lastSource);
const usableAircraft = snapshot.records; feed._backoff = snapshot.stale || snapshot.freshness === 'unknown';
flightState._backoff = feed._retryAt = 0;
snapshot.stale || snapshot.freshness === 'unknown'; feed._lastError =
flightState._retryAt = 0;
flightState._lastError =
snapshot.reason || snapshot.reason ||
(snapshot.freshness === 'unknown' (snapshot.freshness === 'unknown'
? 'Source snapshot time unavailable' ? 'Source snapshot time unavailable'
: null); : null);
const currentIcaos = new Set(); const accepted = applySnapshot(snapshot, viewer);
const receiptNowMs = snapshot.observedAtMs; feed._count = accepted.count;
// Field-test fix (RS46): coarse floor cells to warm for the below-ground feed._lastUpdate = snapshot.observedAtMs;
// clamp — collected during the loop (low airborne contacts only), feed._lastTrackingRefreshOutcome = {
// 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 = {
epoch: trackingRefreshEpoch, epoch: trackingRefreshEpoch,
status: 'accepted', status: 'accepted',
ids: currentIcaos, ids: accepted.ids,
source: flightState._lastSource, source: feed._lastSource,
}; };
console.log(`[Data:Military] Updated: ${flightState._count} aircraft`); console.log(`[Data:Military] Updated: ${feed._count} aircraft`);
parts.tracking._applyPendingTrackingRestore(); applyPendingTrackingRestore();
} catch (e) { } catch (e) {
if (updateSignal.aborted || e?.name === 'AbortError') { if (updateSignal.aborted || e?.name === 'AbortError') {
throw new DOMException('Military update aborted', 'AbortError'); throw new DOMException('Military update aborted', 'AbortError');
} }
console.warn('[Data:Military] Fetch error:', e); console.warn('[Data:Military] Fetch error:', e);
flightState._backoff = true; feed._backoff = true;
flightState._retryAt = feed._retryAt =
Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL); Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
flightState._lastStatus = e?.status ?? null; feed._lastStatus = e?.status ?? null;
if (e?.source) { if (e?.source) {
flightState._lastSource = e.source; feed._lastSource = e.source;
this.source = flightState._lastSource; setSourceLabel(feed._lastSource);
} }
flightState._lastError = feed._lastError =
e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable'; e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
} finally { } finally {
flightState._activeUpdateControllers.delete(resourceController); feed._activeUpdateControllers.delete(resourceController);
} }
}, },
}; };
return { methods }; return { methods };
} }
/** Construct 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'); throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') if (typeof source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
flightState._source = source; flightState.feed._source = source;
flightState._lastSource = source.label || flightState._lastSource; flightState.feed._lastSource =
this.source = flightState._lastSource; source.label || flightState.feed._lastSource;
this.source = flightState.feed._lastSource;
}, },
/** /**
@@ -35,7 +36,7 @@ export function createLifecycle({
init(viewer) { init(viewer) {
if (flightState._viewer) if (flightState._viewer)
throw new Error('Flight layer is already initialized'); throw new Error('Flight layer is already initialized');
if (typeof flightState._source?.getSnapshot !== 'function') if (typeof flightState.feed._source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted) if (flightState.lifetime.signal.aborted)
flightState.lifetime = new AbortController(); flightState.lifetime = new AbortController();
@@ -72,17 +73,18 @@ export function createLifecycle({
}); });
} }
flightState._billboards = new Map(); flightState._billboards = new Map();
flightState._cullPositions.clear();
flightState._detectionObjects = new Map(); flightState._detectionObjects = new Map();
flightState._flightData = new Map(); flightState.records.data = new Map();
flightState._positionHistory = new Map(); flightState._positionHistory = new Map();
flightState._displayCourse.clear(); flightState._displayCourse.clear();
flightState._groundSnap.clear(); flightState._groundSnap.clear();
flightState._count = 0; flightState.feed._count = 0;
flightState._lastUpdate = null; flightState.feed._lastUpdate = null;
flightState._backoff = false; flightState.feed._backoff = false;
flightState._retryAt = 0; flightState.feed._retryAt = 0;
flightState._lastError = null; flightState.feed._lastError = null;
flightState._lastStatus = null; flightState.feed._lastStatus = null;
flightState._trackedIcao = null; flightState._trackedIcao = null;
parts.tracking._resetTrackedSelectionState(); parts.tracking._resetTrackedSelectionState();
flightState._trackedEntity = null; flightState._trackedEntity = null;
@@ -122,11 +124,11 @@ export function createLifecycle({
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop // Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
// only reads geoidHeight() synchronously after this resolves (guarded by // only reads geoidHeight() synchronously after this resolves (guarded by
// _geoidReady) — never awaited per-aircraft, never blocking a poll tick. // _geoidReady) — never awaited per-aircraft, never blocking a poll tick.
if (!flightState._geoidReady) { if (!flightState.records.geoidReady) {
const lifetime = flightState.lifetime; const lifetime = flightState.lifetime;
ensureGeoidReady() ensureGeoidReady()
.then(() => { .then(() => {
if (!lifetime.signal.aborted) flightState._geoidReady = true; if (!lifetime.signal.aborted) flightState.records.geoidReady = true;
}) })
.catch(() => { .catch(() => {
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */ /* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
@@ -274,23 +276,24 @@ export function createLifecycle({
} }
flightState._planeModelLoaded = false; flightState._planeModelLoaded = false;
flightState._billboards.clear(); flightState._billboards.clear();
flightState._cullPositions.clear();
flightState._detectionObjects.clear(); flightState._detectionObjects.clear();
flightState._flightData.clear(); flightState.records.data.clear();
flightState._positionHistory.clear(); flightState._positionHistory.clear();
flightState._displayCourse.clear(); flightState._displayCourse.clear();
flightState._groundSnap.clear(); flightState._groundSnap.clear();
flightState._missingPolls.clear(); flightState.records.missingPolls.clear();
flightState._count = 0; flightState.feed._count = 0;
flightState._lastUpdate = null; flightState.feed._lastUpdate = null;
flightState._cockpitContactMode = false; flightState._cockpitContactMode = false;
flightState._cockpitNearContacts = new Set(); flightState._cockpitNearContacts = new Set();
flightState._cockpitSubjectId = null; flightState._cockpitSubjectId = null;
flightState._trackingRefreshEpoch += 1; flightState.feed._trackingRefreshEpoch += 1;
flightState._lastTrackingRefreshOutcome = { flightState.feed._lastTrackingRefreshOutcome = {
epoch: flightState._trackingRefreshEpoch, epoch: flightState.feed._trackingRefreshEpoch,
status: 'destroyed', status: 'destroyed',
ids: new Set(), ids: new Set(),
source: flightState._lastSource, source: flightState.feed._lastSource,
}; };
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
flightState._viewer = null; flightState._viewer = null;
+8 -8
View File
@@ -55,7 +55,7 @@ export function createMotion({
function _deadReckon(icao24, result) { function _deadReckon(icao24, result) {
const history = flightState._positionHistory.get(icao24); const history = flightState._positionHistory.get(icao24);
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (!history || history.length === 0) { if (!history || history.length === 0) {
flightState._drCourseDeg = null; flightState._drCourseDeg = null;
flightState._drSpeedMps = null; flightState._drSpeedMps = null;
@@ -229,7 +229,7 @@ export function createMotion({
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position); const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
if (parts.rendering._modelOwnsVisual(icao24)) { if (parts.rendering._modelOwnsVisual(icao24)) {
const spec = parts.rendering._modelSpec( const spec = parts.rendering._modelSpec(
flightState._flightData.get(icao24)?.klass, flightState.records.data.get(icao24)?.klass,
); );
const scale = trackedModelScaleForPixelCap({ const scale = trackedModelScaleForPixelCap({
baseScale: spec.scale, baseScale: spec.scale,
@@ -256,7 +256,7 @@ export function createMotion({
const scale = const scale =
billboard?.scale?.getValue(time) ?? billboard?.scale?.getValue(time) ??
BILLBOARD_SCALE * BILLBOARD_SCALE *
(CLASS_SCALE_2D[flightState._flightData.get(icao24)?.klass] || 1); (CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1);
const scaleByDistance = const scaleByDistance =
billboard?.scaleByDistance?.getValue(time) ?? billboard?.scaleByDistance?.getValue(time) ??
TRACKED_BILLBOARD_SCALE_BY_DISTANCE; TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
@@ -299,7 +299,7 @@ export function createMotion({
} }
const nowMs = Date.now(); const nowMs = Date.now();
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (sameTrack) { if (sameTrack) {
const dtSec = Math.max(0.001, (nowMs - flightState._drPrevMs) / 1000); const dtSec = Math.max(0.001, (nowMs - flightState._drPrevMs) / 1000);
const speed = (info && info.speedMps) || 0; const speed = (info && info.speedMps) || 0;
@@ -380,7 +380,7 @@ export function createMotion({
parts.rendering._modelOwnsVisual(flightState._trackedIcao) parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) { ) {
const spec = parts.rendering._modelSpec( const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
return modelVisualAnchor( return modelVisualAnchor(
flightState._trackedModel.modelMatrix, flightState._trackedModel.modelMatrix,
@@ -423,7 +423,7 @@ export function createMotion({
) )
return 0; return 0;
const spec = parts.rendering._modelSpec( const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
const scale = Number.isFinite(flightState._trackedModel.computedScale) const scale = Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale ? flightState._trackedModel.computedScale
@@ -440,7 +440,7 @@ export function createMotion({
parts.rendering._modelOwnsVisual(flightState._trackedIcao) parts.rendering._modelOwnsVisual(flightState._trackedIcao)
) { ) {
const spec = parts.rendering._modelSpec( const spec = parts.rendering._modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled // Through the model's OWN render chain (modelVisualAnchor's hand-rolled
// half-correction put this offset on the lateral axis — see // half-correction put this offset on the lateral axis — see
@@ -466,7 +466,7 @@ export function createMotion({
* same aircraft can never disagree across the handoff (mirror of flights.js). */ * same aircraft can never disagree across the handoff (mirror of flights.js). */
function _trackedDisplayCourse() { function _trackedDisplayCourse() {
const info = flightState._flightData.get(flightState._trackedIcao); const info = flightState.records.data.get(flightState._trackedIcao);
const fallback = (info && info.track) || 0; const fallback = (info && info.track) || 0;
const cacheValid = const cacheValid =
flightState._drReconcileValid && 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 first = createFlightState({ services: services() });
const second = createFlightState({ services: services() }); const second = createFlightState({ services: services() });
for (const key of [ for (const key of [
'_flightData', 'records',
'feed',
'_billboards', '_billboards',
'_positionHistory', '_positionHistory',
'_groundSnap', '_groundSnap',
'_scratchCarto', '_scratchCarto',
'_models', '_models',
'_activeUpdateControllers',
'lifetime', 'lifetime',
]) { ]) {
assert.notEqual(first[key], second[key], key); assert.notEqual(first[key], second[key], key);
} }
assert.notEqual(first.records.data, second.records.data);
assert.notEqual(first.records.missingPolls, second.records.missingPolls);
assert.notEqual(first.records.geoidNCache, second.records.geoidNCache);
assert.notEqual(
first.feed._activeUpdateControllers,
second.feed._activeUpdateControllers,
);
}); });
test('a normalized source can retain its stale reason without changing standalone cache policy', async () => { 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'; 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 */ /** @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 */ /** @constant {number} Max position samples retained per aircraft for dead reckoning */
export const POSITION_HISTORY_LIMIT = 5;
/** @constant {number} Base billboard display scale */ /** @constant {number} Base billboard display scale */
export const BILLBOARD_SCALE = 0.7; 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). */ /** @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 // --- Landed-plane fast cull (mirror of flights.js; owner field report
// 2026-07-02: "phantom" planes lingered ~2 min at airports after touchdown). // 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 // 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). */ /** @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 // Field-test fix (RS46, 2026-07-06): only contacts rendering below this
// ellipsoidal height get the below-ground floor clamp + a coarse floor-cell // ellipsoidal height get the below-ground floor clamp + a coarse floor-cell
// warm. Terrain outside the extreme Himalaya tops out well under this, so // 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. // gets the floor.
/** @constant {number} Max render altitude (m, ellipsoidal) eligible for the ground-floor clamp. */ /** @constant {number} Max render altitude (m, ellipsoidal) eligible for the ground-floor clamp. */
export const GROUND_FLOOR_WARM_MAX_ALT_M = 4500;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Nadir-stable icon orientation (mirrors flights.js): surface-normal alignment // Nadir-stable icon orientation (mirrors flights.js): surface-normal alignment
// degenerates when the camera looks straight down (the normal is parallel to // 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) { function _likelyLanded(icao24) {
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (!info) return false; if (!info) return false;
// Round 7 (mirror of flights.js): fast cull only for contacts seen // Round 7 (mirror of flights.js): fast cull only for contacts seen
// AIRBORNE this session — parked contacts ride the normal grace so feed // AIRBORNE this session — parked contacts ride the normal grace so feed
@@ -91,7 +91,7 @@ export function createQueries({
*/ */
function _describeFlight(icao24) { function _describeFlight(icao24) {
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const basePos = const basePos =
parts.motion._deadReckon(icao24) || (bb ? bb.position : null); parts.motion._deadReckon(icao24) || (bb ? bb.position : null);
@@ -130,7 +130,8 @@ export function createQueries({
velocityMps: displayed.speedMps, velocityMps: displayed.speedMps,
track: displayed.trackDeg, track: displayed.trackDeg,
stale: Boolean( stale: Boolean(
flightState._missingPolls.get(icao24) || flightState._backoff, flightState.records.missingPolls.get(icao24) ||
flightState.feed._backoff,
), ),
}; };
} }
@@ -199,7 +200,7 @@ export function createQueries({
icon: '🎖️', icon: '🎖️',
source: flightState._lastSource, source: flightState.feed._lastSource,
/** @type {number} Polling interval in ms between API fetches */ /** @type {number} Polling interval in ms between API fetches */
updateInterval: 15000, updateInterval: 15000,
@@ -362,7 +363,7 @@ export function createQueries({
const distance = Cesium.Cartesian3.distance(center, pos); const distance = Cesium.Cartesian3.distance(center, pos);
if (distance > maxRange) continue; if (distance > maxRange) continue;
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
nearby.push({ nearby.push({
id: info?.callsign?.trim() || info?.registration?.trim() || icao24, id: info?.callsign?.trim() || info?.registration?.trim() || icao24,
icao24, icao24,
@@ -434,7 +435,7 @@ export function createQueries({
const model = flightState._models.get(icao24); const model = flightState._models.get(icao24);
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24); const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24);
if (!isTracked && !bb.show && !modelOwnsVisual) continue; if (!isTracked && !bb.show && !modelOwnsVisual) continue;
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
const callsign = info?.callsign?.trim(); const callsign = info?.callsign?.trim();
const registration = info?.registration?.trim(); const registration = info?.registration?.trim();
let object = flightState._detectionObjects.get(icao24); 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. * Best match with a cloned, dead-reckoned position, or null if none.
*/ */
findByQuery(query) { findByQuery(query) {
if (!flightState._flightData || flightState._flightData.size === 0) if (!flightState.records.data || flightState.records.data.size === 0)
return null; return null;
const q = String(query || '') const q = String(query || '')
.trim() .trim()
@@ -524,7 +525,7 @@ export function createQueries({
// cannot disagree, and it is strictly tiered so a registration can never // cannot disagree, and it is strictly tiered so a registration can never
// out-rank a real callsign on feed order alone. // out-rank a real callsign on feed order alone.
let best = null; let best = null;
for (const [icao24, info] of flightState._flightData) { for (const [icao24, info] of flightState.records.data) {
const candidate = { const candidate = {
tier: rankContactMatch({ tier: rankContactMatch({
query: q, query: q,
@@ -595,7 +596,7 @@ export function createQueries({
flightState._scratchCarto, flightState._scratchCarto,
); );
if (!carto) continue; if (!carto) continue;
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
result.push({ result.push({
id: icao24, // identity (trackById/Context resolve this) id: icao24, // identity (trackById/Context resolve this)
// Last surface still reading as the raw hex for a callsign-less // Last surface still reading as the raw hex for a callsign-less
@@ -626,14 +627,14 @@ export function createQueries({
if ( if (
!flightState._billboardCollection || !flightState._billboardCollection ||
!flightState._billboardCollection.show || !flightState._billboardCollection.show ||
flightState._flightData.size === 0 flightState.records.data.size === 0
) )
return []; return [];
const limit = Number.isFinite(maxCount) const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount)) ? Math.max(1, Math.floor(maxCount))
: 2000; : 2000;
const result = []; const result = [];
for (const [icao24, info] of flightState._flightData) { for (const [icao24, info] of flightState.records.data) {
result.push(mapAnalystRecord(icao24, info)); result.push(mapAnalystRecord(icao24, info));
if (result.length >= limit) break; if (result.length >= limit) break;
} }
@@ -672,11 +673,11 @@ export function createQueries({
.trim() .trim()
.toLowerCase(); .toLowerCase();
if (!id) return { status: 'missing', reason: 'invalid-target' }; if (!id) return { status: 'missing', reason: 'invalid-target' };
const outcome = flightState._lastTrackingRefreshOutcome; const outcome = flightState.feed._lastTrackingRefreshOutcome;
if (outcome.status !== 'accepted') { if (outcome.status !== 'accepted') {
return { return {
status: 'source-unavailable', status: 'source-unavailable',
reason: `${flightState._lastSource} snapshot unavailable`, reason: `${flightState.feed._lastSource} snapshot unavailable`,
refreshEpoch: outcome.epoch, refreshEpoch: outcome.epoch,
source: outcome.source, 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}} * @returns {{count: number, lastUpdate: number|null, stale: boolean, error: string|null, status: number|null, retryInSec: number}}
*/ */
getStats() { getStats() {
const retryInSec = flightState._retryAt const retryInSec = flightState.feed._retryAt
? Math.max(0, Math.ceil((flightState._retryAt - Date.now()) / 1000)) ? Math.max(
0,
Math.ceil((flightState.feed._retryAt - Date.now()) / 1000),
)
: 0; : 0;
return { return {
count: flightState._count, count: flightState.feed._count,
lastUpdate: flightState._lastUpdate, lastUpdate: flightState.feed._lastUpdate,
stale: flightState._backoff, stale: flightState.feed._backoff,
error: flightState._lastError, error: flightState.feed._lastError,
status: flightState._lastStatus, status: flightState.feed._lastStatus,
retryInSec, retryInSec,
source: flightState._lastSource, source: flightState.feed._lastSource,
fallback: false, 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) { function _militaryBillboardScale(icao24) {
const meta = flightState._flightData.get(icao24); const meta = flightState.records.data.get(icao24);
return ( return (
BILLBOARD_SCALE * BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta?.klass] || 1) * (CLASS_SCALE_2D[meta?.klass] || 1) *
@@ -162,7 +162,7 @@ export function createRendering({
return; return;
} }
const meta = flightState._flightData.get(icao24); const meta = flightState.records.data.get(icao24);
bb.image = aircraftIcon( bb.image = aircraftIcon(
_iconKind(icao24, meta?.klass), _iconKind(icao24, meta?.klass),
bb._gevIconLarge ? TRACKED_ICON_PX : undefined, bb._gevIconLarge ? TRACKED_ICON_PX : undefined,
@@ -280,7 +280,7 @@ export function createRendering({
* outage inside groundSnap's drift bound, so taxiing does not pop it back to 2D. */ * outage inside groundSnap's drift bound, so taxiing does not pop it back to 2D. */
function _modelDisplayPosition(icao24, pos, result) { function _modelDisplayPosition(icao24, pos, result) {
const meta = flightState._flightData.get(icao24); const meta = flightState.records.data.get(icao24);
if (!meta || !meta.onGround) return pos; if (!meta || !meta.onGround) return pos;
const h = flightState._groundSnap.heightFor( const h = flightState._groundSnap.heightFor(
flightState._viewer, flightState._viewer,
@@ -332,7 +332,7 @@ export function createRendering({
displayPos, displayPos,
course, course,
model.modelMatrix, model.modelMatrix,
_modelSpec(flightState._flightData.get(icao24)?.klass).headingOffsetDeg, _modelSpec(flightState.records.data.get(icao24)?.klass).headingOffsetDeg,
); );
if (!model.ready) { if (!model.ready) {
model.show = false; // not loaded yet → keep the 2D icon, no half-model flash 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. */ * fleet billboard FIRST (gap-proof) and covering the tracked standalone. */
function _syncModelToClass(icao24) { function _syncModelToClass(icao24) {
const key = _specKeyFor(flightState._flightData.get(icao24)?.klass); const key = _specKeyFor(flightState.records.data.get(icao24)?.klass);
const current = flightState._models.get(icao24); const current = flightState._models.get(icao24);
if ( if (
(current && current._gevSpecKey !== key) || (current && current._gevSpecKey !== key) ||
@@ -450,10 +450,10 @@ export function createRendering({
// Spec identity captured at load START (mirror of flights.js): a mid-load // Spec identity captured at load START (mirror of flights.js): a mid-load
// reclassification makes the post-await admission reject the stale asset. // reclassification makes the post-await admission reject the stale asset.
// Boost state likewise — creation options bake it in. // 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; const loadIrBoost = flightState._irBoost;
try { try {
const spec = _modelSpec(flightState._flightData.get(icao24)?.klass); const spec = _modelSpec(flightState.records.data.get(icao24)?.klass);
model = await Cesium.Model.fromGltfAsync({ model = await Cesium.Model.fromGltfAsync({
url: resolveAsset(spec.url), url: resolveAsset(spec.url),
asynchronous: false, asynchronous: false,
@@ -498,12 +498,12 @@ export function createRendering({
!_modelRegimeActive() || !_modelRegimeActive() ||
!flightState._modelCollection || !flightState._modelCollection ||
flightState._modelCollection.isDestroyed() || flightState._modelCollection.isDestroyed() ||
!flightState._flightData.has(icao24) || !flightState.records.data.has(icao24) ||
icao24 === flightState._trackedIcao || icao24 === flightState._trackedIcao ||
flightState._models.has(icao24) || flightState._models.has(icao24) ||
flightState._models.size >= _modelCap() || flightState._models.size >= _modelCap() ||
// Class reclassified mid-load → this GLB/scale is for the OLD class. // Class reclassified mid-load → this GLB/scale is for the OLD class.
_specKeyFor(flightState._flightData.get(icao24)?.klass) !== specKey || _specKeyFor(flightState.records.data.get(icao24)?.klass) !== specKey ||
// IR boost flipped mid-load → this model baked the wrong shader/tint. // IR boost flipped mid-load → this model baked the wrong shader/tint.
flightState._irBoost !== loadIrBoost; flightState._irBoost !== loadIrBoost;
if (stale) { if (stale) {
@@ -651,10 +651,10 @@ export function createRendering({
flightState._trackedModelLoading = true; flightState._trackedModelLoading = true;
const gen = flightState._trackedModelGen; const gen = flightState._trackedModelGen;
const trackedSpec = _modelSpec( const trackedSpec = _modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
const trackedKey = _specKeyFor( const trackedKey = _specKeyFor(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
const trackedIrBoost = flightState._irBoost; const trackedIrBoost = flightState._irBoost;
Cesium.Model.fromGltfAsync({ Cesium.Model.fromGltfAsync({
@@ -691,7 +691,7 @@ export function createRendering({
// _trackedModel was still null): drop the stale asset; driver reloads. // _trackedModel was still null): drop the stale asset; driver reloads.
if ( if (
_specKeyFor( _specKeyFor(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
) !== trackedKey || ) !== trackedKey ||
flightState._irBoost !== trackedIrBoost flightState._irBoost !== trackedIrBoost
) { ) {
@@ -760,7 +760,7 @@ export function createRendering({
return; return;
} }
const spec = _modelSpec( const spec = _modelSpec(
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
); );
_modelMatrix( _modelMatrix(
displayPos, displayPos,
@@ -933,7 +933,7 @@ export function createRendering({
// ellipsoid (mirror of flights.js — sub-ellipsoid points near the limb // ellipsoid (mirror of flights.js — sub-ellipsoid points near the limb
// read "beyond the horizon" and would hide low contacts awaiting floors). // read "beyond the horizon" and would hide low contacts awaiting floors).
const beyondHorizon = !occluder.isPointVisible( 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) // A billboard flipping INTO view (horizon reveal while the camera idles)
// gets its rotation refreshed THIS tick even without a pose change — // gets its rotation refreshed THIS tick even without a pose change —
@@ -950,7 +950,7 @@ export function createRendering({
continue; 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 // One sprite-owned write site composes freshness × focus × limb haze and
// base class/ground scale × limb taper. The locked NearFarScalar remains // base class/ground scale × limb taper. The locked NearFarScalar remains
@@ -983,7 +983,7 @@ export function createRendering({
flightState._cockpitContactMode && !isCockpitNear flightState._cockpitContactMode && !isCockpitNear
? 1 ? 1
: _militaryBillboardScale(icao24), : _militaryBillboardScale(icao24),
baseAlpha: flightState._missingPolls.get(icao24) ? 0.45 : 1, baseAlpha: flightState.records.missingPolls.get(icao24) ? 0.45 : 1,
baseColor: MIL_ICON_COLOR, baseColor: MIL_ICON_COLOR,
focusFactor: focus.factor, focusFactor: focus.factor,
cameraDistanceM, cameraDistanceM,
@@ -1003,7 +1003,7 @@ export function createRendering({
if (wantLarge !== !!bb._gevIconLarge) { if (wantLarge !== !!bb._gevIconLarge) {
bb._gevIconLarge = wantLarge; bb._gevIconLarge = wantLarge;
bb.image = aircraftIcon( bb.image = aircraftIcon(
_iconKind(icao24, flightState._flightData.get(icao24)?.klass), _iconKind(icao24, flightState.records.data.get(icao24)?.klass),
wantLarge ? TRACKED_ICON_PX : undefined, 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'; import * as Cesium from 'cesium';
export function createFlightState({ source, services }) { export function createFlightState({ source, services }) {
const { createGroundSnap } = services.groundSnap; const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() }; const flightState = { lifetime: new AbortController() };
flightState.feed = createMilitaryFeed(source);
flightState._source = 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: /** 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 * 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(); 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 */ /** @type {Map<string, Array<{time: Cesium.JulianDate, position: Cesium.Cartesian3}>>} ICAO hex -> recent position samples for dead reckoning */
flightState._positionHistory = new Map(); 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 -- // -- Click-to-track state --
/** @type {string|null} ICAO hex of the currently tracked aircraft */ /** @type {string|null} ICAO hex of the currently tracked aircraft */
@@ -155,15 +121,6 @@ export function createFlightState({ source, services }) {
flightState._trackingIntentGeneration = 0; flightState._trackingIntentGeneration = 0;
flightState._trackingRefreshEpoch = 0;
flightState._lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
ids: new Set(),
source: flightState._lastSource,
};
/** @type {Cesium.Entity|null} Entity created for the tracked aircraft (camera follows this) */ /** @type {Cesium.Entity|null} Entity created for the tracked aircraft (camera follows this) */
flightState._trackedEntity = null; flightState._trackedEntity = null;
@@ -226,10 +183,6 @@ export function createFlightState({ source, services }) {
flightState._trailBackfillToken = 0; flightState._trailBackfillToken = 0;
/** @type {Map<string, number>} icao24 -> consecutive missed polls */
flightState._missingPolls = new Map();
/** @type {number} Epoch ms of the last fleet dead-reckoning pass */ /** @type {number} Epoch ms of the last fleet dead-reckoning pass */
flightState._lastFleetTickMs = 0; flightState._lastFleetTickMs = 0;
+10 -9
View File
@@ -40,14 +40,15 @@ export function createTesting({
flightState._modelCollection = modelCollection; flightState._modelCollection = modelCollection;
flightState._billboardCollection = billboardCollection; flightState._billboardCollection = billboardCollection;
flightState._billboards = new Map([[icao24, billboard]]); flightState._billboards = new Map([[icao24, billboard]]);
flightState._cullPositions.clear();
flightState._models.clear(); flightState._models.clear();
for (const [key, model] of models) flightState._models.set(key, model); for (const [key, model] of models) flightState._models.set(key, model);
flightState._detectionObjects = new Map(); flightState._detectionObjects = new Map();
flightState._flightData = new Map([[icao24, meta]]); flightState.records.data = new Map([[icao24, meta]]);
flightState._positionHistory = new Map([[icao24, history]]); flightState._positionHistory = new Map([[icao24, history]]);
flightState._missingPolls = new Map(); flightState.records.missingPolls = new Map();
flightState._displayCourse.clear(); flightState._displayCourse.clear();
flightState._geoidNCache.clear(); flightState.records.geoidNCache.clear();
flightState._trackedIcao = tracked ? icao24 : null; flightState._trackedIcao = tracked ? icao24 : null;
flightState._trackedEntity = tracked ? entity : null; flightState._trackedEntity = tracked ? entity : null;
flightState._trackedModel = null; flightState._trackedModel = null;
@@ -57,8 +58,8 @@ export function createTesting({
// production state owned by the tracking lifecycle (_resetTrackedSelectionState), // production state owned by the tracking lifecycle (_resetTrackedSelectionState),
// and clearing them here would mask exactly the deselect→re-track hole this // and clearing them here would mask exactly the deselect→re-track hole this
// seam is used to test. // seam is used to test.
flightState._backoff = false; flightState.feed._backoff = false;
flightState._retryAt = 0; flightState.feed._retryAt = 0;
} }
/** Seed the authoritative snapshot outcome used by share-Follow tests. */ /** Seed the authoritative snapshot outcome used by share-Follow tests. */
@@ -66,10 +67,10 @@ export function createTesting({
function _setMilitaryTrackingRefreshOutcomeForTest({ function _setMilitaryTrackingRefreshOutcomeForTest({
status = 'accepted', status = 'accepted',
ids = [], ids = [],
source = flightState._lastSource, source = flightState.feed._lastSource,
} = {}) { } = {}) {
const epoch = ++flightState._trackingRefreshEpoch; const epoch = ++flightState.feed._trackingRefreshEpoch;
flightState._lastTrackingRefreshOutcome = { flightState.feed._lastTrackingRefreshOutcome = {
epoch, epoch,
status, status,
ids: new Set(ids.map((id) => String(id).trim().toLowerCase())), ids: new Set(ids.map((id) => String(id).trim().toLowerCase())),
@@ -86,7 +87,7 @@ export function createTesting({
history = [], history = [],
}) { }) {
flightState._billboards.set(icao24, billboard); flightState._billboards.set(icao24, billboard);
flightState._flightData.set(icao24, meta); flightState.records.data.set(icao24, meta);
flightState._positionHistory.set(icao24, history); flightState._positionHistory.set(icao24, history);
} }
+15 -13
View File
@@ -61,7 +61,7 @@ export function createTracking({
function _publishTrackedSelection(icao24, origin = 'programmatic') { function _publishTrackedSelection(icao24, origin = 'programmatic') {
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (!bb?.position || !info) return false; if (!bb?.position || !info) return false;
if (flightState._trackedEntity) if (flightState._trackedEntity)
flightState._trackedEntity.gevSelectionOrigin = origin; flightState._trackedEntity.gevSelectionOrigin = origin;
@@ -90,13 +90,13 @@ export function createTracking({
function _contextSubjectMetadata(icao24) { function _contextSubjectMetadata(icao24) {
const described = parts.queries._describeFlight(icao24); const described = parts.queries._describeFlight(icao24);
if (!described) return null; if (!described) return null;
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
const label = described.callsign || described.registration || icao24; const label = described.callsign || described.registration || icao24;
return { return {
id: icao24, id: icao24,
layerId: 'military', layerId: 'military',
layerName: 'Military Flights', layerName: 'Military Flights',
source: flightState._lastSource, source: flightState.feed._lastSource,
label, label,
latitude: described.latitude, latitude: described.latitude,
longitude: described.longitude, longitude: described.longitude,
@@ -218,7 +218,7 @@ export function createTracking({
function _buildTrackedLabel(info, icao24) { function _buildTrackedLabel(info, icao24) {
const stale = const stale =
flightState._missingPolls.get(icao24) || flightState._backoff flightState.records.missingPolls.get(icao24) || flightState.feed._backoff
? ' · STALE' ? ' · STALE'
: ''; : '';
const callsign = const callsign =
@@ -251,7 +251,7 @@ export function createTracking({
if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao) if (!flightState._trackedEntity || icao24 !== flightState._trackedIcao)
return; return;
flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText( flightState._trackedEntity.gevLabelModel = trackedLabelModelFromText(
_buildTrackedLabel(flightState._flightData.get(icao24), icao24), _buildTrackedLabel(flightState.records.data.get(icao24), icao24),
'#ffd166', '#ffd166',
); );
refreshTrackedReadout(flightState._trackedEntity); refreshTrackedReadout(flightState._trackedEntity);
@@ -268,7 +268,7 @@ export function createTracking({
flightState._trackedEntity.billboard.image = aircraftIcon( flightState._trackedEntity.billboard.image = aircraftIcon(
parts.rendering._iconKind( parts.rendering._iconKind(
flightState._trackedIcao, flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
), ),
TRACKED_ICON_PX, TRACKED_ICON_PX,
); );
@@ -606,8 +606,8 @@ export function createTracking({
async function _backfillTrail(icao24, token, oldestFixEpochSec) { async function _backfillTrail(icao24, token, oldestFixEpochSec) {
let trace = null; let trace = null;
try { try {
const track = await flightState._source.getTrack?.( const track = await flightState.feed._source.getTrack?.(
flightState._flightData.get(icao24)?.sourceReference ?? icao24, flightState.records.data.get(icao24)?.sourceReference ?? icao24,
{ {
signal: AbortSignal.any([ signal: AbortSignal.any([
flightState.lifetime.signal, flightState.lifetime.signal,
@@ -775,7 +775,7 @@ export function createTracking({
// ROTATION_REFRESH_MS of a wrong (possibly reversed) nose on release. // ROTATION_REFRESH_MS of a wrong (possibly reversed) nose on release.
if (flightState._billboards.has(flightState._trackedIcao)) { if (flightState._billboards.has(flightState._trackedIcao)) {
const bb = flightState._billboards.get(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.show = true;
bb.width = 20; bb.width = 20;
bb.height = 20; bb.height = 20;
@@ -855,7 +855,7 @@ export function createTracking({
_clearTracking(false, { origin }); // switching planes — the new follow-camera takes over _clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const info = flightState._flightData.get(icao24); const info = flightState.records.data.get(icao24);
if (!bb || !info) return; if (!bb || !info) return;
flightState._trackedIcao = icao24; flightState._trackedIcao = icao24;
@@ -908,7 +908,7 @@ export function createTracking({
image: aircraftIcon( image: aircraftIcon(
parts.rendering._iconKind( parts.rendering._iconKind(
flightState._trackedIcao, flightState._trackedIcao,
flightState._flightData.get(flightState._trackedIcao)?.klass, flightState.records.data.get(flightState._trackedIcao)?.klass,
), ),
TRACKED_ICON_PX, TRACKED_ICON_PX,
), ),
@@ -917,7 +917,7 @@ export function createTracking({
scale: scale:
BILLBOARD_SCALE * BILLBOARD_SCALE *
(CLASS_SCALE_2D[ (CLASS_SCALE_2D[
flightState._flightData.get(flightState._trackedIcao)?.klass flightState.records.data.get(flightState._trackedIcao)?.klass
] || 1), ] || 1),
// Solid amber when the billboard is the visual (zoomed out, 3D off, or model still loading); // 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). // 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 // Screen-projected rotation, evaluated per frame: exact in tracked-orbit
// mode where camera.heading lives in the entity's reference frame. // mode where camera.heading lives in the entity's reference frame.
rotation: new Cesium.CallbackProperty(() => { rotation: new Cesium.CallbackProperty(() => {
const tracked = flightState._flightData.get(flightState._trackedIcao); const tracked = flightState.records.data.get(
flightState._trackedIcao,
);
const pos = getTrackedPosition(); const pos = getTrackedPosition();
if (!tracked || !pos || !flightState._viewer) if (!tracked || !pos || !flightState._viewer)
return flightState._lastTrackedRotation; return flightState._lastTrackedRotation;