diff --git a/changelog.d/1096-military-hold-track-speed.md b/changelog.d/1096-military-hold-track-speed.md new file mode 100644 index 000000000..adc1793c4 --- /dev/null +++ b/changelog.d/1096-military-hold-track-speed.md @@ -0,0 +1,3 @@ +- 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 (Aditya, @Rohilalala, #1096). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 8c8896e19..df92c0d50 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); +});