Files
gods-eye-view/src/sources/live/aircraft.js
T

193 lines
5.4 KiB
JavaScript

import { normalizeSquawk } from '../../data/aircraftEmergency.js';
import {
admitRecords,
cleanText,
coordinates,
epoch,
finite,
} from './contract.js';
/** Normalize the OpenSky state-vector route, including its regional fallback. */
export function normalizeOpenSkyAircraft(row) {
if (!Array.isArray(row)) return null;
const id = cleanText(row[0]).toLowerCase();
if (!id || !coordinates(row[6], row[5])) return null;
return {
id,
reference: id,
latitude: row[6],
longitude: row[5],
callsign: cleanText(row[1]),
originCountry: cleanText(row[2]),
positionTimeMs: epoch(row[3], 1000),
contactTimeMs: epoch(row[4], 1000),
baroAltitudeM: finite(row[7]),
ellipsoidAltitudeM: finite(row[13]),
onGround: row[8] === true,
speedMps: finite(row[9]),
courseDeg: finite(row[10]),
verticalRateMps: finite(row[11]),
category: finite(row[17]),
typeCode: null,
registration: null,
operator: null,
squawk: normalizeSquawk(row[14]),
// State vectors carry no ADS-B emergency field; the squawk stands alone.
emergency: undefined,
};
}
/** Normalize readsb observations. The caller supplies the source snapshot epoch. */
export function normalizeReadsbAircraft(row, snapshotTimeMs) {
const id = cleanText(row?.hex).toLowerCase();
const latitude = finite(row?.lat),
longitude = finite(row?.lon);
if (!id || !coordinates(latitude, longitude)) return null;
const baroFt = finite(row.alt_baro),
geoFt = finite(row.alt_geom);
const speedKts = finite(row.gs),
rateFtMin = finite(row.baro_rate);
const seen = finite(row.seen),
seenPos = finite(row.seen_pos);
return {
id,
reference: id,
latitude,
longitude,
callsign: cleanText(row.flight),
originCountry: null,
positionTimeMs:
snapshotTimeMs == null ? null : snapshotTimeMs - (seenPos ?? 0) * 1000,
contactTimeMs:
snapshotTimeMs == null || seen == null
? null
: snapshotTimeMs - seen * 1000,
baroAltitudeM: baroFt == null ? null : baroFt * 0.3048,
ellipsoidAltitudeM: geoFt == null ? null : geoFt * 0.3048,
onGround: cleanText(row.alt_baro).toLowerCase() === 'ground',
speedMps: speedKts == null ? null : speedKts * 0.514444,
courseDeg: finite(row.track),
verticalRateMps: rateFtMin == null ? null : rateFtMin * 0.00508,
category: row.category ?? null,
typeCode: cleanText(row.t),
registration: cleanText(row.r),
operator: cleanText(row.ownOp),
squawk: normalizeSquawk(row.squawk),
emergency: cleanText(row.emergency).toLowerCase() || null,
};
}
export function openSkySnapshot(
payload,
{
source = 'OpenSky Network',
coverage = 'worldwide upstream snapshot',
now = Date.now(),
stale = false,
} = {},
) {
const admitted = admitRecords(
payload?.states,
(row) => {
const record = normalizeOpenSkyAircraft(row);
const statuses = payload?.emergencyByIcao;
if (record && statuses && Object.hasOwn(statuses, record.id))
record.emergency = cleanText(statuses[record.id]).toLowerCase() || null;
return record;
},
'OpenSky',
);
const observedAtMs = epoch(payload?.time, 1000);
const ageMs = observedAtMs == null ? null : Math.max(0, now - observedAtMs);
return {
...admitted,
source,
coverage,
observedAtMs,
ageMs,
stale: stale || (ageMs != null && ageMs > 120000),
freshness:
observedAtMs == null
? 'unknown'
: stale || ageMs > 120000
? 'stale'
: 'current',
};
}
export function readsbSnapshot(
payload,
{
observedAtMs,
source = 'adsb.lol',
coverage = 'military upstream snapshot',
now = Date.now(),
stale = false,
} = {},
) {
const admitted = admitRecords(
payload?.ac,
(row) => normalizeReadsbAircraft(row, observedAtMs),
'adsb.lol',
);
const ageMs = observedAtMs == null ? null : Math.max(0, now - observedAtMs);
return {
...admitted,
source,
coverage,
observedAtMs,
ageMs,
stale,
freshness: observedAtMs == null ? 'unknown' : stale ? 'stale' : 'current',
};
}
/** Track altitude remains barometric; renderers choose a visual ground fallback. */
export function normalizeAircraftTrack(
rows,
{ baseTimeMs = 0, readsb = false } = {},
) {
if (!Array.isArray(rows)) return [];
return rows.flatMap((row) => {
if (!Array.isArray(row)) return [];
const latitude = finite(row[1]),
longitude = finite(row[2]);
const offset = finite(row[0]);
const observedAtMs =
offset == null ? null : epoch(baseTimeMs + offset * 1000);
if (
!coordinates(latitude, longitude) ||
observedAtMs == null ||
observedAtMs <= 0
)
return [];
const altitude = finite(row[3]);
return [
{
latitude,
longitude,
observedAtMs,
baroAltitudeM:
altitude == null ? null : altitude * (readsb ? 0.3048 : 1),
ellipsoidAltitudeM: null,
onGround: readsb
? cleanText(row[3]).toLowerCase() === 'ground'
: row[5] === true,
},
];
});
}
/** 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);
}