From 7b328232edcb5f3c621b314bfc33fd02d1506042 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Tue, 6 Oct 2026 11:46:34 -0700 Subject: [PATCH 1/2] fix(flights): start new aircraft at their delayed display position The fleet renders behind real time, so a newly seen aircraft is displayed at its fix projected back to that delayed time. Its billboard was created at the raw fix instead, so it appeared ahead of where the next fleet tick put it and then jumped back about one render delay of travel before moving forward. --- src/data/flights.test.mjs | 71 ++++++++++++++++++++++++++ src/layers/flights/snapshotRenderer.js | 8 ++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/data/flights.test.mjs b/src/data/flights.test.mjs index abb047384..6f2544034 100644 --- a/src/data/flights.test.mjs +++ b/src/data/flights.test.mjs @@ -168,6 +168,77 @@ test('nonempty OpenSky payload with zero usable rows cannot prove share target a } }); +test('a newly seen aircraft appears at its delayed position, not ahead of it', async () => { + // The fleet renders RENDER_DELAY_SEC behind real time, so a fresh contact's + // first displayed position is its fix projected back to that delayed time. + // Creating the billboard at the raw fix drew it ahead until the next fleet + // tick, which then jumped it back about one delay of travel. + const added = []; + const billboardCollection = { + show: false, + add(options) { + const billboard = { ...options }; + added.push(billboard); + return billboard; + }, + remove() {}, + }; + const viewer = { camera: { positionCartographic: null }, scene: {} }; + _setTrackedFlightRefreshStateForTest({ + icao24: 'seed00', + entity: null, + billboard: { show: false, position: Cesium.Cartesian3.fromDegrees(0, 0, 0) }, + billboardCollection, + viewer, + meta: { rawLat: 0, rawLon: 0, onGround: false }, + tracked: false, + }); + const icao24 = 'fresh1'; + const fixLon = -97.6; + const fixLat = 30.3; + const speedMps = 250; + const nowSec = Math.floor(Date.now() / 1000); + const realFetch = globalThis.fetch; + globalThis.fetch = async (url) => { + if (!String(url).startsWith('/api/flights')) { + return { ok: true, status: 200, json: async () => ({ ac: [] }) }; + } + return { + ok: true, + status: 200, + headers: { get: () => null }, + json: async () => ({ + time: nowSec, + states: [[ + icao24, 'NEW123 ', 'United States', nowSec - 2, nowSec - 2, + fixLon, fixLat, 10_668, false, speedMps, 90, 0, null, 10_700, + null, null, null, 5, + ]], + }), + }; + }; + try { + await flightsLayer.update(viewer); + const billboard = added.find((entry) => entry.id === icao24); + assert.ok(billboard, 'the new contact gets a billboard'); + const shown = Cesium.Cartographic.fromCartesian(billboard.position); + const fix = Cesium.Cartesian3.fromDegrees(fixLon, fixLat, 10_700); + const behindM = Cesium.Cartesian3.distance( + Cesium.Cartesian3.fromRadians(shown.longitude, shown.latitude, 10_700), + fix, + ); + // Eastbound, so the delayed position is west of the fix by the travel + // between the delayed time and the fix (about 28 s). + assert.ok(Cesium.Math.toDegrees(shown.longitude) < fixLon, 'drawn behind the fix'); + assert.ok( + behindM > speedMps * 25 && behindM < speedMps * 31, + `expected about ${speedMps * 28} m behind the fix, got ${Math.round(behindM)} m`, + ); + } finally { + globalThis.fetch = realFetch; + } +}); + test('flights poll refreshes tracked callsign/FL/kts and marks a missed poll STALE', async () => { const icao24 = 'a1b2c3'; const entity = { gevLabelModel: { title: 'OLD', details: [] } }; diff --git a/src/layers/flights/snapshotRenderer.js b/src/layers/flights/snapshotRenderer.js index 636e691fd..369fb4af2 100644 --- a/src/layers/flights/snapshotRenderer.js +++ b/src/layers/flights/snapshotRenderer.js @@ -179,8 +179,14 @@ export function createFlightSnapshotRenderer({ // as the enrichment path — the class's GLB/scale may have changed. if (prevMeta?.klass !== meta.klass) rendering._syncModelToClass(icao24); } else { + // Start where the fleet pass will draw it: the fleet renders behind + // real time, so a fresh contact's displayed position is its fix + // projected back to that delayed time. Creating it at the raw fix drew + // it ahead until the next fleet tick snapped it back. + const displayed = + motion._deadReckon(icao24, new Cesium.Cartesian3()) || position; const bb = flightState._billboardCollection.add({ - position, + position: displayed, image: aircraftIcon(rendering._iconKind(icao24, meta.klass)), width: isTracked ? 24 : 20, height: isTracked ? 24 : 20, From d9737d94f213826bce63ecf7d249faa812c2844f Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Tue, 6 Oct 2026 12:01:56 -0700 Subject: [PATCH 2/2] fix(military): start new contacts at their delayed display position The military fleet also renders behind real time and projects a fresh contact's first fix back to that delayed time, but its billboard was created at the raw fix. A new contact therefore appeared ahead of where the next fleet tick put it and jumped back. The snapshot renderer now receives the layer's motion and creates the billboard at the dead-reckoned delayed position. --- src/layers/military/index.js | 1 + src/layers/military/ownership.test.mjs | 98 +++++++++++++++++++++++++ src/layers/military/snapshotRenderer.js | 9 ++- 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/layers/military/index.js b/src/layers/military/index.js index 283c9b7f8..53a1a25b4 100644 --- a/src/layers/military/index.js +++ b/src/layers/military/index.js @@ -33,6 +33,7 @@ export function createMilitaryFlightLayer({ militaryRegistry: services.militaryRegistry, rendering: parts.rendering, tracking: parts.tracking, + motion: parts.motion, queries: parts.queries, }); parts.ingestion = createIngestion({ diff --git a/src/layers/military/ownership.test.mjs b/src/layers/military/ownership.test.mjs index 167900b7c..41081c137 100644 --- a/src/layers/military/ownership.test.mjs +++ b/src/layers/military/ownership.test.mjs @@ -113,3 +113,101 @@ test('a normalized source can retain its stale reason without changing standalon assert.equal(layer.getStats().error, null); assert.equal(layer.getStats().stale, true); }); + +test('a newly seen military contact appears at its delayed position, not ahead of it', async () => { + // The military fleet renders RENDER_DELAY_SEC behind real time, so a fresh + // contact's first displayed position is its fix projected back to that + // delayed time. Creating the billboard at the raw fix drew it ahead until + // the next fleet tick jumped it back. + const Cesium = await import('cesium'); + const { RENDER_DELAY_SEC } = await import('./policy.js'); + // The application's own scene services, as src/app/layers/militaryFlights.js + // composes them; only the surface samplers and registry are stubbed. + const supplied = { + picking: await import('../../data/pickRegistry.js'), + sprites: await import('../../data/spriteOrder.js'), + trails: await import('../../data/trailRenderer.js'), + aircraftPresentation: await import('../../data/tr3bRegistry.js'), + camera: await import('../../data/trackedCamera.js'), + labels: await import('../../data/detectionDraw.js'), + geoid: await import('../../data/geoid.js'), + focus: await import('../../data/focusDeemphasis.js'), + readout: await import('../../data/trackedReadout.js'), + context: await import('../../data/contextStore.js'), + render: await import('../../renderGovernor.js'), + recession: await import('../../data/aircraftRecession.js'), + groundFloor: await import('../../data/groundFloor.js'), + groundSnap: await import('../../data/groundSnap.js'), + meshFloor: { sampleMeshFloorCells: () => {} }, + militaryRegistry: { registerMilitaryIcaos: () => {} }, + }; + const fixLon = -97.6; + const fixLat = 30.3; + const speedMps = 200; + const fixAgeMs = 2_000; + const nowMs = Date.now(); + const layer = createMilitaryFlightLayer({ + services: supplied, + source: { + label: 'Fixture aircraft', + async getSnapshot() { + return { + source: 'Fixture aircraft', + complete: true, + observedAtMs: nowMs, + records: [ + { + id: 'ae0001', + latitude: fixLat, + longitude: fixLon, + onGround: false, + baroAltitudeM: 9_000, + courseDeg: 90, + speedMps, + positionTimeMs: nowMs - fixAgeMs, + contactTimeMs: nowMs - fixAgeMs, + }, + ], + }; + }, + }, + }); + const added = []; + layer.testing._setTrackedMilitaryRefreshStateForTest({ + icao24: 'seed00', + entity: null, + meta: { rawLat: 0, rawLon: 0, onGround: false }, + billboard: { + show: false, + position: Cesium.Cartesian3.fromDegrees(0, 0, 0), + }, + billboardCollection: { + show: false, + add(options) { + const billboard = { ...options }; + added.push(billboard); + return billboard; + }, + remove() {}, + }, + viewer: { camera: { positionCartographic: null }, scene: {} }, + tracked: false, + }); + await layer.update({ camera: { positionCartographic: null }, scene: {} }); + const billboard = added.find((entry) => entry.id === 'ae0001'); + assert.ok(billboard, 'the new contact gets a billboard'); + const shown = Cesium.Cartographic.fromCartesian(billboard.position); + const behindM = Cesium.Cartesian3.distance( + Cesium.Cartesian3.fromRadians(shown.longitude, shown.latitude, 0), + Cesium.Cartesian3.fromDegrees(fixLon, fixLat, 0), + ); + const expectedM = speedMps * (RENDER_DELAY_SEC - fixAgeMs / 1000); + assert.ok( + Cesium.Math.toDegrees(shown.longitude) < fixLon, + 'drawn behind the fix', + ); + assert.ok( + Math.abs(behindM - expectedM) < speedMps * 3, + `expected about ${Math.round(expectedM)} m behind the fix, got ${Math.round(behindM)} m`, + ); +}); diff --git a/src/layers/military/snapshotRenderer.js b/src/layers/military/snapshotRenderer.js index d36576cf7..58eb57972 100644 --- a/src/layers/military/snapshotRenderer.js +++ b/src/layers/military/snapshotRenderer.js @@ -23,6 +23,7 @@ export function createMilitarySnapshotRenderer({ militaryRegistry, rendering, tracking, + motion, queries, }) { const { warmGroundFloor, cachedGroundFloor, GROUND_FLOOR_LIFT_M } = @@ -142,8 +143,14 @@ export function createMilitarySnapshotRenderer({ // Round 5: depth policy is uniform (always depth-test-free, see // _groundDepthDistance) — nothing to flip on landing/takeoff. } else { + // Start where the fleet pass will draw it: the fleet renders behind + // real time, so a fresh contact's displayed position is its fix + // projected back to that delayed time. Creating it at the raw fix drew + // it ahead until the next fleet tick snapped it back. + const displayed = + motion._deadReckon(icao24, new Cesium.Cartesian3()) || position; const bb = flightState._billboardCollection.add({ - position, + position: displayed, image: aircraftIcon(rendering._iconKind(icao24, meta.klass)), width: isTracked ? 24 : 20, height: isTracked ? 24 : 20,