Files
gps-denied-onboard/_docs/02_tasks/done/AZ-700_replay_map_visualization.md
Oleksandr Bezdieniezhnykh b66b68ff76 [AZ-700] gps-denied-render-map: HTML map of estimated vs truth tracks
New operator-side console-script renders a self-contained HTML map
(folium / Leaflet) comparing the estimator's JSONL track against
the tlog ground-truth track. Pinned visual style: red truth + blue
estimated polylines, start/end markers per track, 100 m + 50 m
scale circles, optional AZ-699 accuracy-summary banner, and an
--offline-tiles mode (with optional local tile-URL template) for
Jetsons without internet.

folium is gated behind a new [operator-tools] optional-dep so the
airborne binary's cold-start NFR is unaffected (C12 binary doesn't
import the new module). 14 new unit tests pin polyline count,
marker count, scale-circle radii, summary embedding, offline-tile
behaviour, and full CLI smoke. Zero mypy --strict errors.

Refines the 2026-05-20 Jetson-only test policy: unit tests may run
locally, e2e/perf/resilience/security stay Jetson-only. Documented
in _docs/02_document/tests/environment.md (Where each tier runs)
and .cursor/rules/testing.mdc (Test environment for this project).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 17:04:01 +03:00

7.6 KiB

Replay map visualization (estimated vs ground-truth tracks)

Task: AZ-700_replay_map_visualization Name: HTML map showing estimated GPS track vs tlog ground-truth track Description: New gps-denied-render-map console script. Takes a JSONL of estimator output + a tlog (or CSV fallback) and renders a single-file HTML map (folium / Leaflet) with both tracks in distinct colors, start/end markers, and an embedded accuracy summary from AZ-699. Complexity: 3 points Dependencies: AZ-699 Component: cli (offline analysis surface) Tracker: AZ-700 Epic: AZ-696

Problem

Today the only feedback from a replay run is a JSONL file. There is no way to visually verify whether the estimator is drifting, jumping, or roughly tracking the real flight. A human reading the JSONL cannot quickly answer "does this make sense geographically?"

The user's pipeline explicitly calls for: "and then show both points on the map."

Outcome

  • A standalone CLI gps-denied-render-map produces a self-contained HTML map of the estimated track + the tlog ground-truth track for any prior replay run.
  • The map is shareable as a single file (no server required); developers open it locally; AZ-701's HTTP API serves it back to API consumers.

Scope

Included

  • New module src/gps_denied_onboard/cli/render_map.py.
  • New console script gps-denied-render-map in pyproject.toml.
  • folium dependency pin in the appropriate [project.optional-dependencies] group (NOT in airborne-binary deps — operator-side only).
  • Default map style + tile provider (OpenStreetMap fallback documented for offline use).
  • Auto-fit bounds; distance circles (100 m, 50 m) around start point for scale.
  • Accuracy summary banner (read from _docs/06_metrics/real_flight_validation_{date}.md when --summary is passed).

Excluded

  • Interactive time-slider playback (deferred follow-up).
  • Embedded altitude profile chart.
  • Animated marker traversal.

Acceptance Criteria

AC-1: CLI produces self-contained HTML Given a JSONL + tlog When gps-denied-render-map --estimated out.jsonl --truth derkachi.tlog --output map.html runs Then map.html exists, parses as valid HTML, exits 0

AC-2: Two distinct tracks visible Given the rendered map opened in a browser When inspected Then it contains exactly two polyline layers (red = truth, blue = estimated) with start/end markers

AC-3: Markers + scale circles Given the rendered map When parsed Then it contains the start (green) + end (black) markers + 100 m + 50 m scale circles

AC-4: Accuracy summary inclusion Given --summary _docs/06_metrics/real_flight_validation_2026-XX-XX.md When the map renders Then the HTML header contains the accuracy metrics table

AC-5: Offline fallback documented Given an environment without internet access When the map is rendered with --offline-tiles Then tile loading uses a documented fallback (or fails fast with a clear error if no fallback is configured)

Non-Functional Requirements

Compatibility

  • Output HTML must render in Chrome 110+ and Firefox 110+ without console errors.

Performance

  • For a 60 s flight (~600 truth points + ~600 estimated points), render time < 5 s on Tier-1 hardware.

Unit Tests

AC Ref What to Test Required Outcome
AC-1 CLI invocation with synthetic data Output HTML file exists + non-empty
AC-2 Parse output HTML Exactly 2 polyline layers + 4 expected markers
AC-4 Summary embed Markdown summary metrics present in HTML

Blackbox Tests

AC Ref Initial Data/Conditions What to Test Expected Behavior NFR References
AC-1 Real Derkachi replay JSONL + tlog End-to-end render HTML opens in browser, both tracks visible Compat

Constraints

  • folium MUST be in the operator-only dep group; airborne binary cold-start regression test must remain green.
  • HTML output MUST be self-contained — embedded JS/CSS, no per-page CDN calls in --offline-tiles mode.
  • Console script naming follows the project pattern (gps-denied-<verb>).

Risks & Mitigation

Risk 1: folium dep size

  • Risk: folium pulls ~5 MB of JS. Adding to airborne deps would regress cold-start.
  • Mitigation: optional-dependencies group + ADR-002 build-time exclusion principle.

Risk 2: CDN dependency at render time

  • Risk: Default folium uses Leaflet via CDN — fails on offline Jetsons.
  • Mitigation: Document --offline-tiles flag; provide bundled assets path or fail-fast.

Implementation Notes (Batch 101 — Cycle 2)

Status: In Testing (Jira AZ-700).

Files changed

Production:

  • src/gps_denied_onboard/cli/render_map.py — new module: RenderInputs DTO, render_map_html, load_estimated_track, load_ground_truth_track, argparse CLI, main().
  • pyproject.toml — new [project.optional-dependencies] operator-tools = ["folium>=0.16,<1.0"] group; new console script gps-denied-render-map = "gps_denied_onboard.cli.render_map:main".

Tests:

  • tests/unit/test_az700_render_map.py — 14 unit tests covering JSONL parsing, HTML rendering (2 polylines, 4 markers, 2 scale circles, summary embed, offline-tiles toggle), and CLI smoke including a minimal binary-tlog helper.

AC coverage

AC Test / Artefact Result
AC-1 test_cli_writes_html_with_default_tiles PASS (local). The Jetson e2e visual smoke is AC-4 and is operator-driven on Tier-2.
AC-2 test_render_map_html_emits_two_polylines, …emits_four_markers_and_two_circles PASS
AC-3 test_render_map_html_emits_two_polylines, …emits_four_markers_and_two_circles PASS — output HTML contains exactly 2 polyline layers (red + blue) and 4 markers + 2 scale circles.
AC-4 Visual smoke on Tier-2 Jetson (operator opens map.html produced by AZ-699's e2e run) DEFERRED to Jetson — wired and ready.
AC-5 test_render_map_html_offline_tiles_omits_openstreetmap, …_template_uses_local_url PASS

Test results

pytest tests/unit/test_az700_render_map.py → 14 passed in 2.5 s. Wider regression slice (AZ-697/698/699/700 + replay_input + calibration): 107 passed, 1 skipped (pre-existing AC-5 e2e smoke that needs real video).

Strict typing

mypy --strict src/gps_denied_onboard/cli/render_map.pySuccess: no issues found in 1 source file. Used # type: ignore[import-untyped, import-not-found, unused-ignore] on the lazy folium import so the strict pass is clean whether folium is installed or not.

Design notes

  • folium 0.20 (the latest in the pinned range) was used. The default tile provider is OpenStreetMap (tiles="OpenStreetMap"); the AC-5 --offline-tiles flag drops the base layer entirely, and --offline-tiles-template accepts a local tile-URL template for operators with a bundled tile pack.
  • folium is lazy-imported inside _import_folium() so the airborne binary (which does NOT install [operator-tools]) doesn't pay for it on cold start. The C12 cold-start NFR is unaffected.
  • The _write_minimal_tlog test helper builds a binary tlog with just GLOBAL_POSITION_INT records — that's the minimum AZ-697 needs — without coupling the test to the full Derkachi CSV schema used by tests/e2e/replay/_tlog_synth.py.
  • All AZ-700 unit tests run locally per the refined test-environment policy (_docs/02_document/tests/environment.md § Where each tier runs); the Tier-2 visual-smoke AC-4 stays on the Jetson.