Files
gods-eye-view/src/data/adsbLolFallback.js
T

131 lines
4.0 KiB
JavaScript

const KNOT_TO_MPS = 0.514444;
const FOOT_TO_M = 0.3048;
const FPM_TO_MPS = 0.00508;
function finiteNumber(value) {
if (value === null || value === undefined || value === '') return null;
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function emitterCategory(value) {
const category = String(value || '')
.trim()
.toUpperCase();
const categories = {
A1: 2,
A2: 3,
A3: 4,
A4: 5,
A5: 6,
A6: 7,
A7: 8,
B1: 9,
B2: 10,
B3: 11,
B4: 12,
B6: 14,
B7: 15,
};
return categories[category] || 0;
}
/**
* Convert one adsb.lol v2 aircraft record into the OpenSky state-vector shape
* consumed by the existing Flights renderer.
* @param {object} aircraft adsb.lol aircraft record.
* @param {number} nowSeconds Feed response time in epoch seconds.
* @returns {Array|null} OpenSky-compatible state vector, or null when invalid.
*/
export function normalizeAdsbLolAircraftState(aircraft, nowSeconds) {
const hex = String(aircraft?.hex || '')
.trim()
.toLowerCase();
const latitude = finiteNumber(aircraft?.lat);
const longitude = finiteNumber(aircraft?.lon);
if (
!hex ||
latitude === null ||
longitude === null ||
Math.abs(latitude) > 90 ||
Math.abs(longitude) > 180
)
return null;
const seenPosition = Math.max(
0,
finiteNumber(aircraft?.seen_pos) ?? finiteNumber(aircraft?.seen) ?? NaN,
);
const seen = Math.max(0, finiteNumber(aircraft?.seen) ?? seenPosition);
const onGround = aircraft?.alt_baro === 'ground';
const barometricFeet = onGround ? null : finiteNumber(aircraft?.alt_baro);
const geometricFeet = finiteNumber(aircraft?.alt_geom);
const groundSpeedKnots = finiteNumber(aircraft?.gs);
const verticalRateFpm =
finiteNumber(aircraft?.baro_rate) ?? finiteNumber(aircraft?.geom_rate);
const track = finiteNumber(aircraft?.track);
return [
hex,
String(aircraft?.flight || aircraft?.r || '').trim() || null,
null,
Number.isFinite(seenPosition)
? Math.max(0, nowSeconds - seenPosition)
: null,
Number.isFinite(seen) ? Math.max(0, nowSeconds - seen) : null,
longitude,
latitude,
barometricFeet === null ? null : barometricFeet * FOOT_TO_M,
onGround,
groundSpeedKnots === null ? null : groundSpeedKnots * KNOT_TO_MPS,
track,
verticalRateFpm === null ? null : verticalRateFpm * FPM_TO_MPS,
null,
geometricFeet === null ? null : geometricFeet * FOOT_TO_M,
aircraft?.squawk || null,
aircraft?.spi === 1,
0,
emitterCategory(aircraft?.category),
];
}
/**
* Normalize an adsb.lol point response to an OpenSky-compatible response.
* Invalid rows and positionless contacts are intentionally excluded.
* @param {object} payload adsb.lol v2 response.
* @returns {{time:number,states:Array[]}}
*/
export function normalizeAdsbLolPointResponse(payload) {
const responseNow = finiteNumber(payload?.now);
const nowSeconds =
responseNow === null
? Math.floor(Date.now() / 1000)
: Math.floor(
responseNow > 10_000_000_000 ? responseNow / 1000 : responseNow,
);
const aircraft = Array.isArray(payload?.ac) ? payload.ac : [];
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();
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,
...(Object.keys(emergencyByIcao).length ? { emergencyByIcao } : {}),
};
}