mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
314 lines
14 KiB
JavaScript
314 lines
14 KiB
JavaScript
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 };
|
||
};
|
||
}
|