mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +00:00
fix(flights): an aircraft with no ground track faces its true heading, not north
readsb rows (adsb.lol and a local dump1090 receiver) often send no track for an aircraft on the ground, only its true heading. The track then read as missing and the aircraft was drawn facing north, so one taxiing south looked like it was moving backwards. Each normalizer now falls back to true_heading, never the magnetic heading.
This commit is contained in:
1 parent
3934be9ff9
commit
fd06272e37
6 files changed
+65
-3
No files matched your search
@@ -64,7 +64,11 @@ export function normalizeAdsbLolAircraftState(aircraft, nowSeconds) {
|
||||
const geometricFeet = finiteNumber(aircraft?.alt_geom);
|
||||
const groundSpeedKnots = finiteNumber(aircraft?.gs);
|
||||
const verticalRateFpm = readAdsbVerticalRateFpm(aircraft);
|
||||
const track = finiteNumber(aircraft?.track);
|
||||
// A parked or slow aircraft reports no ground track, but often its true
|
||||
// heading (surface messages): the way its nose points, which is what an
|
||||
// aircraft standing still is drawn by. Magnetic heading is not true north.
|
||||
const track =
|
||||
finiteNumber(aircraft?.track) ?? finiteNumber(aircraft?.true_heading);
|
||||
|
||||
return [
|
||||
hex,
|
||||
|
||||
@@ -49,3 +49,15 @@ test('keeps grounded fallback contacts and rejects rows without positions', () =
|
||||
assert.equal(normalized.states[0][7], null);
|
||||
assert.equal(normalized.states[0][8], true);
|
||||
});
|
||||
|
||||
test('an aircraft with no track takes its true heading, never its magnetic one', () => {
|
||||
const at = (extra) =>
|
||||
normalizeAdsbLolAircraftState(
|
||||
{ hex: 'abc123', lat: 37.7, lon: -122.2, alt_baro: 'ground', ...extra },
|
||||
1000,
|
||||
)[10];
|
||||
assert.equal(at({ track: 56.25, true_heading: 40 }), 56.25, 'track first');
|
||||
assert.equal(at({ true_heading: 244.69 }), 244.69, 'true heading');
|
||||
assert.equal(at({ mag_heading: 278.44 }), null, 'not magnetic');
|
||||
assert.equal(at({}), null, 'none');
|
||||
});
|
||||
@@ -248,7 +248,8 @@ export function normalizeDump1090Aircraft(json, nowMs, { band = '1090' } = {}) {
|
||||
lon: hasPosition ? position.lon : null,
|
||||
altitudeFt: altitude,
|
||||
groundSpeedKt: finiteOrNull(entry.gs),
|
||||
trackDeg: normalizeTrack(entry.track),
|
||||
// No track (parked, slow): the true heading, where the nose points.
|
||||
trackDeg: normalizeTrack(entry.track ?? entry.true_heading),
|
||||
verticalRateFpm: readAdsbVerticalRateFpm(entry),
|
||||
lastPositionAt: hasPosition
|
||||
? receivedAt - Math.max(0, seenPosS) * 1000
|
||||
|
||||
@@ -68,6 +68,34 @@ test('dump1090 aircraft.json maps to the shared local ADS-B records', () => {
|
||||
assert.deepEqual(summary, { heard: 4, positioned: 2 });
|
||||
});
|
||||
|
||||
test('a dump1090 aircraft with no track takes its true heading, never its magnetic one', () => {
|
||||
const trackOf = (extra) =>
|
||||
normalizeDump1090Aircraft(
|
||||
{
|
||||
now: 1,
|
||||
aircraft: [
|
||||
{
|
||||
hex: 'a4418c',
|
||||
lat: 37.6,
|
||||
lon: -122.38,
|
||||
alt_baro: 'ground',
|
||||
seen: 1,
|
||||
...extra,
|
||||
},
|
||||
],
|
||||
},
|
||||
1000,
|
||||
)[0].trackDeg;
|
||||
assert.equal(
|
||||
trackOf({ track: 56.25, true_heading: 40 }),
|
||||
56.25,
|
||||
'track first',
|
||||
);
|
||||
assert.equal(trackOf({ true_heading: 185.62 }), 185.62, 'true heading');
|
||||
assert.equal(trackOf({ mag_heading: 278.44 }), null, 'not magnetic');
|
||||
assert.equal(trackOf({}), null, 'none');
|
||||
});
|
||||
|
||||
test('dump1090 adapter skips invalid addresses and tolerates missing fields', () => {
|
||||
const records = normalizeDump1090Aircraft(
|
||||
{
|
||||
|
||||
@@ -71,7 +71,8 @@ export function normalizeReadsbAircraft(row, snapshotTimeMs) {
|
||||
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),
|
||||
// No track (parked, slow): the true heading, where the nose points.
|
||||
courseDeg: finite(row.track) ?? finite(row.true_heading),
|
||||
verticalRateMps: rateFtMin == null ? null : rateFtMin * 0.00508,
|
||||
category: row.category ?? null,
|
||||
typeCode: cleanText(row.t),
|
||||
|
||||
@@ -168,6 +168,22 @@ test('readsb units and position/contact ages are normalized once against the sou
|
||||
);
|
||||
});
|
||||
|
||||
test('a readsb aircraft with no track takes its true heading, never its magnetic one', () => {
|
||||
const course = (extra) =>
|
||||
normalizeReadsbAircraft(
|
||||
{ hex: 'a4418c', lat: 37.6, lon: -122.38, alt_baro: 'ground', ...extra },
|
||||
now,
|
||||
).courseDeg;
|
||||
assert.equal(
|
||||
course({ track: 56.25, true_heading: 40 }),
|
||||
56.25,
|
||||
'track first',
|
||||
);
|
||||
assert.equal(course({ true_heading: 185.62 }), 185.62, 'true heading');
|
||||
assert.equal(course({ mag_heading: 278.44 }), null, 'not magnetic');
|
||||
assert.equal(course({}), null, 'none');
|
||||
});
|
||||
|
||||
test('readsb vertical rate falls back to the GNSS rate when no baro rate is sent', () => {
|
||||
// readsb reports the rate under geom_rate when the aircraft's source is
|
||||
// GNSS; 26 of 157 positioned rows in a live /v2/mil fetch had only that.
|
||||
|
||||
Reference in new issue
Block a user