mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Separate flight observations and tracking from the scene renderer
This commit is contained in:
1 parent
4bd7f97cc4
commit
a0e5f04c30
10 files changed
+371
-187
No files matched your search
@@ -1660,7 +1660,7 @@ test('hasContact declines while a layer is disabled, whatever its maps still hol
|
||||
for (const [name, source, guard] of [
|
||||
// Flights asks its renderer whether the fleet is shown (phase 3).
|
||||
['flights', readLayerSource(new URL('./flights.js', import.meta.url)),
|
||||
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!(?:flightState\.)?_renderer\?\.fleet\.visible\(\)\s*\|\|\s*(?:flightState\.)?_billboards\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
||||
/hasContact\(\s*icao24,?\s*\)\s*\{\s*if\s*\(\s*!flightState\.enabled\s*\|\|\s*flightState\.records\.data\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
||||
// Military asks its renderer too (phase 3).
|
||||
['militaryFlights', readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
|
||||
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!(?:flightState\.)?_renderer\?\.fleet\.visible\(\)\s*\|\|\s*(?:flightState\.)?_billboards\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
||||
|
||||
@@ -29,7 +29,7 @@ export function createCivilFlightLayer({
|
||||
parts.evidence = createEvidence(context);
|
||||
parts.testing = createTesting(context);
|
||||
parts.queries = createQueries(context);
|
||||
const applySnapshot = createFlightSnapshotRenderer({
|
||||
parts.snapshots = createFlightSnapshotRenderer({
|
||||
flightState,
|
||||
records: flightState.records,
|
||||
militaryRegistry: services.militaryRegistry,
|
||||
@@ -45,7 +45,7 @@ export function createCivilFlightLayer({
|
||||
feed: flightState.feed,
|
||||
getQuery: (viewer) =>
|
||||
parts.controller._flightQuery(viewer || flightState._renderer?.view),
|
||||
applySnapshot,
|
||||
applySnapshot: (snapshot) => parts.snapshots.applySnapshot(snapshot),
|
||||
setSourceLabel: (source) => {
|
||||
layer.source = source;
|
||||
},
|
||||
|
||||
+124
-79
@@ -13,10 +13,30 @@ export function createLifecycle({
|
||||
const { ensureGeoidReady } = services.geoid;
|
||||
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
||||
|
||||
let initialized = false;
|
||||
let resumeFollow = false;
|
||||
|
||||
function removeSceneListeners() {
|
||||
parts.tracking._removeClickHandler();
|
||||
unregisterPickOwner('flights');
|
||||
if (flightState._preRenderRemove) {
|
||||
flightState._preRenderRemove();
|
||||
flightState._preRenderRemove = null;
|
||||
}
|
||||
if (flightState._trackedModelPreUpdateRemove) {
|
||||
flightState._trackedModelPreUpdateRemove();
|
||||
flightState._trackedModelPreUpdateRemove = null;
|
||||
}
|
||||
if (flightState._moveEndRemove) {
|
||||
flightState._moveEndRemove();
|
||||
flightState._moveEndRemove = null;
|
||||
}
|
||||
}
|
||||
|
||||
const methods = {
|
||||
/** Configure the source before initialization; an active layer keeps its owner. */
|
||||
setSource(source) {
|
||||
if (flightState._renderer)
|
||||
if (initialized)
|
||||
throw new Error('Configure the source before layer initialization');
|
||||
if (typeof source?.getSnapshot !== 'function')
|
||||
throw new TypeError('A snapshot source is required');
|
||||
@@ -35,36 +55,13 @@ export function createLifecycle({
|
||||
* @param {{ engine?: object }} [context] - LayerLifecycle's context.
|
||||
*/
|
||||
init(viewer, { engine = null } = {}) {
|
||||
if (flightState._renderer)
|
||||
throw new Error('Flight layer is already initialized');
|
||||
if (initialized) throw new Error('Flight layer is already initialized');
|
||||
if (typeof flightState.feed._source?.getSnapshot !== 'function')
|
||||
throw new TypeError('A snapshot source is required');
|
||||
if (flightState.lifetime.signal.aborted)
|
||||
flightState.lifetime = new AbortController();
|
||||
clearFocusTarget('flights');
|
||||
flightState._focusEvidenceIds.clear();
|
||||
const renderer = services.renderer.create({ viewer, engine });
|
||||
flightState._renderer = renderer;
|
||||
renderer.fleet.attach();
|
||||
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
|
||||
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
|
||||
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
||||
if (!flightState._preloadModel) {
|
||||
const epoch = flightState._modelEpoch;
|
||||
renderer.models
|
||||
.preload(resolveAsset(PLANE_MODEL_URL))
|
||||
.then((m) => {
|
||||
if (epoch === flightState._modelEpoch) {
|
||||
flightState._preloadModel = m;
|
||||
flightState._planeModelLoaded = true;
|
||||
} else {
|
||||
renderer.models.destroy(m);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* tracked plane just stays a billboard a beat longer */
|
||||
});
|
||||
}
|
||||
flightState._billboards = new Map();
|
||||
flightState._cullPositions.clear();
|
||||
flightState._detectionObjects = new Map();
|
||||
@@ -86,9 +83,9 @@ export function createLifecycle({
|
||||
flightState._trackedEntity = null;
|
||||
flightState._cockpitSubjectId = null;
|
||||
flightState._cockpitContactMode =
|
||||
document.body.classList.contains('cockpit-mode');
|
||||
globalThis.document?.body.classList.contains('cockpit-mode') === true;
|
||||
flightState._cockpitNearContacts = new Set();
|
||||
if (!flightState._cockpitModeListener) {
|
||||
if (globalThis.window && !flightState._cockpitModeListener) {
|
||||
flightState._cockpitModeListener = (event) =>
|
||||
parts.tracking._applyCockpitState(event?.detail);
|
||||
window.addEventListener(
|
||||
@@ -101,8 +98,6 @@ export function createLifecycle({
|
||||
parts.enrichment._ambientBudgetKnobs().ceil;
|
||||
flightState._enrichAmbientRefillAnchorMs = 0;
|
||||
|
||||
parts.tracking._installClickHandler();
|
||||
|
||||
// React to Military-layer toggles IMMEDIATELY (suppress/restore sweep)
|
||||
// instead of waiting out the 30 s poll (M2).
|
||||
if (!flightState._milActiveChangeUnsub) {
|
||||
@@ -111,9 +106,97 @@ export function createLifecycle({
|
||||
);
|
||||
}
|
||||
|
||||
renderer.fleet.restoreOrder();
|
||||
initialized = true;
|
||||
try {
|
||||
if (viewer || engine) methods.attachRenderer(viewer, { engine });
|
||||
} catch (error) {
|
||||
methods.destroy(viewer);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
console.log('[Data:Flights] Initialized with billboard icons');
|
||||
attachRenderer(viewer, { engine = null } = {}) {
|
||||
if (!initialized)
|
||||
throw new Error('Initialize the flight layer before attaching');
|
||||
if (flightState._renderer)
|
||||
throw new Error('Flight renderer already attached');
|
||||
const renderer = services.renderer.create({ viewer, engine });
|
||||
flightState._renderer = renderer;
|
||||
try {
|
||||
renderer.fleet.attach();
|
||||
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
|
||||
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
|
||||
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
||||
if (!flightState._preloadModel) {
|
||||
const epoch = flightState._modelEpoch;
|
||||
renderer.models
|
||||
.preload(resolveAsset(PLANE_MODEL_URL))
|
||||
.then((m) => {
|
||||
if (epoch === flightState._modelEpoch) {
|
||||
flightState._preloadModel = m;
|
||||
flightState._planeModelLoaded = true;
|
||||
} else {
|
||||
renderer.models.destroy(m);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* tracked plane just stays a billboard a beat longer */
|
||||
});
|
||||
}
|
||||
parts.snapshots.presentRecords();
|
||||
if (flightState._trackedIcao) {
|
||||
parts.tracking._trackFlight(flightState._trackedIcao, {
|
||||
resume: true,
|
||||
follow: resumeFollow,
|
||||
});
|
||||
}
|
||||
renderer.fleet.setVisible(flightState.enabled);
|
||||
renderer.models.setVisible(flightState.enabled);
|
||||
renderer.fleet.restoreOrder();
|
||||
if (flightState.enabled) {
|
||||
methods.enable();
|
||||
parts.tracking._applyPendingTrackingRestore();
|
||||
}
|
||||
} catch (error) {
|
||||
methods.detachRenderer(viewer);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
detachRenderer(viewer) {
|
||||
const renderer = flightState._renderer;
|
||||
if (!renderer) return;
|
||||
removeSceneListeners();
|
||||
releaseContinuousRender('flights');
|
||||
flightState._trackedCameraFrameStop?.();
|
||||
flightState._trackedCameraFrameStop = null;
|
||||
if (flightState._trackedEntity) {
|
||||
resumeFollow = renderer.follow.isFollowing(flightState._trackedEntity);
|
||||
if (resumeFollow) renderer.follow.release();
|
||||
renderer.tracked.remove(flightState._trackedEntity);
|
||||
flightState._trackedEntity = null;
|
||||
}
|
||||
parts.rendering._releaseTrackedModel();
|
||||
parts.tracking._detachTrail();
|
||||
parts.rendering._releaseModels();
|
||||
// Removing the model collection destroys it and its models.
|
||||
renderer.fleet.detach(viewer);
|
||||
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
||||
flightState._modelPending.clear();
|
||||
flightState._modelGen.clear();
|
||||
if (flightState._preloadModel) {
|
||||
renderer.models.destroy(flightState._preloadModel);
|
||||
flightState._preloadModel = null;
|
||||
}
|
||||
flightState._planeModelLoaded = false;
|
||||
flightState._billboards.clear();
|
||||
flightState._cullPositions.clear();
|
||||
flightState._detectionObjects.clear();
|
||||
flightState._displayFloorState.clear();
|
||||
flightState._groundSnap.clear();
|
||||
parts.motion._resetTrackedDisplay();
|
||||
renderer.destroy();
|
||||
flightState._renderer = null;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -121,11 +204,12 @@ export function createLifecycle({
|
||||
*/
|
||||
enable() {
|
||||
const renderer = flightState._renderer;
|
||||
flightState.enabled = true;
|
||||
renderer?.fleet.setVisible(true);
|
||||
holdContinuousRender('flights'); // per-frame animator (perf wave 2)
|
||||
if (renderer) holdContinuousRender('flights'); // per-frame animator (perf wave 2)
|
||||
renderer?.models.setVisible(true);
|
||||
parts.tracking._setCockpitContactMode(
|
||||
document.body.classList.contains('cockpit-mode'),
|
||||
globalThis.document?.body.classList.contains('cockpit-mode') === true,
|
||||
);
|
||||
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
|
||||
// only reads geoidHeight() synchronously after this resolves (guarded by
|
||||
@@ -140,6 +224,7 @@ export function createLifecycle({
|
||||
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
|
||||
});
|
||||
}
|
||||
if (!renderer) return;
|
||||
parts.tracking._installClickHandler();
|
||||
registerPickOwner('flights', (pickedId) =>
|
||||
flightState._billboards.has(pickedId),
|
||||
@@ -181,6 +266,7 @@ export function createLifecycle({
|
||||
* Also clears any active flight tracking so the camera is released.
|
||||
*/
|
||||
disable() {
|
||||
flightState.enabled = false;
|
||||
parts.controller._abortActiveUpdates();
|
||||
parts.tracking._cancelPendingTrackingRestore();
|
||||
flightState._renderer?.fleet.setVisible(false);
|
||||
@@ -189,22 +275,7 @@ export function createLifecycle({
|
||||
flightState._renderer?.models.setVisible(false);
|
||||
parts.tracking._clearTracking();
|
||||
parts.tracking._destroyTrail();
|
||||
// Leave the selection dispatcher (clicks, Escape) while disabled to
|
||||
// avoid intercepting input when the layer is off
|
||||
parts.tracking._removeClickHandler();
|
||||
unregisterPickOwner('flights');
|
||||
if (flightState._preRenderRemove) {
|
||||
flightState._preRenderRemove();
|
||||
flightState._preRenderRemove = null;
|
||||
}
|
||||
if (flightState._trackedModelPreUpdateRemove) {
|
||||
flightState._trackedModelPreUpdateRemove();
|
||||
flightState._trackedModelPreUpdateRemove = null;
|
||||
}
|
||||
if (flightState._moveEndRemove) {
|
||||
flightState._moveEndRemove();
|
||||
flightState._moveEndRemove = null;
|
||||
}
|
||||
removeSceneListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -213,6 +284,8 @@ export function createLifecycle({
|
||||
* @param {object|null} viewer - The viewer the layer was drawn on.
|
||||
*/
|
||||
destroy(viewer) {
|
||||
initialized = false;
|
||||
flightState.enabled = false;
|
||||
flightState.lifetime.abort();
|
||||
flightState._enrichActive = 0;
|
||||
flightState._enrichLastDispatchMs = 0;
|
||||
@@ -221,7 +294,6 @@ export function createLifecycle({
|
||||
parts.tracking._clearTracking();
|
||||
parts.tracking._destroyTrail();
|
||||
parts.tracking._cancelPendingTrackingRestore();
|
||||
parts.tracking._removeClickHandler();
|
||||
if (flightState._milActiveChangeUnsub) {
|
||||
flightState._milActiveChangeUnsub();
|
||||
flightState._milActiveChangeUnsub = null;
|
||||
@@ -233,33 +305,7 @@ export function createLifecycle({
|
||||
);
|
||||
flightState._cockpitModeListener = null;
|
||||
}
|
||||
unregisterPickOwner('flights');
|
||||
if (flightState._preRenderRemove) {
|
||||
flightState._preRenderRemove();
|
||||
flightState._preRenderRemove = null;
|
||||
}
|
||||
if (flightState._trackedModelPreUpdateRemove) {
|
||||
flightState._trackedModelPreUpdateRemove();
|
||||
flightState._trackedModelPreUpdateRemove = null;
|
||||
}
|
||||
if (flightState._moveEndRemove) {
|
||||
flightState._moveEndRemove();
|
||||
flightState._moveEndRemove = null;
|
||||
}
|
||||
parts.rendering._releaseModels();
|
||||
// Removing the model collection destroys it and its models.
|
||||
flightState._renderer?.fleet.detach(viewer);
|
||||
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
||||
flightState._modelPending.clear();
|
||||
flightState._modelGen.clear();
|
||||
if (flightState._preloadModel) {
|
||||
flightState._renderer?.models.destroy(flightState._preloadModel);
|
||||
flightState._preloadModel = null;
|
||||
}
|
||||
flightState._planeModelLoaded = false;
|
||||
flightState._billboards.clear();
|
||||
flightState._cullPositions.clear();
|
||||
flightState._detectionObjects.clear();
|
||||
methods.detachRenderer(viewer);
|
||||
flightState.records.data.clear();
|
||||
flightState._positionHistory.clear();
|
||||
flightState._displayCourse.clear();
|
||||
@@ -272,6 +318,7 @@ export function createLifecycle({
|
||||
flightState._enrichDripTimer = null;
|
||||
}
|
||||
flightState.records.missingPolls.clear();
|
||||
flightState.records.geoidNCache.clear();
|
||||
flightState._focusEvidenceIds.clear();
|
||||
flightState.feed._count = 0;
|
||||
flightState.feed._lastUpdate = null;
|
||||
@@ -287,8 +334,6 @@ export function createLifecycle({
|
||||
coverage: flightState.feed._lastCoverage,
|
||||
};
|
||||
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
|
||||
flightState._renderer?.destroy();
|
||||
flightState._renderer = null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -146,3 +146,100 @@ test('voice context reads a track that rounds to north as 0°, not 360°', () =>
|
||||
'0°',
|
||||
);
|
||||
});
|
||||
|
||||
test('a live flight keeps observations, selection and freshness across renderer lifetimes', async () => {
|
||||
const { createFakeEngine } =
|
||||
await import('../../globe/testing/fakeEngine.js');
|
||||
const { aircraftAtlasLayout, createAircraftNavaraRenderer } =
|
||||
await import('../aircraft/render/navara.js');
|
||||
const { createApplicationFlights } =
|
||||
await import('../../app/layers/flights.js');
|
||||
const first = createFakeEngine();
|
||||
const second = createFakeEngine();
|
||||
const epoch = Date.now();
|
||||
let polls = 0;
|
||||
const atl = aircraftAtlasLayout();
|
||||
const source = {
|
||||
label: 'QA',
|
||||
async getSnapshot() {
|
||||
polls++;
|
||||
return {
|
||||
source: 'QA',
|
||||
observedAtMs: epoch + polls * 1000,
|
||||
ageMs: 0,
|
||||
complete: true,
|
||||
records: [
|
||||
{
|
||||
id: '71be26',
|
||||
callsign: 'QA',
|
||||
latitude: 40.25,
|
||||
longitude: -125.58 + polls / 1000,
|
||||
ellipsoidAltitudeM: 9000,
|
||||
baroAltitudeM: 8800,
|
||||
courseDeg: 90,
|
||||
speedMps: 200,
|
||||
positionTimeMs: epoch + polls * 1000,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
const layer = createApplicationFlights({
|
||||
source,
|
||||
surface: {
|
||||
groundSnap: { createGroundSnap: () => ({ clear() {}, forget() {} }) },
|
||||
groundFloor: {
|
||||
cachedGroundFloor: () => null,
|
||||
warmGroundFloor() {},
|
||||
floorAltitudeM: (h) => h,
|
||||
},
|
||||
meshFloor: { sampleMeshFloorCells() {} },
|
||||
},
|
||||
militaryRegistry: {
|
||||
isMilitaryIcao: () => false,
|
||||
isMilitaryLayerActive: () => false,
|
||||
refreshMilitaryRegistryIfStale() {},
|
||||
onMilitaryLayerActiveChange: () => () => {},
|
||||
},
|
||||
renderer: {
|
||||
create: ({ engine }) =>
|
||||
createAircraftNavaraRenderer({
|
||||
engine,
|
||||
layerId: 'flights',
|
||||
order: 1,
|
||||
headingOffsetDeg: 180,
|
||||
drawAtlas: async () => ({
|
||||
canvas: { width: atl.width, height: atl.height },
|
||||
cells: atl.cells,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
try {
|
||||
layer.init(null);
|
||||
layer.enable();
|
||||
await layer.update(null);
|
||||
assert.equal(layer.hasContact('71BE26'), true);
|
||||
assert.equal(layer.getAnalystRecords().length, 1);
|
||||
assert.equal(layer.findByQuery('QA').icao24, '71be26');
|
||||
layer.attachRenderer(null, { engine: first });
|
||||
assert.equal(layer.qaDrawn().length, 1);
|
||||
assert.equal(layer.trackById('71be26'), true);
|
||||
layer.detachRenderer();
|
||||
assert.equal(first.camera.following(), null);
|
||||
assert.equal(layer.getTrackedInfo().icao24, '71be26');
|
||||
await layer.update(null);
|
||||
assert.equal(layer.getStats().lastUpdate, epoch + 2000);
|
||||
layer.attachRenderer(null, { engine: second });
|
||||
assert.equal(second.camera.following()?.label.id, 'flights:71be26');
|
||||
assert.equal(layer.qaDrawn().length, 1);
|
||||
assert.equal(polls, 2, 'attaching either renderer performs no source poll');
|
||||
layer.disable();
|
||||
assert.equal(layer.hasContact('71be26'), null);
|
||||
assert.deepEqual(layer.getAnalystRecords(), []);
|
||||
} finally {
|
||||
layer.destroy(null);
|
||||
first.destroy();
|
||||
second.destroy();
|
||||
}
|
||||
});
|
||||
@@ -574,7 +574,7 @@ export function createQueries({
|
||||
* @returns {Array<{id: string, icao24: string, callsign: string|null, position: Cesium.Cartesian3, distance: number, aircraftClass: string|null, altitudeM: number|null, velocityMps: number|null, track: number|null}>}
|
||||
*/
|
||||
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
|
||||
if (!center || !flightState._renderer?.fleet.visible()) return [];
|
||||
if (!center || !flightState.enabled) return [];
|
||||
|
||||
const limit = Number.isFinite(maxCount)
|
||||
? Math.max(1, Math.floor(maxCount))
|
||||
@@ -584,11 +584,14 @@ export function createQueries({
|
||||
|
||||
const nearby = [];
|
||||
|
||||
for (const [icao24, bb] of flightState._billboards) {
|
||||
for (const [icao24, info] of flightState.records.data) {
|
||||
const bb = flightState._billboards.get(icao24);
|
||||
if (!bb && flightState._renderer) continue;
|
||||
const isTracked = icao24 === flightState._trackedIcao;
|
||||
// Keep planes rendered as a 3D model (billboard hidden) so proximity counts
|
||||
// don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden.
|
||||
if (
|
||||
bb &&
|
||||
!aircraftIncludedInNearby({
|
||||
isTracked,
|
||||
billboardShown: bb.show,
|
||||
@@ -601,13 +604,13 @@ export function createQueries({
|
||||
const trackedPos = isTracked
|
||||
? parts.motion._trackedDisplayCached()
|
||||
: null; // cached, no recompute (anti-jitter)
|
||||
const pos = trackedPos || bb.position;
|
||||
const pos =
|
||||
trackedPos || bb?.position || parts.motion._deadReckon(icao24);
|
||||
if (!pos) continue;
|
||||
|
||||
const distance = geo.cartesian3.distance(center, pos);
|
||||
if (distance > maxRange) continue;
|
||||
|
||||
const info = flightState.records.data.get(icao24);
|
||||
const callsign = info?.callsign?.trim() || null;
|
||||
nearby.push({
|
||||
// Label. Callers that need identity read `icao24` (Context cohorts do).
|
||||
@@ -657,16 +660,13 @@ export function createQueries({
|
||||
* holds no data and therefore cannot answer.
|
||||
*/
|
||||
hasContact(icao24) {
|
||||
if (
|
||||
!flightState._renderer?.fleet.visible() ||
|
||||
flightState._billboards.size === 0
|
||||
)
|
||||
if (!flightState.enabled || flightState.records.data.size === 0)
|
||||
return null;
|
||||
if (!icao24) return false;
|
||||
const id = String(icao24).trim();
|
||||
return (
|
||||
flightState._billboards.has(id) ||
|
||||
flightState._billboards.has(id.toLowerCase())
|
||||
flightState.records.data.has(id) ||
|
||||
flightState.records.data.has(id.toLowerCase())
|
||||
);
|
||||
},
|
||||
|
||||
@@ -726,10 +726,7 @@ export function createQueries({
|
||||
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
|
||||
*/
|
||||
getAnalystRecords(maxCount = 2000) {
|
||||
if (
|
||||
!flightState._renderer?.fleet.visible() ||
|
||||
flightState.records.data.size === 0
|
||||
)
|
||||
if (!flightState.enabled || flightState.records.data.size === 0)
|
||||
return [];
|
||||
const limit = Number.isFinite(maxCount)
|
||||
? Math.max(1, Math.floor(maxCount))
|
||||
|
||||
@@ -25,14 +25,94 @@ export function createFlightSnapshotRenderer({
|
||||
const { refreshMilitaryRegistryIfStale, isMilitaryIcao } = militaryRegistry;
|
||||
const { warmGroundFloor } = groundFloor;
|
||||
const { sampleMeshFloorCells } = meshFloor;
|
||||
return function applySnapshot(snapshot, viewer) {
|
||||
function presentContact(icao24, meta, position, prevMeta, groundFlipped) {
|
||||
const isTracked = icao24 === flightState._trackedIcao;
|
||||
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(cyberSonarBaseAlpha(bb));
|
||||
}
|
||||
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 {
|
||||
// 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, and a grounded contact is then
|
||||
// lifted onto the floor under that delayed position. Creating it at
|
||||
// the raw fix drew it ahead until the next fleet tick snapped it back;
|
||||
// skipping the floor started a taxiing contact low and then raised it.
|
||||
const delayed = motion._deadReckon(icao24, geo.cartesian3.create());
|
||||
const displayed =
|
||||
(delayed &&
|
||||
motion._floorGroundedDisplayPosition(
|
||||
icao24,
|
||||
flightState.records.data.get(icao24) || meta,
|
||||
delayed,
|
||||
rendering._modelOwnsVisual(icao24),
|
||||
)) ||
|
||||
position;
|
||||
const bb = flightState._renderer.fleet.add({
|
||||
position: displayed,
|
||||
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: geo.cartesian3.ZERO,
|
||||
color: isTracked ? 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);
|
||||
}
|
||||
}
|
||||
|
||||
function presentRecords() {
|
||||
for (const [id, meta] of records.data) {
|
||||
const position = geo.cartesian3.fromDegrees(
|
||||
meta.rawLon,
|
||||
meta.rawLat,
|
||||
meta.renderAltitudeM,
|
||||
);
|
||||
presentContact(id, meta, position, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
function applySnapshot(snapshot) {
|
||||
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._renderer?.view)?.camera?.positionCartographic ||
|
||||
flightState._renderer?.view.camera?.positionCartographic ||
|
||||
null;
|
||||
const viewerLatDeg = viewerCarto
|
||||
? geo.math.toDegrees(viewerCarto.latitude)
|
||||
@@ -55,8 +135,8 @@ export function createFlightSnapshotRenderer({
|
||||
const isMil = isMilitaryIcao(icao24);
|
||||
if (isMil && tracking._militaryLayerSuppresses(icao24)) {
|
||||
const dupe = flightState._billboards.get(icao24);
|
||||
if (dupe) {
|
||||
flightState._renderer.fleet.remove(dupe);
|
||||
if (records.data.has(icao24)) {
|
||||
if (dupe) flightState._renderer.fleet.remove(dupe);
|
||||
flightState._billboards.delete(icao24);
|
||||
rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
|
||||
records.forget(icao24);
|
||||
@@ -158,71 +238,8 @@ export function createFlightSnapshotRenderer({
|
||||
}
|
||||
}
|
||||
|
||||
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(cyberSonarBaseAlpha(bb));
|
||||
}
|
||||
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 {
|
||||
// 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, and a grounded contact is then
|
||||
// lifted onto the floor under that delayed position. Creating it at
|
||||
// the raw fix drew it ahead until the next fleet tick snapped it back;
|
||||
// skipping the floor started a taxiing contact low and then raised it.
|
||||
const delayed = motion._deadReckon(icao24, geo.cartesian3.create());
|
||||
const displayed =
|
||||
(delayed &&
|
||||
motion._floorGroundedDisplayPosition(
|
||||
icao24,
|
||||
flightState.records.data.get(icao24) || meta,
|
||||
delayed,
|
||||
rendering._modelOwnsVisual(icao24),
|
||||
)) ||
|
||||
position;
|
||||
const bb = flightState._renderer.fleet.add({
|
||||
position: displayed,
|
||||
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: geo.cartesian3.ZERO,
|
||||
color: isTracked ? 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);
|
||||
}
|
||||
if (!flightState._renderer) continue;
|
||||
presentContact(icao24, meta, position, prevMeta, groundFlipped);
|
||||
|
||||
// Takeoff while TRACKED: ground traffic drew no trail, so start one
|
||||
// from the fresh airborne history (touchdown needs no action — the
|
||||
@@ -243,7 +260,7 @@ export function createFlightSnapshotRenderer({
|
||||
// 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) {
|
||||
for (const icao24 of records.data.keys()) {
|
||||
if (currentIcaos.has(icao24)) continue;
|
||||
const absence = records.absence(icao24, {
|
||||
complete: snapshot.complete,
|
||||
@@ -272,7 +289,8 @@ export function createFlightSnapshotRenderer({
|
||||
tracking._clearTracking(false, { evicted: true });
|
||||
}
|
||||
|
||||
flightState._renderer.fleet.remove(bb);
|
||||
const bb = flightState._billboards.get(icao24);
|
||||
if (bb) flightState._renderer.fleet.remove(bb);
|
||||
flightState._billboards.delete(icao24);
|
||||
rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
|
||||
records.forget(icao24);
|
||||
@@ -286,6 +304,9 @@ export function createFlightSnapshotRenderer({
|
||||
// Fresh courses arrived — force a rotation pass on the next fleet tick
|
||||
flightState._lastCamPoseSig = '';
|
||||
|
||||
const accepted = { count: records.data.size, ids: acceptedSnapshotIcaos };
|
||||
if (!flightState._renderer) return accepted;
|
||||
|
||||
// Ambient type enrichment: give ON-SCREEN planes real types (bounded
|
||||
// sweep — see _sweepAmbientEnrichment; internally fail-silent).
|
||||
enrichment._sweepAmbientEnrichment();
|
||||
@@ -337,6 +358,8 @@ export function createFlightSnapshotRenderer({
|
||||
// in place is safe (the DR extrapolates a zero-velocity fix).
|
||||
motion._refloorStaleGroundedContacts(currentIcaos);
|
||||
|
||||
return { count: flightState._billboards.size, ids: acceptedSnapshotIcaos };
|
||||
};
|
||||
return accepted;
|
||||
}
|
||||
|
||||
return { applySnapshot, presentRecords };
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { ENRICH_AMBIENT_BUDGET_CEIL } from './policy.js';
|
||||
|
||||
export function createFlightState({ source, services }) {
|
||||
const { createGroundSnap } = services.groundSnap;
|
||||
const flightState = { lifetime: new AbortController() };
|
||||
const flightState = { lifetime: new AbortController(), enabled: false };
|
||||
flightState.feed = createFlightFeed(source);
|
||||
flightState.records = new FlightRecords({
|
||||
...services.geoid,
|
||||
|
||||
@@ -70,6 +70,7 @@ export function createTesting({
|
||||
viewer,
|
||||
adopt: { billboardCollection, modelCollection },
|
||||
});
|
||||
flightState.enabled = flightState._renderer.fleet.visible();
|
||||
flightState._billboards = new Map([[icao24, billboard]]);
|
||||
flightState._cullPositions.clear();
|
||||
flightState._models.clear();
|
||||
|
||||
@@ -378,6 +378,14 @@ export function createTracking({
|
||||
flightState._trailPositions.push(geo.cartesian3.clone(fix.position));
|
||||
}
|
||||
}
|
||||
_attachTrail();
|
||||
const oldestFixEpochSec = history.length
|
||||
? history[0].timeMs / 1000
|
||||
: Infinity;
|
||||
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
||||
}
|
||||
|
||||
function _attachTrail() {
|
||||
if (!flightState._trail && flightState._renderer) {
|
||||
flightState._trail = flightState._renderer.trails.create({
|
||||
color: TRAIL_COLOR,
|
||||
@@ -440,11 +448,6 @@ export function createTracking({
|
||||
});
|
||||
}
|
||||
_refreshTrailDisplay();
|
||||
|
||||
const oldestFixEpochSec = history.length
|
||||
? history[0].timeMs / 1000
|
||||
: Infinity;
|
||||
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -565,6 +568,13 @@ export function createTracking({
|
||||
|
||||
function _destroyTrail() {
|
||||
_clearTrail();
|
||||
_detachTrail();
|
||||
}
|
||||
|
||||
function _detachTrail() {
|
||||
if (flightState._trailHeadEntity)
|
||||
flightState._renderer?.trails.removeHead(flightState._trailHeadEntity);
|
||||
flightState._trailHeadEntity = null;
|
||||
if (flightState._trail) {
|
||||
flightState._trail.destroy();
|
||||
flightState._trail = null;
|
||||
@@ -870,8 +880,11 @@ export function createTracking({
|
||||
* @param {string} icao24 - ICAO 24-bit transponder address to track.
|
||||
*/
|
||||
|
||||
function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
|
||||
_clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
|
||||
function _trackFlight(
|
||||
icao24,
|
||||
{ origin = 'programmatic', resume = false, follow = true } = {},
|
||||
) {
|
||||
if (!resume) _clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
|
||||
|
||||
const bb = flightState._billboards.get(icao24);
|
||||
const info = flightState.records.data.get(icao24);
|
||||
@@ -998,14 +1011,20 @@ export function createTracking({
|
||||
refreshTrackedReadout(flightState._trackedEntity);
|
||||
// Camera follows the tracked entity (the renderer cancels any flight
|
||||
// first and applies the one exact tracked frame).
|
||||
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
|
||||
flightState._trackedEntity,
|
||||
);
|
||||
if (follow) {
|
||||
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
|
||||
flightState._trackedEntity,
|
||||
);
|
||||
}
|
||||
|
||||
// Track-history trail (PRD F1): seed from local history + async backfill.
|
||||
// Ground traffic draws NO trail (a taxi path is noise, not a track) — if the
|
||||
// plane takes off while tracked, the on_ground→false transition in update()
|
||||
// starts one.
|
||||
if (resume) {
|
||||
_attachTrail();
|
||||
return;
|
||||
}
|
||||
parts.enrichment._requestTypeEnrichment(icao24, true); // tracked plane — front of the enrichment queue
|
||||
parts.enrichment._requestRouteEnrichment(icao24);
|
||||
// Round 2 (owner): grounded contacts get trails too — a landed-but-taxiing
|
||||
@@ -1040,23 +1059,24 @@ export function createTracking({
|
||||
*/
|
||||
|
||||
function _onMilitaryActiveChange(active) {
|
||||
if (!flightState._renderer?.fleet.attached()) return;
|
||||
if (!flightState.enabled) return;
|
||||
if (active) {
|
||||
for (const [icao24, bb] of flightState._billboards) {
|
||||
for (const icao24 of flightState.records.data.keys()) {
|
||||
if (!isMilitaryIcao(icao24) || icao24 === flightState._trackedIcao)
|
||||
continue;
|
||||
flightState._renderer.fleet.remove(bb);
|
||||
const bb = flightState._billboards.get(icao24);
|
||||
if (bb) flightState._renderer.fleet.remove(bb);
|
||||
flightState._billboards.delete(icao24);
|
||||
parts.rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
|
||||
flightState.records.data.delete(icao24);
|
||||
flightState.records.forget(icao24);
|
||||
flightState._cullPositions.delete(icao24);
|
||||
flightState._positionHistory.delete(icao24);
|
||||
flightState._displayCourse.delete(icao24);
|
||||
flightState._groundSnap.forget(icao24);
|
||||
flightState.records.missingPolls.delete(icao24);
|
||||
}
|
||||
flightState.feed._count = flightState._billboards.size;
|
||||
} else if (flightState._renderer.fleet.visible()) {
|
||||
flightState.feed._count = flightState.records.data.size;
|
||||
} else {
|
||||
// Fire-and-forget refresh; only while the layer is actually enabled.
|
||||
// The query reads the renderer's camera (no viewer argument needed).
|
||||
void flightsLayer.update(null).catch((error) => {
|
||||
@@ -1196,6 +1216,7 @@ export function createTracking({
|
||||
_backfillTrail,
|
||||
_clearTrail,
|
||||
_destroyTrail,
|
||||
_detachTrail,
|
||||
_clearTracking,
|
||||
_militaryLayerSuppresses,
|
||||
_applyPendingTrackingRestore,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createTracking } from './tracking.js';
|
||||
function handoff(update) {
|
||||
return createTracking({
|
||||
flightState: {
|
||||
_renderer: { fleet: { attached: () => true, visible: () => true } },
|
||||
enabled: true,
|
||||
},
|
||||
services: Object.fromEntries(
|
||||
[
|
||||
|
||||
Reference in new issue
Block a user