From 557d3bb41e7cc706d030177ac89d54f864f3ab00 Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:24:09 +0530 Subject: [PATCH] fix(military): hold the last track and speed when a poll omits them MilitaryFlightRecords coerced a missing course to 0 (`courseDeg || 0`) and a missing speed to 0 (`speedMps ?? 0`) before stickyNumber, so the hold-last-known-good merge never ran. When an adsb.lol row arrived without `track` or `gs`, the aircraft's icon swung to north and the position history got a 0 kt / 0 degree fix, so dead reckoning stopped or turned it north. The civil FlightRecords holds the last value for the same input. Pass the raw values to stickyNumber, with 0 as the fallback only until a value is known, as before. A grounded row without a speed still reads 0, so a parked aircraft doesn't coast at its landing speed. --- CHANGELOG.md | 4 ++++ docs/CURRENT-STATE.md | 4 +++- src/layers/military/records.js | 13 ++++++---- src/layers/military/records.test.mjs | 36 ++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd76d7a25..df39cef89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +- Military aircraft keep their last heading and speed when an adsb.lol row omits + `track` or `gs`, instead of turning north and stopping; a grounded row without + a speed still reads 0. + - Add Barcelona's bike share to the GBFS registry: Bicing (543 stations, the city's PBSC system) and AMBici (237 e-bike stations across the metropolitan area, run by nextbike). Both are keyless GBFS 2.3 feeds listed in diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 4c06dd3cb..b4a531482 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -584,7 +584,9 @@ pinning, first-position grace, source time and sea-surface placement are unchang Military flights also separate portable records and source acquisition from Cesium snapshot application. The metadata owner retains aviation feet, source -fix times, sticky fields and partial-feed retention. Rendering retains ground +fix times, sticky fields and partial-feed retention. A row without track or +speed holds the last known value, as civil records do; a grounded row without +speed reads 0. Rendering retains ground model ownership, motion history, military registration and tracking cleanup. Civil flights separate source acquisition, portable record reconciliation and diff --git a/src/layers/military/records.js b/src/layers/military/records.js index 9cd6754e6..05d91ccf0 100644 --- a/src/layers/military/records.js +++ b/src/layers/military/records.js @@ -39,8 +39,6 @@ export class MilitaryFlightRecords { ? prevMeta.altitudeFt * 0.3048 : 0 : 3048); - const track = aircraft.courseDeg || 0; - const speedMps = aircraft.speedMps ?? 0; const verticalRateMps = aircraft.verticalRateMps; const callsign = aircraft.callsign; const type = aircraft.typeCode; @@ -148,8 +146,15 @@ export class MilitaryFlightRecords { // altitudeFt/baro). geoAltitudeM, renderAltitudeM, - speedMps: stickyNumber(speedMps, prevMeta?.speedMps, null), - track: stickyNumber(track, prevMeta?.track, null), + // A poll that omits track/speed holds the last values, like the civil + // records; 0 only until one is known. A grounded row without a speed + // reads 0, so a parked plane never coasts at its landing speed. + speedMps: stickyNumber( + aircraft.speedMps, + onGround ? 0 : prevMeta?.speedMps, + 0, + ), + track: stickyNumber(aircraft.courseDeg, prevMeta?.track, 0), // Analyst seam (additive): sticky like the other kinematics. verticalRateMps: stickyNumber( verticalRateMps, diff --git a/src/layers/military/records.test.mjs b/src/layers/military/records.test.mjs index 5054fb28a..ce00c7761 100644 --- a/src/layers/military/records.test.mjs +++ b/src/layers/military/records.test.mjs @@ -114,3 +114,39 @@ test('military emergency status follows the latest poll instead of sticking', () ); assert.equal(next.meta.emergency, null); }); + +test('military records hold the last track and speed when a poll omits them', () => { + const records = store(); + records.receive(observation({ courseDeg: 270, speedMps: 200 }), context()); + const missing = records.receive( + observation({ courseDeg: null, speedMps: null }), + context(), + ); + assert.equal(missing.meta.track, 270); + assert.equal(missing.meta.speedMps, 200); + const north = records.receive( + observation({ courseDeg: 0, speedMps: 0 }), + context(), + ); + assert.equal(north.meta.track, 0); + assert.equal(north.meta.speedMps, 0); + const unseen = store().receive( + observation({ courseDeg: undefined, speedMps: undefined }), + context(), + ); + assert.equal(unseen.meta.track, 0); + assert.equal(unseen.meta.speedMps, 0); + const landing = store(); + landing.receive(observation({ courseDeg: 90, speedMps: 70 }), context()); + const parked = landing.receive( + observation({ + onGround: true, + baroAltitudeM: null, + courseDeg: null, + speedMps: null, + }), + context(), + ); + assert.equal(parked.meta.speedMps, 0); + assert.equal(parked.meta.track, 90); +});