mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +00:00
Merge pull request #1732 from bilawalsidhu/fix/adsblol-true-heading
fix(flights): an aircraft with no ground track faces its true heading, not north
This commit is contained in:
7 files changed
+69
-3
No files matched your search
@@ -0,0 +1,4 @@
|
||||
- An aircraft that reports no ground track (parked or taxiing, from adsb.lol
|
||||
or a local receiver) now faces its reported true heading instead of north,
|
||||
so one taxiing south no longer looks like it is moving backwards
|
||||
(samehkhamis, #1732).
|
||||
@@ -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