diff --git a/src/data/adsbLolFallback.js b/src/data/adsbLolFallback.js index 2ae17fbe6..e83cf118b 100644 --- a/src/data/adsbLolFallback.js +++ b/src/data/adsbLolFallback.js @@ -43,7 +43,14 @@ export function normalizeAdsbLolAircraftState(aircraft, nowSeconds) { .toLowerCase(); const latitude = finiteNumber(aircraft?.lat); const longitude = finiteNumber(aircraft?.lon); - if (!hex || latitude === null || longitude === null) return null; + if ( + !hex || + latitude === null || + longitude === null || + Math.abs(latitude) > 90 || + Math.abs(longitude) > 180 + ) + return null; const seenPosition = Math.max( 0, @@ -95,21 +102,24 @@ export function normalizeAdsbLolPointResponse(payload) { responseNow > 10_000_000_000 ? responseNow / 1000 : responseNow, ); const aircraft = Array.isArray(payload?.ac) ? payload.ac : []; - const states = aircraft - .map((aircraft) => normalizeAdsbLolAircraftState(aircraft, nowSeconds)) - .filter(Boolean); + const states = []; // Keep the OpenSky vectors unchanged. The regional fallback's extra readsb // status travels alongside them, keyed only to admitted aircraft. - const admitted = new Set(states.map((row) => row[0])); - const emergencyByIcao = Object.fromEntries( - aircraft.flatMap((row) => { - const id = String(row?.hex || '') - .trim() - .toLowerCase(); - if (!admitted.has(id) || typeof row?.emergency !== 'string') return []; - return [[id, row.emergency.trim().toLowerCase()]]; - }), - ); + const admitted = new Set(); + const statuses = []; + for (const row of aircraft) { + const state = normalizeAdsbLolAircraftState(row, nowSeconds); + if (!state) continue; + states.push(state); + // The live contract admits the first valid vector for each ICAO. Its + // status must come from that same row, never an invalid or later duplicate. + const id = state[0]; + if (admitted.has(id)) continue; + admitted.add(id); + if (typeof row.emergency === 'string') + statuses.push([id, row.emergency.trim().toLowerCase()]); + } + const emergencyByIcao = Object.fromEntries(statuses); return { time: nowSeconds, states, diff --git a/src/data/aircraftEmergencyAnalyst.test.mjs b/src/data/aircraftEmergencyAnalyst.test.mjs index 2bb9ede1b..9e4a6e2fe 100644 --- a/src/data/aircraftEmergencyAnalyst.test.mjs +++ b/src/data/aircraftEmergencyAnalyst.test.mjs @@ -16,6 +16,25 @@ import { createGevActionRunner } from '../voice/gevActions.js'; const NOW_MS = 1800000000000; +test('fallback status belongs to the first valid admitted aircraft row', () => { + const valid = { hex: 'a1b2c3', lat: 30, lon: -97, squawk: '1200' }; + for (const status of [undefined, 'general', 'none']) { + const payload = normalizeAdsbLolPointResponse({ + now: NOW_MS, + ac: [ + { ...valid, lat: 100, emergency: 'general' }, + { ...valid, emergency: status }, + { ...valid, lat: null, emergency: 'general' }, + { ...valid, emergency: status === 'general' ? 'none' : 'general' }, + ], + }); + const snapshot = openSkySnapshot(payload, { now: NOW_MS }); + assert.equal(snapshot.records.length, 1); + assert.equal(snapshot.records[0].emergency, status ?? null); + assert.equal(snapshot.records[0].latitude, 30); + } +}); + function civilStore() { return new FlightRecords({ geoidHeight: () => 0,