diff --git a/scripts/qa-local-adsb-engine-switch.mjs b/scripts/qa-local-adsb-engine-switch.mjs index e5dbc1f9d..f0dcc2a09 100644 --- a/scripts/qa-local-adsb-engine-switch.mjs +++ b/scripts/qa-local-adsb-engine-switch.mjs @@ -97,7 +97,12 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { await p.waitForFunction( () => window.qaAdsb.mod.getAllPositions().length === 1, ); - assert.ok(await p.evaluate(() => window.qaAdsb.mod.selectAircraft('abc123'))); + // The selection hook the selectable layers share, by the row's own id. + assert.ok( + await p.evaluate(() => + window.qaAdsb.mod.selectById(window.qaAdsb.mod.getAllPositions()[0].id), + ), + ); for (const kind of ['cesium', 'navara']) { await p.evaluate(() => { window.qaSelections = []; diff --git a/src/layers/localAdsb/index.js b/src/layers/localAdsb/index.js index 1ba7f9c8c..a84ed09c3 100644 --- a/src/layers/localAdsb/index.js +++ b/src/layers/localAdsb/index.js @@ -529,6 +529,14 @@ export function createLocalAdsbLayer({ return true; } + function selectById(id) { + if (!enabled || id === null || id === undefined) return false; + let icao = String(id).trim().toLowerCase(); + if (icao.startsWith(ENTITY_PREFIX)) icao = icao.slice(ENTITY_PREFIX.length); + if (!icao) return false; + return selectMarker(`${ENTITY_PREFIX}${icao}`); + } + /** * Join the engine's selection dispatcher with this layer's own click rule * (`lookUnder`): a click on a local aircraft — on top, or under picks no @@ -653,7 +661,8 @@ export function createLocalAdsbLayer({ installFrame(); sync(); const selected = getSelectedEntityContext(); - if (restore && (!selected || selected.id === restore)) selectMarker(restore); + if (restore && (!selected || selected.id === restore)) + selectMarker(restore); } return true; }, @@ -777,13 +786,16 @@ export function createLocalAdsbLayer({ /** * Select a heard aircraft's marker and publish its readout card, exactly - * as a click does. The camera does not follow it. - * @param {string} icao Lowercase ICAO hex. + * as a click does; the selection hook other selectable layers expose + * (vessels' selectById). The camera does not follow it. + * @param {string} id The ICAO hex of a getAllPositions row, in any case, + * or the marker id (`local-adsb:`). * @returns {boolean} Whether a fresh marker was selected. */ - selectAircraft(icao) { - return enabled && selectMarker(`${ENTITY_PREFIX}${icao}`); - }, + selectById, + + /** The same as selectById, by ICAO hex. */ + selectAircraft: (icao) => selectById(icao), /** * Snapshot loaded locally received aircraft as plain JSON-safe records for the analyst diff --git a/src/layers/localAdsb/index.test.mjs b/src/layers/localAdsb/index.test.mjs index bf3ff33b8..7edcba783 100644 --- a/src/layers/localAdsb/index.test.mjs +++ b/src/layers/localAdsb/index.test.mjs @@ -612,6 +612,30 @@ test('markers drop when the position is 60 s old and records when silent 60 s', assert.deepEqual(layer.getDetectableObjects(), []); }); +test('selectById selects a heard aircraft by the id its position rows carry, like a click', async (t) => { + const receiver = fakeReceiver({ mode: 'adsb', connected: true }); + const clock = { now: 100_000 }; + const { layer, calls } = await enabledLayer(receiver, clock); + t.after(() => layer.destroy()); + receiver.set({ aircraft: [record()] }); + await layer.update(); + const [row] = layer.getAllPositions(); + assert.equal(row.id, 'abc123'); + assert.equal(layer.selectById(row.id), true); + assert.equal(layer.selectById(' ABC123 '), true, 'any case, trimmed'); + assert.equal(layer.selectById('local-adsb:abc123'), true, 'the marker id'); + assert.deepEqual(calls.selected, [ + 'local-adsb:abc123', + 'local-adsb:abc123', + 'local-adsb:abc123', + ]); + for (const missing of ['ffffff', '', null, undefined, 'local-adsb:']) + assert.equal(layer.selectById(missing), false, String(missing)); + await layer.disable(); + assert.equal(layer.selectById('abc123'), false, 'nothing while off'); + assert.equal(calls.selected.length, 3); +}); + test('selecting a marker publishes the readout card and eviction clears it', async (t) => { const receiver = fakeReceiver({ mode: 'adsb', connected: true }); const clock = { now: 100_000 }; @@ -1170,14 +1194,24 @@ test('globe handoff retains the receiver, feed, trail fixes and selection', asyn const receiver = fakeReceiver({ aircraft: [record()] }); const feeds = fakeFeeds(); let closes = 0; - receiver.destroy = () => { closes++; }; + 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 })] }); + 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);