mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
Keep local ADS-B receivers and aircraft history alive across engine changes
This commit is contained in:
1 parent
c1d829ef51
commit
d7b3d45135
4 files changed
+224
-30
No files matched your search
@@ -0,0 +1,143 @@
|
|||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
|
|
||||||
|
const base = new URL(process.argv[2]);
|
||||||
|
if (
|
||||||
|
!['localhost', '127.0.0.1'].includes(base.hostname) ||
|
||||||
|
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||||
|
base.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw new Error('Supply an unreserved local keyless preview URL');
|
||||||
|
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||||
|
base.hash =
|
||||||
|
'v=2&lat=41.5340&lon=-90.5070&alt=20000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
|
||||||
|
const b = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--use-angle=metal', '--enable-gpu'],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const p = await b.newPage();
|
||||||
|
await blockPaidQaMaps(p);
|
||||||
|
await p.setViewport({ width: 1024, height: 768 });
|
||||||
|
const errors = [];
|
||||||
|
p.on('pageerror', (e) => {
|
||||||
|
errors.push(e.message);
|
||||||
|
console.log('error', e.message.split('?')[0]);
|
||||||
|
});
|
||||||
|
await p.evaluateOnNewDocument(() => {
|
||||||
|
const raw = window.fetch;
|
||||||
|
window.fetch = function (input, ...args) {
|
||||||
|
const url = new URL(input?.url ?? input, location.href);
|
||||||
|
if (url.pathname === '/api/local-receivers/aircraft') {
|
||||||
|
const at = Date.now();
|
||||||
|
return Promise.resolve(
|
||||||
|
Response.json({
|
||||||
|
configured: true,
|
||||||
|
generatedAt: at,
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
icao: 'abc123',
|
||||||
|
callsign: 'LOCAL1',
|
||||||
|
lat: 41.534,
|
||||||
|
lon: -90.507,
|
||||||
|
altitudeFt: 11000,
|
||||||
|
groundSpeedKt: 180,
|
||||||
|
trackDeg: 5,
|
||||||
|
lastPositionAt: at,
|
||||||
|
lastMessageAt: at,
|
||||||
|
messageCount: 12,
|
||||||
|
band: '1090',
|
||||||
|
source: 'decoder',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return raw.call(this, input, ...args);
|
||||||
|
};
|
||||||
|
window.qaSelections = [];
|
||||||
|
window.addEventListener('gev:entity-selected', ({ detail }) =>
|
||||||
|
window.qaSelections.push(detail.id),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await p.goto(base.href);
|
||||||
|
const ready = async (kind) => {
|
||||||
|
await p.waitForFunction(
|
||||||
|
(k) =>
|
||||||
|
window.__godsEyeView?.engine.kind === k &&
|
||||||
|
!document.querySelector('#globe-engine')?.disabled &&
|
||||||
|
!document.querySelector('.engine-switch-frame') &&
|
||||||
|
document.querySelector('#loading-screen.hidden'),
|
||||||
|
{ timeout: 90000 },
|
||||||
|
kind,
|
||||||
|
);
|
||||||
|
await p.evaluate(
|
||||||
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await ready('navara');
|
||||||
|
await p.evaluate(async () => {
|
||||||
|
const { dataManager, tileset } = window.__godsEyeView;
|
||||||
|
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||||
|
throw Error('Keyless preview required');
|
||||||
|
const mod = dataManager.layers.get('local-adsb').module;
|
||||||
|
window.qaAdsb = {
|
||||||
|
mod,
|
||||||
|
receiver: mod.receiver,
|
||||||
|
feeds: mod.feeds,
|
||||||
|
starts: 0,
|
||||||
|
stops: 0,
|
||||||
|
closes: 0,
|
||||||
|
};
|
||||||
|
for (const [obj, key, counter] of [
|
||||||
|
[mod.receiver, 'destroy', 'closes'],
|
||||||
|
[mod.feeds, 'start', 'starts'],
|
||||||
|
[mod.feeds, 'stop', 'stops'],
|
||||||
|
]) {
|
||||||
|
const original = obj[key].bind(obj);
|
||||||
|
obj[key] = (...args) => {
|
||||||
|
window.qaAdsb[counter]++;
|
||||||
|
return original(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await dataManager.setEnabled('local-adsb', true, { origin: 'user' });
|
||||||
|
});
|
||||||
|
await p.waitForFunction(
|
||||||
|
() => window.qaAdsb.mod.getAllPositions().length === 1,
|
||||||
|
);
|
||||||
|
assert.ok(await p.evaluate(() => window.qaAdsb.mod.selectAircraft('abc123')));
|
||||||
|
for (const kind of ['cesium', 'navara']) {
|
||||||
|
await p.evaluate(() => {
|
||||||
|
window.qaSelections = [];
|
||||||
|
});
|
||||||
|
await p.select('#globe-engine', kind);
|
||||||
|
await ready(kind);
|
||||||
|
await p.waitForFunction(() =>
|
||||||
|
window.qaSelections.includes('local-adsb:abc123'),
|
||||||
|
);
|
||||||
|
const result = await p.evaluate(() => {
|
||||||
|
const q = window.qaAdsb,
|
||||||
|
manager = window.__godsEyeView.dataManager;
|
||||||
|
const mod = manager.layers.get('local-adsb').module;
|
||||||
|
return [
|
||||||
|
manager.isEnabled(mod.id),
|
||||||
|
q.mod === mod,
|
||||||
|
q.receiver === mod.receiver,
|
||||||
|
q.feeds === mod.feeds,
|
||||||
|
mod.getAllPositions()[0]?.id,
|
||||||
|
q.starts,
|
||||||
|
q.stops,
|
||||||
|
q.closes,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
assert.deepEqual(result, [true, true, true, true, 'abc123', 1, 0, 0]);
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(
|
||||||
|
'PASS retained local decoder, receiver, marker and selection in both scenes',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await b.close();
|
||||||
|
}
|
||||||
@@ -173,7 +173,7 @@ export function createApplicationCatalog({
|
|||||||
[
|
[
|
||||||
'local-adsb',
|
'local-adsb',
|
||||||
() =>
|
() =>
|
||||||
createApplicationLocalAdsb({
|
sessionModel('local-adsb', () => createApplicationLocalAdsb({
|
||||||
surface,
|
surface,
|
||||||
// QA fixture receivers (?qaFeed=local-adsb:fixture, dev only).
|
// QA fixture receivers (?qaFeed=local-adsb:fixture, dev only).
|
||||||
...(sources.localAdsbFeeds
|
...(sources.localAdsbFeeds
|
||||||
@@ -182,7 +182,7 @@ export function createApplicationCatalog({
|
|||||||
enrichment: sources.flights,
|
enrichment: sources.flights,
|
||||||
displayParams: () => flights().getParams(),
|
displayParams: () => flights().getParams(),
|
||||||
...(resolveAsset ? { resolveAsset } : {}),
|
...(resolveAsset ? { resolveAsset } : {}),
|
||||||
}),
|
})),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'earthquakes',
|
'earthquakes',
|
||||||
|
|||||||
@@ -400,6 +400,10 @@ export function createLocalAdsbLayer({
|
|||||||
const newFix = marker.motion.observe(record, at);
|
const newFix = marker.motion.observe(record, at);
|
||||||
rejectedFixes += marker.motion.rejectedFixes - rejectedBefore;
|
rejectedFixes += marker.motion.rejectedFixes - rejectedBefore;
|
||||||
updateDisplay(marker, at);
|
updateDisplay(marker, at);
|
||||||
|
if (!renderer) {
|
||||||
|
refreshClass(marker);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
if (!marker.entity) {
|
if (!marker.entity) {
|
||||||
// Magenta glyph, 20 px, ×3 near / ×0.5 far, always on top, not drawn
|
// Magenta glyph, 20 px, ×3 near / ×0.5 far, always on top, not drawn
|
||||||
// past 2,000 km (the renderer's display distance).
|
// past 2,000 km (the renderer's display distance).
|
||||||
@@ -432,13 +436,13 @@ export function createLocalAdsbLayer({
|
|||||||
|
|
||||||
function refreshSelectedCard() {
|
function refreshSelectedCard() {
|
||||||
const marker = selectedId ? markers.get(selectedId) : null;
|
const marker = selectedId ? markers.get(selectedId) : null;
|
||||||
if (!marker) return;
|
if (!marker?.entity) return;
|
||||||
marker.entity.gevLabelModel = cardModel(marker, now());
|
marker.entity.gevLabelModel = cardModel(marker, now());
|
||||||
services.overlays?.refreshReadout?.(marker.entity);
|
services.overlays?.refreshReadout?.(marker.entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRenderHold() {
|
function updateRenderHold() {
|
||||||
const want = enabled && markers.size > 0;
|
const want = Boolean(renderer) && enabled && markers.size > 0;
|
||||||
if (want === renderHeld) return;
|
if (want === renderHeld) return;
|
||||||
renderHeld = want;
|
renderHeld = want;
|
||||||
if (want) holdContinuousRender(RENDER_HOLD_ID);
|
if (want) holdContinuousRender(RENDER_HOLD_ID);
|
||||||
@@ -457,7 +461,6 @@ export function createLocalAdsbLayer({
|
|||||||
function sync() {
|
function sync() {
|
||||||
clearTimeout(syncTimer);
|
clearTimeout(syncTimer);
|
||||||
syncTimer = null;
|
syncTimer = null;
|
||||||
if (!renderer) return;
|
|
||||||
const at = now();
|
const at = now();
|
||||||
const live = new Set();
|
const live = new Set();
|
||||||
let added = false;
|
let added = false;
|
||||||
@@ -479,8 +482,8 @@ export function createLocalAdsbLayer({
|
|||||||
if (removed) models?.retain(markers);
|
if (removed) models?.retain(markers);
|
||||||
if (selectedId && !live.has(selectedId))
|
if (selectedId && !live.has(selectedId))
|
||||||
clearSelection({ evicted: enabled });
|
clearSelection({ evicted: enabled });
|
||||||
removeEntityContextsForLayer(LAYER_ID, { retainIds: live });
|
if (renderer) removeEntityContextsForLayer(LAYER_ID, { retainIds: live });
|
||||||
if (selectedId) {
|
if (renderer && selectedId) {
|
||||||
const selected = markers.get(selectedId);
|
const selected = markers.get(selectedId);
|
||||||
if (getSelectedEntityContext()?.id !== selectedId) {
|
if (getSelectedEntityContext()?.id !== selectedId) {
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
@@ -515,7 +518,7 @@ export function createLocalAdsbLayer({
|
|||||||
|
|
||||||
function selectMarker(id) {
|
function selectMarker(id) {
|
||||||
const marker = markers.get(id);
|
const marker = markers.get(id);
|
||||||
if (!marker) return false;
|
if (!marker?.entity) return false;
|
||||||
const reselect = id === selectedId && selectedTrail?.id === id;
|
const reselect = id === selectedId && selectedTrail?.id === id;
|
||||||
selectedId = id;
|
selectedId = id;
|
||||||
enrichment.request(marker.record, { selected: true });
|
enrichment.request(marker.record, { selected: true });
|
||||||
@@ -610,7 +613,22 @@ export function createLocalAdsbLayer({
|
|||||||
* without one).
|
* without one).
|
||||||
* @param {{ engine?: object }} [context] LayerLifecycle's context.
|
* @param {{ engine?: object }} [context] LayerLifecycle's context.
|
||||||
*/
|
*/
|
||||||
init(nextViewer, { engine = null } = {}) {
|
init(nextViewer, options = {}) {
|
||||||
|
services.geoid
|
||||||
|
?.ensureGeoidReady?.()
|
||||||
|
?.then(() => {
|
||||||
|
geoidReady = true;
|
||||||
|
for (const marker of markers.values()) marker.geoidN = null;
|
||||||
|
})
|
||||||
|
?.catch(() => {
|
||||||
|
/* the baro path stays un-geoid-corrected */
|
||||||
|
});
|
||||||
|
unsubscribe = receiver.subscribe?.(scheduleSync) || null;
|
||||||
|
unsubscribeFeeds = feeds?.subscribe?.(requestSync) || null;
|
||||||
|
return this.attachRenderer(nextViewer, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
attachRenderer(nextViewer, { engine = null } = {}) {
|
||||||
renderer = services.renderer.create({
|
renderer = services.renderer.create({
|
||||||
viewer: nextViewer,
|
viewer: nextViewer,
|
||||||
engine,
|
engine,
|
||||||
@@ -626,20 +644,41 @@ export function createLocalAdsbLayer({
|
|||||||
groundSnap: services.groundSnap?.createGroundSnap?.() || null,
|
groundSnap: services.groundSnap?.createGroundSnap?.() || null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
services.geoid
|
if (enabled) {
|
||||||
?.ensureGeoidReady?.()
|
const restore = selectedId;
|
||||||
?.then(() => {
|
selectedId = null;
|
||||||
geoidReady = true;
|
renderer.setVisible(true);
|
||||||
for (const marker of markers.values()) marker.geoidN = null;
|
registerPickOwner(LAYER_ID, (id) => markers.has(id));
|
||||||
})
|
installInteraction();
|
||||||
?.catch(() => {
|
installFrame();
|
||||||
/* the baro path stays un-geoid-corrected */
|
sync();
|
||||||
});
|
const selected = getSelectedEntityContext();
|
||||||
unsubscribe = receiver.subscribe?.(scheduleSync) || null;
|
if (restore && (!selected || selected.id === restore)) selectMarker(restore);
|
||||||
unsubscribeFeeds = feeds?.subscribe?.(requestSync) || null;
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
detachRenderer() {
|
||||||
|
if (!renderer) return;
|
||||||
|
if (getSelectedEntityContext()?.id !== selectedId) selectedId = null;
|
||||||
|
releaseInteraction();
|
||||||
|
releaseFrame();
|
||||||
|
unregisterPickOwner(LAYER_ID);
|
||||||
|
releaseTrail();
|
||||||
|
models?.destroy();
|
||||||
|
models = null;
|
||||||
|
renderer.detach();
|
||||||
|
renderer.destroy();
|
||||||
|
renderer = null;
|
||||||
|
for (const marker of markers.values()) {
|
||||||
|
marker.entity = null;
|
||||||
|
marker.iconKind = marker.iconScale = null;
|
||||||
|
marker.modelOwnsVisual = false;
|
||||||
|
}
|
||||||
|
removeEntityContextsForLayer(LAYER_ID);
|
||||||
|
updateRenderHold();
|
||||||
|
},
|
||||||
|
|
||||||
async enable() {
|
async enable() {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
renderer?.setVisible(true);
|
renderer?.setVisible(true);
|
||||||
@@ -686,9 +725,6 @@ export function createLocalAdsbLayer({
|
|||||||
clearTimeout(syncTimer);
|
clearTimeout(syncTimer);
|
||||||
tickTimer = null;
|
tickTimer = null;
|
||||||
syncTimer = null;
|
syncTimer = null;
|
||||||
releaseInteraction();
|
|
||||||
releaseFrame();
|
|
||||||
unregisterPickOwner(LAYER_ID);
|
|
||||||
unsubscribe?.();
|
unsubscribe?.();
|
||||||
unsubscribe = null;
|
unsubscribe = null;
|
||||||
unsubscribeFeeds?.();
|
unsubscribeFeeds?.();
|
||||||
@@ -696,14 +732,8 @@ export function createLocalAdsbLayer({
|
|||||||
feeds?.destroy?.();
|
feeds?.destroy?.();
|
||||||
enrichment.destroy();
|
enrichment.destroy();
|
||||||
clearSelection();
|
clearSelection();
|
||||||
removeEntityContextsForLayer(LAYER_ID);
|
this.detachRenderer();
|
||||||
markers.clear();
|
markers.clear();
|
||||||
updateRenderHold();
|
|
||||||
models?.destroy();
|
|
||||||
models = null;
|
|
||||||
renderer?.detach();
|
|
||||||
renderer?.destroy();
|
|
||||||
renderer = null;
|
|
||||||
void receiver.destroy?.();
|
void receiver.destroy?.();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1165,3 +1165,24 @@ test('a click on a local aircraft under an unowned or trail pick still selects i
|
|||||||
harness.click();
|
harness.click();
|
||||||
assert.equal(calls.selected.length, 2, 'the Flights contact is not taken');
|
assert.equal(calls.selected.length, 2, 'the Flights contact is not taken');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('globe handoff retains the receiver, feed, trail fixes and selection', async (t) => {
|
||||||
|
const receiver = fakeReceiver({ aircraft: [record()] });
|
||||||
|
const feeds = fakeFeeds();
|
||||||
|
let closes = 0;
|
||||||
|
receiver.destroy = () => { closes++; };
|
||||||
|
const clock = { now: 100_000 };
|
||||||
|
const { layer, calls } = await enabledLayer(receiver, clock, feeds);
|
||||||
|
t.after(() => layer.destroy());
|
||||||
|
assert.ok(layer.selectAircraft('abc123'));
|
||||||
|
layer.detachRenderer();
|
||||||
|
clock.now += 1000;
|
||||||
|
receiver.set({ aircraft: [record({ lon: 0.001, lastPositionAt: clock.now, lastMessageAt: clock.now })] });
|
||||||
|
await layer.update();
|
||||||
|
assert.equal(layer.getAnalystRecords()[0].icao24, 'abc123');
|
||||||
|
layer.attachRenderer(fakeViewer().viewer);
|
||||||
|
assert.deepEqual(receiver.modeCalls, ['adsb']);
|
||||||
|
assert.deepEqual(feeds.calls, ['start']);
|
||||||
|
assert.equal(closes, 0);
|
||||||
|
assert.deepEqual(calls.selected, ['local-adsb:abc123', 'local-adsb:abc123']);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user