From 8fec5a76bfd9779a5ac3a24c920d05e3cb31896b Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Tue, 10 Nov 2026 11:15:18 -0800 Subject: [PATCH 1/2] feat(flights): a tracked flight's trail splits at gaps, follows the drawn aircraft, and a lost aircraft coasts then reads SIGNAL LOST The trail no longer joins a day of history into one line. It lifts the pen across a gap of more than 48 minutes, an implied speed over 1,500 kt, or a report without altitude (a parked aircraft within 0.1 nm for under 3 h stays joined), and every history point is kept as its own segment. History never gets an invented altitude. The live end is the path the aircraft was drawn along, thinned to its shape, so it no longer cuts corners through turns; a late report trims what was drawn after it. Old segments are frozen in chunks, so a 2,000-point trail costs about 0.1 ms a frame. A tracked aircraft that misses an update shows STALE and is dimmed. It keeps flying on its last speed and heading for five minutes (at most 1,500 kt), then stops, dimmed, labelled SIGNAL LOST with the time since its last report, and is kept for ten minutes after that report so the camera keeps its target. Aircraft on or near known ground hold at their last report instead of coasting. Military flights follow the same rules. --- docs/CURRENT-STATE.md | 107 ++- scripts/package-boundaries.json | 5 + scripts/qa-trail-freeze.mjs | 241 ++++++ scripts/track-regression.mjs | 174 ++++- src/data/motionModel.js | 189 ++++- src/data/routeSegment.test.mjs | 5 +- src/data/trailContinuity.js | 607 +++++++++++++++ src/data/trailRenderer.js | 327 ++++++-- src/data/trailRenderer.test.mjs | 252 +++++- src/data/trailReports.test.mjs | 165 ++++ src/data/trailThinning.test.mjs | 180 +++++ src/i18n/locales/ar.json | 1 + src/i18n/locales/de.json | 1 + src/i18n/locales/es.json | 1 + src/i18n/locales/fr.json | 1 + src/i18n/locales/he.json | 1 + src/i18n/locales/it.json | 1 + src/i18n/locales/ja.json | 1 + src/i18n/locales/pt.json | 1 + src/i18n/locales/ru.json | 1 + src/i18n/locales/uk.json | 1 + src/i18n/locales/zh-CN.json | 1 + src/i18n/locales/zh-Hant.json | 1 + src/layers/flights/motion.js | 100 ++- src/layers/flights/policy.js | 5 +- src/layers/flights/recordPolicy.js | 4 + src/layers/flights/records.js | 73 +- src/layers/flights/records.test.mjs | 96 +++ src/layers/flights/rendering.js | 26 +- src/layers/flights/snapshotRenderer.js | 26 +- src/layers/flights/snapshotStale.test.mjs | 153 ++++ src/layers/flights/staleHold.test.mjs | 421 ++++++++++ src/layers/flights/state.js | 27 +- src/layers/flights/tracking.js | 358 ++++++--- src/layers/flights/trail.test.mjs | 414 ++++++++++ src/layers/flights/trailRecord.test.mjs | 846 +++++++++++++++++++++ src/layers/military/motion.js | 95 ++- src/layers/military/policy.js | 5 +- src/layers/military/recordPolicy.js | 4 + src/layers/military/records.js | 64 +- src/layers/military/records.test.mjs | 95 +++ src/layers/military/rendering.js | 26 +- src/layers/military/snapshotRenderer.js | 22 +- src/layers/military/snapshotStale.test.mjs | 152 ++++ src/layers/military/staleHold.test.mjs | 199 +++++ src/layers/military/state.js | 27 +- src/layers/military/tracking.js | 390 +++++++--- src/layers/military/trail.test.mjs | 260 +++++++ src/layers/military/trailRecord.test.mjs | 697 +++++++++++++++++ 49 files changed, 6486 insertions(+), 363 deletions(-) create mode 100644 scripts/qa-trail-freeze.mjs create mode 100644 src/data/trailContinuity.js create mode 100644 src/data/trailReports.test.mjs create mode 100644 src/data/trailThinning.test.mjs create mode 100644 src/layers/flights/snapshotStale.test.mjs create mode 100644 src/layers/flights/staleHold.test.mjs create mode 100644 src/layers/flights/trail.test.mjs create mode 100644 src/layers/flights/trailRecord.test.mjs create mode 100644 src/layers/military/snapshotStale.test.mjs create mode 100644 src/layers/military/staleHold.test.mjs create mode 100644 src/layers/military/trail.test.mjs create mode 100644 src/layers/military/trailRecord.test.mjs diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 5c2e52c58..82f6f65b3 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1444,6 +1444,52 @@ use the supplied asset resolver, including the preload path. Enrichment receives an abort signal and cannot update a later lifecycle after destruction. Existing camera, terrain floor, trail, selection and measured model-size policies remain. +A tracked civil or military aircraft's trail (`src/data/trailContinuity.js`) lifts +the pen, drawing nothing, between two points more than 48 minutes or over 1,500 kt +apart (a parked aircraft within 0.1 nm keeps one line for up to three hours) and at a +waypoint with no altitude; a ground waypoint is drawn on the ground. The head joins the +aircraft only within 150 km. Each trail keeps up to 2,000 points, a full day of history +at one fix per minute plus the live session; past that the oldest recorded points go +first, and the oldest reports only once none are left. The renderer +(`src/data/trailRenderer.js`) freezes every chunk of 128 points except the newest into +static geometry, so a long trail costs about as much per frame as a short one; +`scripts/qa-trail-freeze.mjs` checks that a freeze never shows a blank frame. +While an aircraft is tracked, its trail between reports is the path its icon was +drawn along: the displayed position is recorded about once a second or every 150 m +(`recordTrailPoint`). Recorded points are thinned by shape: one stays only where +dropping it would move the line more than 30 m across the track or in height from any +position it stood for, or leave a straight piece longer than 10 km, so straight flight +adds a vertex every 10 km at most and a turn keeps its curve. A recorded point that +would turn the line back sharper than 120° over a short step is dropped. Reports stay +in the trail as anchors, in time order. Recorded points never join two reports that are +not one flight under the 48-minute and 1,500 kt rules; the points between them go and +nothing is recorded across such a pair. A report for a time the display has already +passed replaces what was drawn after it, and nothing is recorded until the icon has +eased onto it, so the line reads arc, report, corrected path. History from before tracking started, and the backfill, run report to report. +The trail never runs past the icon or through its 3D model, and the head runs from the +newest kept point outside the model to the icon (to the model's tail when the model is +the visual). A contact with no new report coasts along its reported track for 5 minutes +past its last real report, at no more than 1,500 kt and never below known ground, then stops; +one that +went quiet on the ground, or within 150 m of a known ground floor, holds at its last +report, records nothing and draws no head (with the ground unknown, a low barometric +altitude alone does not hold it). The hold-or-coast choice is made once per report (`staleHoldsForFix` +in `src/data/motionModel.js`), and the forward fix built when a repeated position carries +new speed or course follows the same rules and is never a trail report. A live report +with no altitude breaks the line and leaves no point at an invented height, and so does +such a report seeded from position history when tracking starts. + +A contact that misses an update is stale from the first miss: it is drawn with the +45%-alpha stale fade (`STALE_ALPHA`), the tracked billboard and 3D model included, and the +tracked label reads STALE. For the tracked aircraft a poll that re-sends a fix no newer +than the last one is a miss too. Once its coast has ended, or it holds at its last report +after a miss, it stands still, dimmed, and the label reads SIGNAL LOST ·