Files
gods-eye-view/src/layers/flights/snapshotRenderer.js
T

314 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as Cesium from 'cesium';
import {
turnRateFromFixHistory,
liftRepeatedGroundFix,
synthesizeForwardKinematicsFix,
} from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import { POSITION_HISTORY_LIMIT } from './recordPolicy.js';
/** Apply reconciled aircraft records to Cesium resources and follow state. */
export function createFlightSnapshotRenderer({
flightState,
records,
militaryRegistry,
groundFloor,
meshFloor,
rendering,
tracking,
motion,
enrichment,
queries,
}) {
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } = militaryRegistry;
const { warmGroundFloor } = groundFloor;
const { sampleMeshFloorCells } = meshFloor;
return function applySnapshot(snapshot, viewer) {
const currentIcaos = new Set();
const acceptedSnapshotIcaos = new Set();
// Field-test round 3 (2026-07-06, Austin fleet-underground): viewer
// subpoint + collected floor cells for the viewer-proximate low-contact
// clamp below — one carto read per poll, one batch warm after the loop.
const viewerCarto =
(viewer || flightState._viewer)?.camera?.positionCartographic || null;
const viewerLatDeg = viewerCarto
? Cesium.Math.toDegrees(viewerCarto.latitude)
: null;
const viewerLonDeg = viewerCarto
? Cesium.Math.toDegrees(viewerCarto.longitude)
: null;
const floorWarmPoints = [];
// Classification and display policy consume source-independent observations.
refreshMilitaryRegistryIfStale();
for (const observation of snapshot.records) {
const icao24 = observation.id;
acceptedSnapshotIcaos.add(icao24);
// Known-military aircraft: the dedicated military layer wins
// (icon + track + click) while it is enabled — suppress the
// OpenSky duplicate entirely (except a currently tracked one,
// which hands off on untrack).
const isMil = isMilitaryIcao(icao24);
if (isMil && tracking._militaryLayerSuppresses(icao24)) {
const dupe = flightState._billboards.get(icao24);
if (dupe) {
flightState._billboardCollection.remove(dupe);
flightState._billboards.delete(icao24);
rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
records.forget(icao24);
flightState._cullPositions.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
}
continue;
}
currentIcaos.add(icao24);
const { prevMeta, meta, groundFlipped, fixEpochMs } = records.receive(
observation,
{
viewerLatDeg,
viewerLonDeg,
trackedId: flightState._trackedIcao,
floorWarmPoints,
},
);
const position = Cesium.Cartesian3.fromDegrees(
meta.rawLon,
meta.rawLat,
meta.renderAltitudeM,
);
// Either flip retires the previous one-shot ground sample.
if (groundFlipped) flightState._groundSnap.forget(icao24);
flightState._cullPositions.set(
icao24,
meta.renderAltitudeM < 10
? Cesium.Cartesian3.fromDegrees(meta.rawLon, meta.rawLat, 12)
: null,
);
const isTracked = icao24 === flightState._trackedIcao;
// Append to position history stamped with the FEED's fix epoch
// (time_position), not client receipt time — OpenSky positions arrive
// 5-15s stale and receipt-time stamping is what caused the
// back/forward oscillation. Only append when the fix actually
// advances, so stale repeats don't create zero-dt segments.
const fixTime = Cesium.JulianDate.fromDate(new Date(fixEpochMs));
if (!flightState._positionHistory.has(icao24)) {
flightState._positionHistory.set(icao24, []);
}
const history = flightState._positionHistory.get(icao24);
const newest = history[history.length - 1];
if (!newest || Cesium.JulianDate.greaterThan(fixTime, newest.time)) {
// Per-fix kinematics: the fix's own velocity/track ride along so the
// extrapolation paths use the values that BELONG to the fix they
// extend, not whatever the latest poll reported.
history.push({
time: fixTime,
epochMs: fixEpochMs,
position: position.clone(),
velocity: meta.velocity,
track: meta.true_track,
});
if (history.length > POSITION_HISTORY_LIMIT) {
history.shift();
}
// Turn rate from the fix-track history — computed once per new fix
// (≤5 samples), consumed by the extrapolation paths at tick rate.
meta.turnRateDps = turnRateFromFixHistory(history);
// Trail accumulation is separate from the 5-fix DR history (PRD F1)
// so the visible trail keeps growing while tracked. Ground traffic
// appends nothing — a touchdown freezes the existing trail.
// Round 2 (owner): ground traffic appends too — taxi history stays
// live after touchdown (grounded flights positions are already
// surface-clamped via the surfaceM chain, so the ground leg drapes).
if (isTracked) tracking._appendTrailFix(position.clone());
} else {
const modelOwnsGroundVisual = rendering._modelOwnsVisual(icao24);
if (!modelOwnsGroundVisual) {
liftRepeatedGroundFix(newest, position, meta.onGround);
}
// Apply fresh kinematics only from a forward synthetic fix. Mutating
// the historical fix reprojects the entire stale interval and snaps
// the rendered aircraft when course or speed changes late.
const kinematicsChanged =
newest.velocity !== meta.velocity || newest.track !== meta.true_track;
if (kinematicsChanged) {
const synthetic = synthesizeForwardKinematicsFix(newest, {
epochMs: Date.now(),
velocity: meta.velocity,
track: meta.true_track,
turnRateDps: meta.turnRateDps,
});
if (synthetic) {
history.push(synthetic);
if (history.length > POSITION_HISTORY_LIMIT) history.shift();
meta.turnRateDps = turnRateFromFixHistory(history);
if (isTracked) tracking._appendTrailFix(synthetic.position.clone());
}
}
}
if (flightState._billboards.has(icao24)) {
const bb = flightState._billboards.get(icao24);
// Reclassify if the category resolved/changed (first extended poll);
// a ground flip (landing/takeoff) re-scales the SAME billboard in
// place — the transition is a restyle, never a removal.
// Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff.
// Position AND rotation are owned by the fleet pass (_fleetTick);
// course changes land on the next rotation pass (forced below).
if (!isTracked) {
// Refresh affiliation hue without clobbering the tick-owned
// freshness × focus × horizon alpha composition.
bb.color = rendering
._fleetBillboardColor(icao24)
.withAlpha(bb.color?.alpha ?? 1);
}
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Poll-path class change (category updates): same model resync rule
// as the enrichment path — the class's GLB/scale may have changed.
if (prevMeta?.klass !== meta.klass) rendering._syncModelToClass(icao24);
} else {
const bb = flightState._billboardCollection.add({
position,
image: aircraftIcon(rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale: rendering._fleetBillboardScale(icao24, meta.klass),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: isTracked
? Cesium.Color.CYAN
: rendering._fleetBillboardColor(icao24),
sizeInMeters: false,
scaleByDistance: rendering._normalBillboardScaleByDistance(),
// Grounded/near-surface planes sit at/below the photoreal tile
// surface — render them depth-test-free so they never vanish up
// close (_groundDepthDistance).
disableDepthTestDistance: rendering._groundDepthDistance(),
id: icao24,
show: !isTracked, // hidden if currently tracked (entity replaces it)
});
flightState._billboards.set(icao24, bb);
rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Takeoff while TRACKED: ground traffic drew no trail, so start one
// from the fresh airborne history (touchdown needs no action — the
// append gate above simply freezes the existing trail).
if (isTracked && groundFlipped && !meta.onGround)
tracking._startTrail(icao24);
// If this is the tracked aircraft, update label text
// (position updates automatically via dead-reckoning CallbackProperty)
if (isTracked && flightState._trackedEntity) {
tracking._updateTrackedLabelModel(icao24);
}
}
// Remove aircraft only after MISSING_POLL_LIMIT consecutive absences.
// OpenSky routinely drops aircraft for a single poll; immediate removal
// made planes blink and yanked the camera off actively tracked flights.
// EXCEPTION: likely-landed planes (last fix low + slow) get only
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
// feed gap, and the full grace left phantom planes parked at airports.
for (const [icao24, bb] of flightState._billboards) {
if (currentIcaos.has(icao24)) continue;
const absence = records.absence(icao24, {
complete: snapshot.complete,
likelyLanded: queries._likelyLanded(icao24),
});
if (absence === 'retain') continue;
if (absence === 'stale') {
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) {
// Honest readout: the tracked plane has no faded billboard (its
// entity owns the visual), so refresh the label — with icao24 now
// in _missingPolls, _trackedLabelText appends the STALE cue so
// last-known velocity/altitude aren't presented as live.
tracking._updateTrackedLabelModel(icao24);
}
continue;
}
// If the tracked flight is truly gone, clear tracking BEFORE deleting
// its state (M3 ordering, keep it): teardown reads the maps this loop
// is about to delete (billboard restore, DR cache reset), and we must
// never leave the camera mid-follow with stale tracking state. The
// camera is then RELEASED IN PLACE — it stays where the follow left
// it, fully free (owner decision 2026-07-02: no overview flyTo).
if (icao24 === flightState._trackedIcao) {
tracking._clearTracking(false, { evicted: true });
}
flightState._billboardCollection.remove(bb);
flightState._billboards.delete(icao24);
rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
records.forget(icao24);
flightState._cullPositions.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
flightState._displayFloorState.delete(icao24);
}
// Fresh courses arrived — force a rotation pass on the next fleet tick
flightState._lastCamPoseSig = '';
// Ambient type enrichment: give ON-SCREEN planes real types (bounded
// sweep — see _sweepAmbientEnrichment; internally fail-silent).
enrichment._sweepAmbientEnrichment();
// 2026-08-19: the loop above only ever collects FIX cells, but a grounded
// contact renders across every cell its dead-reckoned position drifts
// through. Add those too, so the display clamp has data where the contact
// actually is instead of silently passing.
motion._collectDisplayCorridorCells(
floorWarmPoints,
viewerLatDeg,
viewerLonDeg,
);
// Field-test round 3: one batch warm of the viewer-proximate low-contact
// floor cells collected in the loop (fire-and-forget, single-flight;
// read synchronously by NEXT poll's clamp — the military-layer pattern).
warmGroundFloor(floorWarmPoints);
// Round 4: sample the RENDERED mesh for those same cells (one-shot per
// cell, budget-capped, viewer-proximate, google-3d regime only). Own
// billboards/models are excluded so a vertical probe can't land on an
// aircraft instead of the pavement.
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
excludeObjects: [
...flightState._billboards.values(),
...flightState._models.values(),
flightState._trackedModel,
].filter(Boolean),
viewerLat: viewerLatDeg,
viewerLon: viewerLonDeg,
});
// Round 5: the grounded exact-key warm that used to live here is gone —
// see the note where _warmGroundedAircraftSurfaceCache was removed. The
// viewer-proximate coarse warm + mesh sampler above cover everything
// whose height is actually visible.
// Round 6: re-floor STALE grounded contacts. A parked plane whose
// transponder went quiet stops receiving poll updates, so a floor that
// warms AFTER its last fix never applied — it sat frozen at the geoid
// (ATL verify: FFT4347 at −30.7 m, 305 m under the apron, forever).
// Grounded contacts are static, so lifting the stored fix + billboard
// in place is safe (the DR extrapolates a zero-velocity fix).
motion._refloorStaleGroundedContacts(currentIcaos);
return { count: flightState._billboards.size, ids: acceptedSnapshotIcaos };
};
}