mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Preserve military aircraft and classification across renderer handoffs
This commit is contained in:
1 parent
1bda760763
commit
e08ddd787b
15 files changed
+360
-302
No files matched your search
@@ -1353,6 +1353,7 @@
|
||||
"src/layers/flights/index.js",
|
||||
"src/layers/flights/ingestion.js",
|
||||
"src/layers/flights/lifecycle.js",
|
||||
"src/layers/aircraft/sceneBinding.js",
|
||||
"src/layers/flights/motion.js",
|
||||
"src/layers/flights/policy.js",
|
||||
"src/layers/flights/queries.js",
|
||||
@@ -1433,6 +1434,7 @@
|
||||
"src/layers/military/controller.js",
|
||||
"src/layers/military/index.js",
|
||||
"src/layers/military/ingestion.js",
|
||||
"src/layers/aircraft/sceneBinding.js",
|
||||
"src/layers/military/lifecycle.js",
|
||||
"src/layers/military/motion.js",
|
||||
"src/layers/military/policy.js",
|
||||
@@ -2868,6 +2870,7 @@
|
||||
"src/layers/flights/index.js",
|
||||
"src/layers/flights/ingestion.js",
|
||||
"src/layers/flights/lifecycle.js",
|
||||
"src/layers/aircraft/sceneBinding.js",
|
||||
"src/layers/flights/motion.js",
|
||||
"src/layers/flights/policy.js",
|
||||
"src/layers/flights/queries.js",
|
||||
@@ -3927,6 +3930,7 @@
|
||||
"src/layers/flights/index.js",
|
||||
"src/layers/flights/ingestion.js",
|
||||
"src/layers/flights/lifecycle.js",
|
||||
"src/layers/aircraft/sceneBinding.js",
|
||||
"src/layers/flights/motion.js",
|
||||
"src/layers/flights/policy.js",
|
||||
"src/layers/flights/queries.js",
|
||||
|
||||
@@ -10,9 +10,13 @@ if (
|
||||
)
|
||||
)
|
||||
throw new Error('Supply an unreserved local keyless preview URL');
|
||||
const military = process.argv.includes('--military');
|
||||
const layerId = military ? 'military' : 'flights';
|
||||
const aircraftId = military ? 'aaa052' : '71be26';
|
||||
url.search = '?engine=navara&welcome=0';
|
||||
url.hash =
|
||||
'v=2&lat=40.2539&lon=-125.5779&alt=21130&heading=360&pitch=-35&map=sentinel2&l=f&lo=f.e.1_f.t.71be26';
|
||||
'v=2&lat=40.2539&lon=-125.5779&alt=21130&heading=360&pitch=-35&map=sentinel2' +
|
||||
(military ? '&l=m&lo=f.e.1_f.u.aaa052' : '&l=f&lo=f.e.1_f.t.71be26');
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--use-angle=metal', '--enable-gpu'],
|
||||
@@ -23,14 +27,24 @@ try {
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await page.setViewport({ width: 1280, height: 720 });
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
await page.evaluateOnNewDocument((layerId, aircraftId) => {
|
||||
window.__qaAircraft = {layerId, aircraftId};
|
||||
const fetch = window.fetch.bind(window);
|
||||
window.__flightPolls = 0;
|
||||
const interval = window.setInterval.bind(window);
|
||||
window.setInterval = function (callback, delay, ...args) {
|
||||
if (typeof callback !== 'function') return interval(callback, delay, ...args);
|
||||
const id = interval(function (...values) {
|
||||
if (window.__handoff?.timer === id) window.__handoff.ticks++;
|
||||
return callback.apply(this, values);
|
||||
}, delay, ...args);
|
||||
return id;
|
||||
};
|
||||
window.fetch = (input, options) => {
|
||||
const path = new URL(input.url ?? input, document.baseURI).pathname;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (path === '/api/flights') {
|
||||
window.__flightPolls++;
|
||||
if (layerId === 'flights') window.__flightPolls++;
|
||||
return Promise.resolve(
|
||||
Response.json({
|
||||
time: now,
|
||||
@@ -58,15 +72,21 @@ try {
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (path === '/api/military')
|
||||
return Promise.resolve(Response.json({ now: Date.now(), ac: [] }));
|
||||
if (path === '/api/military') {
|
||||
if (layerId === 'military') window.__flightPolls++;
|
||||
return Promise.resolve(Response.json({now: Date.now(), ac: [{
|
||||
hex: 'aaa052', flight: 'GEVMIL', lon: -125.5779, lat: 40.2539,
|
||||
alt_geom: 31825, alt_baro: 31825, gs: 389, track: 45,
|
||||
seen: 0, seen_pos: 0, dbFlags: 1, type: 'adsb_icao',
|
||||
}]}));
|
||||
}
|
||||
if (path === '/api/flights/track')
|
||||
return Promise.resolve(Response.json({ path: [] }));
|
||||
if (path.startsWith('/api/flights/') || path.startsWith('/api/openai/'))
|
||||
return Promise.resolve(Response.json({}));
|
||||
return fetch(input, options);
|
||||
};
|
||||
});
|
||||
}, layerId, aircraftId);
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
const ready = (kind) =>
|
||||
page.waitForFunction(
|
||||
@@ -92,16 +112,16 @@ try {
|
||||
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
|
||||
throw Error('Keyless preview required');
|
||||
styleManager.cockpitView.setVisionMode('thermal');
|
||||
const entry = dataManager.layers.get('flights');
|
||||
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
|
||||
window.__handoff = {
|
||||
dataManager,
|
||||
entry,
|
||||
row: document.querySelector('[data-layer-id="flights"] .data-toggle-btn'),
|
||||
row: document.querySelector(`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`),
|
||||
timer: entry.intervalId,
|
||||
ticks: 0,
|
||||
model: entry.module,
|
||||
pollCount: window.__flightPolls,
|
||||
timestamp: entry.module.getStats().lastUpdate,
|
||||
started: Date.now(),
|
||||
};
|
||||
});
|
||||
// Refuse the first Cesium canvas. The application must roll back with the
|
||||
@@ -127,9 +147,9 @@ try {
|
||||
const {dataManager, styleManager} = window.__godsEyeView;
|
||||
return new URL(location.href).searchParams.get('engine') === 'navara' &&
|
||||
dataManager === window.__handoff.dataManager &&
|
||||
dataManager.layers.get('flights') === window.__handoff.entry &&
|
||||
dataManager.layers.get(window.__qaAircraft.layerId) === window.__handoff.entry &&
|
||||
styleManager.getCockpitState().active &&
|
||||
styleManager.getAircraftTrackingTarget()?.id === '71be26';
|
||||
styleManager.getAircraftTrackingTarget()?.id === window.__qaAircraft.aircraftId;
|
||||
}), 'a failed scene must leave the live session usable');
|
||||
console.log('PASS failed renderer rolled back to the same flight and cockpit');
|
||||
|
||||
@@ -139,7 +159,7 @@ try {
|
||||
const result = await page.evaluate(() => {
|
||||
const { dataManager, engine, styleManager } = window.__godsEyeView;
|
||||
const before = window.__handoff;
|
||||
const entry = dataManager.layers.get('flights');
|
||||
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
|
||||
const cockpit = styleManager.getCockpitState();
|
||||
return {
|
||||
sameOwner:
|
||||
@@ -147,18 +167,18 @@ try {
|
||||
before.entry === entry &&
|
||||
before.model === entry.module,
|
||||
sameRow: Boolean(before.row) &&
|
||||
before.row === document.querySelector('[data-layer-id="flights"] .data-toggle-btn'),
|
||||
before.row === document.querySelector(`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`),
|
||||
sameTimer:
|
||||
before.timer === entry.intervalId && entry.intervalId != null,
|
||||
sourcePolls: window.__flightPolls - before.pollCount,
|
||||
elapsed: Date.now() - before.started,
|
||||
timerTicks: before.ticks,
|
||||
stamp: entry.module.getStats().lastUpdate,
|
||||
initialStamp: before.timestamp,
|
||||
id: cockpit.subject?.id,
|
||||
active: cockpit.active,
|
||||
mode: cockpit.visionMode,
|
||||
hud: !document.querySelector('#cockpit-hud').hidden,
|
||||
drawn: entry.module.qaDrawn().some((row) => row.id === '71be26'),
|
||||
drawn: entry.module.qaDrawn().some((row) => row.id === window.__qaAircraft.aircraftId),
|
||||
height: engine.camera.height(),
|
||||
};
|
||||
});
|
||||
@@ -171,10 +191,10 @@ try {
|
||||
result.drawn,
|
||||
JSON.stringify(result),
|
||||
);
|
||||
assert.equal(result.id, '71be26');
|
||||
assert.equal(result.id, aircraftId);
|
||||
assert.equal(result.mode, 'thermal');
|
||||
if (result.elapsed < 30000) {
|
||||
assert.equal(result.sourcePolls, 0);
|
||||
assert.ok(result.sourcePolls <= result.timerTicks, 'switch must not add a source request');
|
||||
if (result.timerTicks === 0) {
|
||||
assert.equal(result.stamp, result.initialStamp);
|
||||
}
|
||||
await page.waitForFunction(
|
||||
|
||||
@@ -124,12 +124,12 @@ export function createApplicationCatalog({
|
||||
),
|
||||
);
|
||||
const military = once(() =>
|
||||
createApplicationMilitary({
|
||||
sessionModel('military', () => createApplicationMilitary({
|
||||
surface,
|
||||
source: sources.military,
|
||||
militaryRegistry,
|
||||
resolveAsset,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
const vessels = once(() =>
|
||||
createApplicationVessels({
|
||||
|
||||
@@ -482,7 +482,8 @@ test('GERMAN: an existing untracked military billboard repaints each poll withou
|
||||
floorAltitudeM: (value) => value,
|
||||
});
|
||||
flightState.records = records;
|
||||
const apply = createMilitarySnapshotRenderer({
|
||||
flightState._renderer = { view: {}, surfaceExclusions: () => [] };
|
||||
const {applySnapshot: apply} = createMilitarySnapshotRenderer({
|
||||
flightState,
|
||||
records,
|
||||
groundFloor: { warmGroundFloor() {}, cachedGroundFloor: () => null },
|
||||
|
||||
@@ -1658,12 +1658,11 @@ test('hasContact declines while a layer is disabled, whatever its maps still hol
|
||||
// disable() hides the collection but keeps the records, so a map lookup
|
||||
// alone would report a preserved subject as FRESH from hidden stale data.
|
||||
for (const [name, source, guard] of [
|
||||
// Flights asks its renderer whether the fleet is shown (phase 3).
|
||||
// Both aircraft layers answer from enabled observation models.
|
||||
['flights', readLayerSource(new URL('./flights.js', import.meta.url)),
|
||||
/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;/],
|
||||
/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;/],
|
||||
['aisLiveVessels', readLayerSource(new URL('./aisLiveVessels.js', import.meta.url)),
|
||||
/hasContact\(\s*mmsi,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!state\.feed\.enabled\s*\|\|\s*!state\.records\.byMmsi\s*\|\|\s*state\.records\.byMmsi\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
||||
]) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/** Scene-only ownership shared by the observed aircraft layers. Source
|
||||
* requests, classification, selection and track history outlive this binding. */
|
||||
export function createAircraftSceneBinding({
|
||||
id,
|
||||
flightState,
|
||||
services,
|
||||
parts,
|
||||
resolveModel,
|
||||
enable,
|
||||
}) {
|
||||
let resumeFollow = false;
|
||||
function removeSceneListeners() {
|
||||
parts.tracking._removeClickHandler();
|
||||
services.picking.unregisterPickOwner(id);
|
||||
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 = {
|
||||
attachRenderer(viewer, { engine = null } = {}) {
|
||||
if (!flightState.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(resolveModel())
|
||||
.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) {
|
||||
enable();
|
||||
parts.tracking._applyPendingTrackingRestore();
|
||||
}
|
||||
} catch (error) {
|
||||
methods.detachRenderer(viewer);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
detachRenderer(viewer) {
|
||||
const renderer = flightState._renderer;
|
||||
if (!renderer) return;
|
||||
removeSceneListeners();
|
||||
services.render.releaseContinuousRender(id);
|
||||
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;
|
||||
},
|
||||
|
||||
removeSceneListeners,
|
||||
};
|
||||
return methods;
|
||||
}
|
||||
+14
-109
@@ -1,3 +1,4 @@
|
||||
import { createAircraftSceneBinding } from '../aircraft/sceneBinding.js';
|
||||
import { PLANE_MODEL_URL } from './policy.js';
|
||||
|
||||
export function createLifecycle({
|
||||
@@ -11,32 +12,20 @@ export function createLifecycle({
|
||||
const { onMilitaryLayerActiveChange } = services.militaryRegistry;
|
||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||
const { ensureGeoidReady } = services.geoid;
|
||||
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
||||
const { registerPickOwner } = 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 binding = createAircraftSceneBinding({
|
||||
id: 'flights', flightState, services, parts,
|
||||
resolveModel: () => resolveAsset(PLANE_MODEL_URL),
|
||||
enable: () => methods.enable(),
|
||||
});
|
||||
|
||||
const methods = {
|
||||
attachRenderer: binding.attachRenderer,
|
||||
detachRenderer: binding.detachRenderer,
|
||||
/** Configure the source before initialization; an active layer keeps its owner. */
|
||||
setSource(source) {
|
||||
if (initialized)
|
||||
if (flightState.initialized)
|
||||
throw new Error('Configure the source before layer initialization');
|
||||
if (typeof source?.getSnapshot !== 'function')
|
||||
throw new TypeError('A snapshot source is required');
|
||||
@@ -55,7 +44,7 @@ export function createLifecycle({
|
||||
* @param {{ engine?: object }} [context] - LayerLifecycle's context.
|
||||
*/
|
||||
init(viewer, { engine = null } = {}) {
|
||||
if (initialized) throw new Error('Flight layer is already initialized');
|
||||
if (flightState.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)
|
||||
@@ -106,7 +95,7 @@ export function createLifecycle({
|
||||
);
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
flightState.initialized = true;
|
||||
try {
|
||||
if (viewer || engine) methods.attachRenderer(viewer, { engine });
|
||||
} catch (error) {
|
||||
@@ -115,90 +104,6 @@ export function createLifecycle({
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the fleet and re-join the selection dispatcher.
|
||||
*/
|
||||
@@ -275,7 +180,7 @@ export function createLifecycle({
|
||||
flightState._renderer?.models.setVisible(false);
|
||||
parts.tracking._clearTracking();
|
||||
parts.tracking._destroyTrail();
|
||||
removeSceneListeners();
|
||||
binding.removeSceneListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -284,7 +189,7 @@ export function createLifecycle({
|
||||
* @param {object|null} viewer - The viewer the layer was drawn on.
|
||||
*/
|
||||
destroy(viewer) {
|
||||
initialized = false;
|
||||
flightState.initialized = false;
|
||||
flightState.enabled = false;
|
||||
flightState.lifetime.abort();
|
||||
flightState._enrichActive = 0;
|
||||
|
||||
@@ -25,7 +25,7 @@ export function createMilitaryFlightLayer({
|
||||
parts.lifecycle = createLifecycle(context);
|
||||
parts.testing = createTesting(context);
|
||||
parts.queries = createQueries(context);
|
||||
const applySnapshot = createMilitarySnapshotRenderer({
|
||||
parts.snapshots = createMilitarySnapshotRenderer({
|
||||
flightState,
|
||||
records: flightState.records,
|
||||
groundFloor: services.groundFloor,
|
||||
@@ -38,7 +38,7 @@ export function createMilitaryFlightLayer({
|
||||
});
|
||||
parts.ingestion = createIngestion({
|
||||
feed: flightState.feed,
|
||||
applySnapshot,
|
||||
applySnapshot: parts.snapshots.applySnapshot,
|
||||
setSourceLabel: (source) => {
|
||||
layer.source = source;
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createAircraftSceneBinding } from '../aircraft/sceneBinding.js';
|
||||
import { JET_MODEL_URL } from './policy.js';
|
||||
|
||||
export function createLifecycle({
|
||||
@@ -10,13 +11,20 @@ export function createLifecycle({
|
||||
const { clearFocusTarget } = services.focus;
|
||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||
const { ensureGeoidReady } = services.geoid;
|
||||
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
||||
const { registerPickOwner } = services.picking;
|
||||
const { setMilitaryLayerActive } = services.militaryRegistry;
|
||||
|
||||
const binding = createAircraftSceneBinding({
|
||||
id: 'military', flightState, services, parts,
|
||||
resolveModel: () => resolveAsset(JET_MODEL_URL),
|
||||
enable: () => methods.enable(),
|
||||
});
|
||||
const methods = {
|
||||
attachRenderer: binding.attachRenderer,
|
||||
detachRenderer: binding.detachRenderer,
|
||||
/** Configure the source before initialization; an active layer keeps its owner. */
|
||||
setSource(source) {
|
||||
if (flightState._renderer)
|
||||
if (flightState.initialized)
|
||||
throw new Error('Configure the source before layer initialization');
|
||||
if (typeof source?.getSnapshot !== 'function')
|
||||
throw new TypeError('A snapshot source is required');
|
||||
@@ -35,35 +43,13 @@ export function createLifecycle({
|
||||
* @param {{ engine?: object }} [context] - LayerLifecycle's context.
|
||||
*/
|
||||
init(viewer, { engine = null } = {}) {
|
||||
if (flightState._renderer)
|
||||
if (flightState.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('militaryFlights');
|
||||
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(JET_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();
|
||||
@@ -82,9 +68,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(
|
||||
@@ -93,11 +79,13 @@ export function createLifecycle({
|
||||
);
|
||||
}
|
||||
|
||||
parts.tracking._installClickHandler();
|
||||
|
||||
renderer.fleet.restoreOrder();
|
||||
|
||||
console.log('[Data:Military] Initialized with billboard icons');
|
||||
flightState.initialized = true;
|
||||
try {
|
||||
if (viewer || engine) binding.attachRenderer(viewer, { engine });
|
||||
} catch (error) {
|
||||
methods.destroy(viewer);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -105,11 +93,12 @@ export function createLifecycle({
|
||||
*/
|
||||
enable() {
|
||||
const renderer = flightState._renderer;
|
||||
flightState.enabled = true;
|
||||
renderer?.fleet.setVisible(true);
|
||||
holdContinuousRender('military'); // per-frame animator (perf wave 2)
|
||||
if (renderer) holdContinuousRender('military'); // 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
|
||||
@@ -124,12 +113,12 @@ export function createLifecycle({
|
||||
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
|
||||
});
|
||||
}
|
||||
setMilitaryLayerActive(true);
|
||||
if (!renderer) return;
|
||||
parts.tracking._installClickHandler();
|
||||
registerPickOwner('military', (pickedId) =>
|
||||
flightState._billboards.has(pickedId),
|
||||
);
|
||||
// The flights layer suppresses its military duplicates while we render them
|
||||
setMilitaryLayerActive(true);
|
||||
// Force a fresh rotation pass on the first tick after re-enable
|
||||
flightState._lastCamPoseSig = '';
|
||||
if (!flightState._preRenderRemove && renderer) {
|
||||
@@ -163,6 +152,7 @@ export function createLifecycle({
|
||||
* dispatcher so clicks do not get intercepted while the layer is off.
|
||||
*/
|
||||
disable() {
|
||||
flightState.enabled = false;
|
||||
parts.controller._abortActiveUpdates();
|
||||
parts.tracking._cancelPendingTrackingRestore();
|
||||
flightState._renderer?.fleet.setVisible(false);
|
||||
@@ -171,24 +161,8 @@ 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('military');
|
||||
// Flights layer takes over rendering known-military aircraft (amber)
|
||||
binding.removeSceneListeners();
|
||||
setMilitaryLayerActive(false);
|
||||
if (flightState._preRenderRemove) {
|
||||
flightState._preRenderRemove();
|
||||
flightState._preRenderRemove = null;
|
||||
}
|
||||
if (flightState._trackedModelPreUpdateRemove) {
|
||||
flightState._trackedModelPreUpdateRemove();
|
||||
flightState._trackedModelPreUpdateRemove = null;
|
||||
}
|
||||
if (flightState._moveEndRemove) {
|
||||
flightState._moveEndRemove();
|
||||
flightState._moveEndRemove = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -197,6 +171,9 @@ export function createLifecycle({
|
||||
* @param {object|null} viewer - The viewer the layer was drawn on.
|
||||
*/
|
||||
destroy(viewer) {
|
||||
flightState.initialized = false;
|
||||
flightState.enabled = false;
|
||||
setMilitaryLayerActive(false);
|
||||
flightState.lifetime.abort();
|
||||
parts.controller._abortActiveUpdates();
|
||||
releaseContinuousRender('military'); // direct-destroy path (perf wave 2 fix)
|
||||
@@ -211,38 +188,13 @@ export function createLifecycle({
|
||||
);
|
||||
flightState._cockpitModeListener = null;
|
||||
}
|
||||
unregisterPickOwner('military');
|
||||
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();
|
||||
binding.detachRenderer(viewer);
|
||||
flightState.records.data.clear();
|
||||
flightState._positionHistory.clear();
|
||||
flightState._displayCourse.clear();
|
||||
flightState._groundSnap.clear();
|
||||
flightState.records.missingPolls.clear();
|
||||
flightState.records.geoidNCache.clear();
|
||||
flightState.feed._count = 0;
|
||||
flightState.feed._lastUpdate = null;
|
||||
flightState._cockpitContactMode = false;
|
||||
@@ -256,8 +208,6 @@ export function createLifecycle({
|
||||
source: flightState.feed._lastSource,
|
||||
};
|
||||
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
|
||||
flightState._renderer?.destroy();
|
||||
flightState._renderer = null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ export function createQueries({
|
||||
* @returns {Array<Object>} Sorted array of nearby aircraft descriptors
|
||||
*/
|
||||
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))
|
||||
@@ -363,11 +363,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,
|
||||
@@ -380,13 +383,12 @@ 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);
|
||||
nearby.push({
|
||||
id: info?.callsign?.trim() || info?.registration?.trim() || icao24,
|
||||
icao24,
|
||||
@@ -581,16 +583,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())
|
||||
);
|
||||
},
|
||||
|
||||
@@ -642,10 +641,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))
|
||||
|
||||
@@ -292,3 +292,37 @@ test('the military logic draws and follows on the Navara renderer; the follow pa
|
||||
assert.equal(engine.movers.list().length, 0, 'no mover set left behind');
|
||||
engine.destroy();
|
||||
});
|
||||
|
||||
test('military classification and the selected aircraft survive an update between scenes', async () => {
|
||||
const first = createFakeEngine(), second = createFakeEngine();
|
||||
let polls = 0, binding;
|
||||
const layer = createApplicationMilitary({
|
||||
surface: defaultSurface, militaryRegistry,
|
||||
source: { getSnapshot: async () => militarySnapshotOf(['ae0001'], 1700000000000 + (++polls)*1000) },
|
||||
renderer: {create: ({engine}) => (binding = recordingAircraftRenderer(engine))},
|
||||
});
|
||||
try {
|
||||
layer.init(null);
|
||||
layer.enable();
|
||||
await layer.update(null);
|
||||
assert.equal(layer.hasContact('AE0001'), true);
|
||||
assert.equal(layer.getAnalystRecords().length, 1);
|
||||
layer.attachRenderer(null, { engine: first });
|
||||
assert.equal(layer.trackById('ae0001'), true);
|
||||
layer.detachRenderer();
|
||||
assert.equal(binding.follow.current(), undefined);
|
||||
assert.equal(militaryRegistry.isMilitaryLayerActive(), true);
|
||||
assert.equal(layer.getTrackedInfo().icao24, 'ae0001');
|
||||
await layer.update(null);
|
||||
layer.attachRenderer(null, {engine: second});
|
||||
assert.ok(binding.follow.current());
|
||||
assert.equal(layer.getStats().lastUpdate, 1700000002000);
|
||||
assert.equal(polls, 2);
|
||||
layer.disable();
|
||||
assert.equal(militaryRegistry.isMilitaryLayerActive(), false);
|
||||
assert.equal(layer.hasContact('ae0001'), null);
|
||||
} finally {
|
||||
layer.destroy(null);
|
||||
first.destroy(); second.destroy();
|
||||
}
|
||||
});
|
||||
@@ -31,7 +31,74 @@ export function createMilitarySnapshotRenderer({
|
||||
groundFloor;
|
||||
const { sampleMeshFloorCells } = meshFloor;
|
||||
const { registerMilitaryIcaos } = militaryRegistry;
|
||||
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);
|
||||
if (!isTracked) {
|
||||
bb.color = rendering
|
||||
._fleetBillboardColor(icao24, meta)
|
||||
.withAlpha(cyberSonarBaseAlpha(bb));
|
||||
}
|
||||
// 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 {
|
||||
// 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, geo.cartesian3.create()) || 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:
|
||||
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: geo.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);
|
||||
}
|
||||
}
|
||||
|
||||
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 receiptNowMs = snapshot.observedAtMs;
|
||||
// Field-test fix (RS46): coarse floor cells to warm for the below-ground
|
||||
@@ -128,63 +195,7 @@ export function createMilitarySnapshotRenderer({
|
||||
}
|
||||
}
|
||||
|
||||
if (flightState._billboards.has(icao24)) {
|
||||
const bb = flightState._billboards.get(icao24);
|
||||
if (!isTracked) {
|
||||
bb.color = rendering
|
||||
._fleetBillboardColor(icao24, meta)
|
||||
.withAlpha(cyberSonarBaseAlpha(bb));
|
||||
}
|
||||
// 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 {
|
||||
// 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, geo.cartesian3.create()) || 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:
|
||||
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: geo.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);
|
||||
}
|
||||
|
||||
if (flightState._renderer) 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
|
||||
// append gate above simply freezes the existing trail).
|
||||
@@ -238,7 +249,7 @@ export function createMilitarySnapshotRenderer({
|
||||
// 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.
|
||||
{
|
||||
if (flightState._renderer) {
|
||||
const viewerCarto =
|
||||
flightState._renderer?.view.camera?.positionCartographic || null;
|
||||
sampleMeshFloorCells(
|
||||
@@ -269,7 +280,7 @@ export function createMilitarySnapshotRenderer({
|
||||
// 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,
|
||||
@@ -297,7 +308,8 @@ export function createMilitarySnapshotRenderer({
|
||||
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);
|
||||
@@ -313,6 +325,7 @@ export function createMilitarySnapshotRenderer({
|
||||
// Feed the shared registry so the flights layer can classify/suppress
|
||||
registerMilitaryIcaos(currentIcaos);
|
||||
|
||||
return { count: flightState._billboards.size, ids: currentIcaos };
|
||||
};
|
||||
return { count: records.data.size, ids: currentIcaos };
|
||||
}
|
||||
return { applySnapshot, presentRecords };
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import * as geo from '../../geo/index.js';
|
||||
|
||||
export function createFlightState({ source, services }) {
|
||||
const { createGroundSnap } = services.groundSnap;
|
||||
const flightState = { lifetime: new AbortController() };
|
||||
const flightState = { lifetime: new AbortController(), enabled: false };
|
||||
flightState.feed = createMilitaryFeed(source);
|
||||
flightState.records = new MilitaryFlightRecords({
|
||||
...services.geoid,
|
||||
|
||||
@@ -36,6 +36,7 @@ export function createTesting({
|
||||
models = [],
|
||||
modelCollection = null,
|
||||
}) {
|
||||
flightState.enabled = billboardCollection?.show !== false;
|
||||
// The production renderer over the test's viewer and (fake) collections.
|
||||
flightState._renderer = services.renderer.create({
|
||||
viewer,
|
||||
|
||||
@@ -502,6 +502,14 @@ export function createTracking({
|
||||
);
|
||||
}
|
||||
}
|
||||
_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,
|
||||
@@ -568,11 +576,6 @@ export function createTracking({
|
||||
});
|
||||
}
|
||||
_refreshTrailDisplay();
|
||||
|
||||
const oldestFixEpochSec = history.length
|
||||
? history[0].timeMs / 1000
|
||||
: Infinity;
|
||||
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -703,6 +706,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;
|
||||
@@ -830,8 +840,8 @@ export function createTracking({
|
||||
* @param {string} icao24 - ICAO hex identifier of the aircraft 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);
|
||||
@@ -958,9 +968,15 @@ export function createTracking({
|
||||
refreshTrackedReadout(flightState._trackedEntity);
|
||||
// Camera follows the tracked entity (the renderer cancels any flight
|
||||
// first and applies the one exact tracked frame).
|
||||
if (follow) {
|
||||
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
|
||||
flightState._trackedEntity,
|
||||
);
|
||||
}
|
||||
if (resume) {
|
||||
_attachTrail();
|
||||
return;
|
||||
}
|
||||
|
||||
// Track-history trail (PRD F2): seed from local history + async backfill.
|
||||
// Ground traffic draws NO trail — if the plane takes off while tracked, the
|
||||
@@ -1108,6 +1124,8 @@ export function createTracking({
|
||||
_appendTrailFix,
|
||||
_refreshTrailDisplay,
|
||||
_startTrail,
|
||||
_attachTrail,
|
||||
_detachTrail,
|
||||
_backfillTrail,
|
||||
_clearTrail,
|
||||
_destroyTrail,
|
||||
|
||||
Reference in new issue
Block a user