mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Preserve positionless aircraft identities for classification
This commit is contained in:
1 parent
1edebcb6f8
commit
22138212b9
8 files changed
+99
-4
No files matched your search
+1
-1
@@ -13,7 +13,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Split military flights into instance-owned state, ingestion, motion, rendering, tracking and query components. Share the existing aircraft calculations and give military classification an explicit source and cleanup lifecycle.
|
||||
- Split military flights into instance-owned state, ingestion, motion, rendering, tracking and query components. Share the existing aircraft calculations and give military classification an explicit source and cleanup lifecycle. Preserve known military identities even when the source has no position for them.
|
||||
|
||||
- Split civil flights into instance-owned state, ingestion, motion, rendering, tracking and query components. Cancel enrichment on teardown and resolve model assets through the application.
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ behavior remains unchanged.
|
||||
metadata, motion, altitude, model-anchor, proximity and selection calculations.
|
||||
It also exports `createMilitaryRegistry`, an explicitly constructed owner for
|
||||
known military identities and active-layer transitions. Its optional background
|
||||
poll consumes normalized records from the supplied source. Source replacement
|
||||
poll consumes the optional `getIdentities` capability, falling back to normalized
|
||||
positioned records when a source has no identity-only capability. The adsb.lol
|
||||
adapter preserves known identities without requiring positions; those entries
|
||||
still cannot enter the renderer. Source replacement
|
||||
and disposal abort pending work and clear retained identities; construction
|
||||
starts no network request. Both standalone aircraft layers use one registry.
|
||||
|
||||
|
||||
@@ -129,10 +129,17 @@ export function createMilitaryRegistry({ source, now = Date.now } = {}) {
|
||||
const signal = AbortSignal.any([owner.signal, AbortSignal.timeout(10000)]);
|
||||
_polling = true;
|
||||
try {
|
||||
const snapshot = await requestSource.getSnapshot({}, { signal });
|
||||
const ids =
|
||||
typeof requestSource.getIdentities === 'function'
|
||||
? await requestSource.getIdentities({}, { signal })
|
||||
: (await requestSource.getSnapshot({}, { signal })).records.map(
|
||||
(record) => record.id,
|
||||
);
|
||||
signal.throwIfAborted();
|
||||
if (lifetime !== owner) return;
|
||||
registerMilitaryIcaos(snapshot.records.map((record) => record.id));
|
||||
if (!Array.isArray(ids))
|
||||
throw new TypeError('Expected aircraft identities');
|
||||
registerMilitaryIcaos(ids);
|
||||
} catch {
|
||||
// Keep the existing classification through source outages.
|
||||
} finally {
|
||||
|
||||
@@ -121,3 +121,21 @@ test('disposing a registry fed directly by its layer also clears classification'
|
||||
registry.dispose();
|
||||
assert.equal(registry.isMilitaryIcao('abc123'), false);
|
||||
});
|
||||
|
||||
test('classification prefers an explicit identity capability over positioned observations', async () => {
|
||||
const registry = createMilitaryRegistry({
|
||||
source: {
|
||||
getSnapshot() {
|
||||
assert.fail(
|
||||
'position snapshots must not replace an available identity capability',
|
||||
);
|
||||
},
|
||||
async getIdentities() {
|
||||
return ['abc123'];
|
||||
},
|
||||
},
|
||||
});
|
||||
await registry.refreshMilitaryRegistryIfStale();
|
||||
assert.equal(registry.isMilitaryIcao('abc123'), true);
|
||||
registry.dispose();
|
||||
});
|
||||
@@ -165,3 +165,16 @@ export function normalizeAircraftTrack(
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/** Known source identities are useful for classification even without positions. */
|
||||
export function readsbIdentities(payload) {
|
||||
const admitted = admitRecords(
|
||||
payload?.ac,
|
||||
(row) => {
|
||||
const id = cleanText(row?.hex).toLowerCase();
|
||||
return id ? { id } : null;
|
||||
},
|
||||
'aircraft identities',
|
||||
);
|
||||
return admitted.records.map((record) => record.id);
|
||||
}
|
||||
@@ -271,3 +271,45 @@ test('out-of-range source epochs do not reach Date or globe time constructors',
|
||||
assert.equal(snapshot.records[0].positionTimeMs, null);
|
||||
assert.equal(snapshot.records[0].contactTimeMs, null);
|
||||
});
|
||||
|
||||
test('classification identities include positionless aircraft without admitting them to rendering', async () => {
|
||||
const source = createAdsbLolSource({
|
||||
fetchImpl: async () =>
|
||||
response({
|
||||
ac: [
|
||||
{ hex: ' ABC123 ' },
|
||||
{ hex: 'abc123' },
|
||||
{ hex: 'def456', lat: 30, lon: -97 },
|
||||
],
|
||||
}),
|
||||
});
|
||||
assert.deepEqual(await source.getIdentities(), ['abc123', 'def456']);
|
||||
const snapshot = await source.getSnapshot();
|
||||
assert.deepEqual(
|
||||
snapshot.records.map((record) => record.id),
|
||||
['def456'],
|
||||
);
|
||||
assert.equal(snapshot.complete, false);
|
||||
});
|
||||
|
||||
test('identity lookup validates its response and honors body-parse cancellation', async () => {
|
||||
const malformed = createAdsbLolSource({
|
||||
fetchImpl: async () => response({ ac: [{}] }),
|
||||
});
|
||||
await assert.rejects(malformed.getIdentities(), /Malformed/);
|
||||
const abort = new AbortController();
|
||||
const source = createAdsbLolSource({
|
||||
fetchImpl: async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
async json() {
|
||||
abort.abort();
|
||||
return { ac: [{ hex: 'abc123' }] };
|
||||
},
|
||||
}),
|
||||
});
|
||||
await assert.rejects(source.getIdentities({}, { signal: abort.signal }), {
|
||||
name: 'AbortError',
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
normalizeAircraftTrack,
|
||||
openSkySnapshot,
|
||||
readsbSnapshot,
|
||||
readsbIdentities,
|
||||
} from './aircraft.js';
|
||||
export {
|
||||
normalizeVesselObservation,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
normalizeAircraftTrack,
|
||||
openSkySnapshot,
|
||||
readsbSnapshot,
|
||||
readsbIdentities,
|
||||
} from './aircraft.js';
|
||||
import { normalizeVesselTrack, vesselSnapshot } from './vessels.js';
|
||||
|
||||
@@ -113,6 +114,16 @@ export function createAdsbLolSource({
|
||||
} = {}) {
|
||||
return {
|
||||
label: 'adsb.lol',
|
||||
async getIdentities(_query = {}, { signal } = {}) {
|
||||
const { response, payload } = await readResponse(
|
||||
fetchImpl,
|
||||
'/api/adsblol/mil',
|
||||
{ signal },
|
||||
'adsb.lol',
|
||||
);
|
||||
if (!response.ok) throw httpError(response, 'adsb.lol');
|
||||
return readsbIdentities(payload);
|
||||
},
|
||||
async getSnapshot(_query = {}, { signal } = {}) {
|
||||
const { response, payload } = await readResponse(
|
||||
fetchImpl,
|
||||
|
||||
Reference in new issue
Block a user