feat: uncluster tracked-station markers and thin overlaps by screen distance #43

Merged
graham merged 1 commit from feat/tracked-station-declutter into main 2026-10-19 16:45:37 +00:00
Owner

Problem

When tracking a single station (e.g. W5ISP-9), every position marker for that station went into the Leaflet cluster group. At zoom < 11 the whole track collapsed into numbered cluster bubbles (4, 9, …) — you saw blobs, not the track.

What changed

Client (map.ts)

  • New trackedMarkerLayer — a plain LayerGroup that never passes through markerClusterGroup. Tracked-station markers route there on add and get re-homed when tracking changes.
  • declutterTrackedMarkers — greedy minimum-distance thinning at 28px screen separation, newest-first so the track head always wins. Overlapping positions are hidden (display:none), not removed: trails, OMS state, and re-show on zoom all keep working. rAF-coalesced; runs on zoomend, marker adds, and tracking changes.
  • Matching checks callsign and callsign_group, SSID-flexible in both directions (tracking W5ISP covers W5ISP-*; tracking W5ISP-9 covers the W5ISP group).
  • Tracked markers exempt from viewport pruning; removal, clear, heat-map, and destroy paths handle the new layer.

Server (map_live)

  • Pushes set_tracked_callsign on map_ready, track/clear events, and URL reconcile, so the client knows the tracked callsign before markers arrive.
  • show_trail_line gate widened from zoom <= 8 to < 11 so the full track is drawn at zooms 9–10 too; new sync_trail_line_for_zoom/3 sends/clears it on boundary crossings (cleared at ≥ 11 where client trails take over).

Verified live

Zoom Result
12 0 clusters; 4/9 tracked markers hidden by declutter
9 0 cluster bubbles (previously all positions grouped)
8 Trail line drawn (existing low-zoom mode)
Untracked z9 12 cluster bubbles — clustering intact for other stations
Clear URL untracks, hidden markers unhide, re-clustering works

Tests

2286/2286 green (map suite 382/382); format + credo --strict + dialyzer clean via pre-commit hooks.

## Problem When tracking a single station (e.g. `W5ISP-9`), every position marker for that station went into the Leaflet cluster group. At zoom < 11 the whole track collapsed into numbered cluster bubbles (4, 9, …) — you saw blobs, not the track. ## What changed **Client (`map.ts`)** - New `trackedMarkerLayer` — a plain `LayerGroup` that never passes through `markerClusterGroup`. Tracked-station markers route there on add and get re-homed when tracking changes. - `declutterTrackedMarkers` — greedy minimum-distance thinning at 28px screen separation, newest-first so the track head always wins. Overlapping positions are *hidden* (`display:none`), not removed: trails, OMS state, and re-show on zoom all keep working. rAF-coalesced; runs on `zoomend`, marker adds, and tracking changes. - Matching checks `callsign` and `callsign_group`, SSID-flexible in both directions (tracking `W5ISP` covers `W5ISP-*`; tracking `W5ISP-9` covers the `W5ISP` group). - Tracked markers exempt from viewport pruning; removal, clear, heat-map, and destroy paths handle the new layer. **Server (`map_live`)** - Pushes `set_tracked_callsign` on `map_ready`, track/clear events, and URL reconcile, so the client knows the tracked callsign before markers arrive. - `show_trail_line` gate widened from `zoom <= 8` to `< 11` so the full track is drawn at zooms 9–10 too; new `sync_trail_line_for_zoom/3` sends/clears it on boundary crossings (cleared at ≥ 11 where client trails take over). ## Verified live | Zoom | Result | |---|---| | 12 | 0 clusters; 4/9 tracked markers hidden by declutter | | 9 | **0 cluster bubbles** (previously all positions grouped) | | 8 | Trail line drawn (existing low-zoom mode) | | Untracked z9 | 12 cluster bubbles — clustering intact for other stations | | Clear | URL untracks, hidden markers unhide, re-clustering works | ## Tests 2286/2286 green (map suite 382/382); format + credo --strict + dialyzer clean via pre-commit hooks.
feat: uncluster tracked-station markers and thin overlaps by screen distance
Some checks failed
Elixir CI / Dialyzer (pull_request) Successful in 58s
skippy-bot/review Skippy review: 1 blocking finding open — see the PR thread
Elixir CI / Build and Push Docker Image (pull_request) Has been cancelled
Elixir CI / Build and test (pull_request) Has been cancelled
4a027d8fe1
Tracked stations' position markers no longer collapse into numbered
cluster bubbles at zoom < 11. Their markers live on a dedicated
unclustered layer; overlapping positions are hidden (not removed) by a
greedy 28px minimum-separation pass that keeps the newest point, so the
full track stays visible while redundant dots thin out.

- server pushes set_tracked_callsign on map_ready, track/clear, and URL
  reconcile so the client knows the tracked callsign before markers arrive
- trail line (show_trail_line) gate widened from zoom <= 8 to < 11 so the
  track is drawn at zooms 9-10 too; cleared when client trails take over
- tracked markers exempt from viewport pruning; removal, clear, heat-map,
  and destroy paths handle the new layer
- matching checks callsign and callsign_group, SSID-flexible both ways
skippy-bot left a comment

🤖 Skippy PR review

1 finding — 1 blocking before merge.

Severity Location Issue
🟡 Warning lib/aprsme_web/live/map_live/events.ex:168 Clearing the callsign form leaves the trail line drawn at zoom 9-10

Reviewed 4a027d8fe15c. Comment skippy review to re-run.

### 🤖 Skippy PR review **1 finding** — 1 blocking before merge. | Severity | Location | Issue | | --- | --- | --- | | 🟡 Warning | `lib/aprsme_web/live/map_live/events.ex:168` | Clearing the callsign form leaves the trail line drawn at zoom 9-10 | <sub>Reviewed `4a027d8fe15c`. Comment `skippy review` to re-run.</sub> <!-- skippy-pr-review -->
@ -161,6 +165,7 @@ defmodule AprsmeWeb.MapLive.Events do
socket
|> assign(tracked_callsign: "", other_ssids: [])
|> update_url_with_current_state()
|> push_event("set_tracked_callsign", %{callsign: ""})
First-time contributor

🟡 Warning — Clearing the callsign form leaves the trail line drawn at zoom 9-10

Submitting the emptied Track form (handle_track_callsign("")) clears tracked_callsign and pushes set_tracked_callsign, but unlike handle_clear_tracking/1 and Index.reconcile_tracked_callsign/4 it never pushes clear_trail_line, and the client's setTrackedCallsign/1 only re-homes markers, so the L.polyline stays on the map.

That gap used to self-heal at zoom <= 8 (the next bounds update pushes show_heat_map, whose handler removes self.trailLine). With the gate widened to zoom < 11 it is now permanent in the 9-10 band: a bounds update short-circuits on tracked_callsign == "", sync_trail_line_for_zoom/3 returns early on the empty callsign, and nothing above zoom 8 ever pushes show_heat_map - so panning/zooming 9<->10 keeps a full track drawn for a station the UI reports as untracked (the follow-up push_patch to / reconciles to the same "", so it clears nothing either).

Fix: add the clear beside the new push, matching the other two clear paths:

socket
|> assign(tracked_callsign: "", other_ssids: [])
|> push_event("clear_trail_line", %{})
|> update_url_with_current_state()
|> push_event("set_tracked_callsign", %{callsign: ""})
**🟡 Warning — Clearing the callsign form leaves the trail line drawn at zoom 9-10** Submitting the emptied Track form (`handle_track_callsign("")`) clears `tracked_callsign` and pushes `set_tracked_callsign`, but unlike `handle_clear_tracking/1` and `Index.reconcile_tracked_callsign/4` it never pushes `clear_trail_line`, and the client's `setTrackedCallsign/1` only re-homes markers, so the `L.polyline` stays on the map. That gap used to self-heal at zoom <= 8 (the next bounds update pushes `show_heat_map`, whose handler removes `self.trailLine`). With the gate widened to `zoom < 11` it is now permanent in the 9-10 band: a bounds update short-circuits on `tracked_callsign == ""`, `sync_trail_line_for_zoom/3` returns early on the empty callsign, and nothing above zoom 8 ever pushes `show_heat_map` - so panning/zooming 9<->10 keeps a full track drawn for a station the UI reports as untracked (the follow-up `push_patch` to `/` reconciles to the same `""`, so it clears nothing either). Fix: add the clear beside the new push, matching the other two clear paths: ```elixir socket |> assign(tracked_callsign: "", other_ssids: []) |> push_event("clear_trail_line", %{}) |> update_url_with_current_state() |> push_event("set_tracked_callsign", %{callsign: ""}) ```
skippy-bot marked this conversation as resolved
graham force-pushed feat/tracked-station-declutter from 4a027d8fe1
Some checks failed
Elixir CI / Dialyzer (pull_request) Successful in 58s
skippy-bot/review Skippy review: 1 blocking finding open — see the PR thread
Elixir CI / Build and Push Docker Image (pull_request) Has been cancelled
Elixir CI / Build and test (pull_request) Has been cancelled
to 443786d8e5
Some checks failed
Elixir CI / Dialyzer (pull_request) Successful in 1m4s
Elixir CI / Build and test (pull_request) Successful in 1m59s
Elixir CI / Build and Push Docker Image (pull_request) Has been skipped
skippy-bot/review Skippy review: 1 blocking finding open — see the PR thread
2026-10-19 16:24:38 +00:00
Compare
skippy-bot left a comment

🤖 Skippy PR review

1 finding — 1 blocking before merge.

Severity Location Issue
🟡 Warning lib/aprsme_web/live/map_live/bounds_updater.ex:105 Widened trail-line gate has no data to draw from at zooms 9-10

Resolved 1 earlier finding: the emptied Track form now pushes clear_trail_line beside set_tracked_callsign (with a test). 1 new finding on the widened trail-line gate.

Reviewed 443786d8e5bb. Comment skippy review to re-run.

### 🤖 Skippy PR review **1 finding** — 1 blocking before merge. | Severity | Location | Issue | | --- | --- | --- | | 🟡 Warning | `lib/aprsme_web/live/map_live/bounds_updater.ex:105` | Widened trail-line gate has no data to draw from at zooms 9-10 | Resolved 1 earlier finding: the emptied Track form now pushes clear_trail_line beside set_tracked_callsign (with a test). 1 new finding on the widened trail-line gate. <sub>Reviewed `443786d8e5bb`. Comment `skippy review` to re-run.</sub> <!-- skippy-pr-review -->
@ -103,3 +103,3 @@
# historical loader send heat map
socket =
if socket.assigns.tracked_callsign != "" and socket.assigns.map_zoom <= 8 do
if socket.assigns.tracked_callsign != "" and socket.assigns.map_zoom < 11 do
First-time contributor

🟡 Warning — Widened trail-line gate has no data to draw from at zooms 9-10

send_trail_line_for_tracked_callsign/1 builds the polyline from socket.assigns.visible_packets ++ historical_packets, and neither holds the track at zoom 9-10. visible_packets is keyed by get_callsign_key/1 (display name), so it carries at most ONE packet for the tracked station, and historical_packets is %{} in markers mode: HistoricalLoader.start_progressive_historical_loading/1 resets it at the top of every load (both branches) and the zoom > 8 clause of handle_zoom_based_display/5 discards the fetched batches (_historical_packets) after pushing them to the client.

Failure path: zoom 8 -> 9 draws the full line from the retained zoom-8 dataset, then the first bounds update at 9 resets historical_packets and re-sends a payload whose only point is the single live packet. The client's show_trail_line handler removes self.trailLine BEFORE the validPoints.length < 2 check, so the track line disappears and stays gone while the user stays in 9-10; entering from 11 never draws it at all.

Fix: give the widened gate data - in the markers-mode clause of handle_zoom_based_display/5, merge the batches into historical_packets (bounded, as the zoom <= 8 clause does) so 9-10 can build the track - or leave the gate at <= 8. Worth also making the client return before removing self.trailLine when the payload has fewer than 2 valid points.

**🟡 Warning — Widened trail-line gate has no data to draw from at zooms 9-10** `send_trail_line_for_tracked_callsign/1` builds the polyline from `socket.assigns.visible_packets ++ historical_packets`, and neither holds the track at zoom 9-10. `visible_packets` is keyed by `get_callsign_key/1` (display name), so it carries at most ONE packet for the tracked station, and `historical_packets` is `%{}` in markers mode: `HistoricalLoader.start_progressive_historical_loading/1` resets it at the top of every load (both branches) and the `zoom > 8` clause of `handle_zoom_based_display/5` discards the fetched batches (`_historical_packets`) after pushing them to the client. Failure path: zoom 8 -> 9 draws the full line from the retained zoom-8 dataset, then the first bounds update at 9 resets `historical_packets` and re-sends a payload whose only point is the single live packet. The client's `show_trail_line` handler removes `self.trailLine` BEFORE the `validPoints.length < 2` check, so the track line disappears and stays gone while the user stays in 9-10; entering from 11 never draws it at all. Fix: give the widened gate data - in the markers-mode clause of `handle_zoom_based_display/5`, merge the batches into `historical_packets` (bounded, as the `zoom <= 8` clause does) so 9-10 can build the track - or leave the gate at `<= 8`. Worth also making the client return before removing `self.trailLine` when the payload has fewer than 2 valid points.
skippy-bot marked this conversation as resolved
graham force-pushed feat/tracked-station-declutter from 443786d8e5
Some checks failed
Elixir CI / Dialyzer (pull_request) Successful in 1m4s
Elixir CI / Build and test (pull_request) Successful in 1m59s
Elixir CI / Build and Push Docker Image (pull_request) Has been skipped
skippy-bot/review Skippy review: 1 blocking finding open — see the PR thread
to fb7c70794c
All checks were successful
Elixir CI / Dialyzer (pull_request) Successful in 1m5s
Elixir CI / Build and test (pull_request) Successful in 1m23s
Elixir CI / Build and Push Docker Image (pull_request) Has been skipped
skippy-bot/review Skippy review: clean — no open findings
2026-10-19 16:38:22 +00:00
Compare
First-time contributor

Resolved 1 of 1 earlier findings — the widened trail-line gate now has data to draw from: handle_zoom_based_display/5 stores the 9-10 batches in historical_packets (bounded by apply_memory_limit/2) and re-sends the trail on the final batch, with maybe_push_final_trail_line/1 covering the packet-cap and empty-drain exits.

Nothing new in 443786d8..fb7c7079.

Resolved 1 of 1 earlier findings — the widened trail-line gate now has data to draw from: `handle_zoom_based_display/5` stores the 9-10 batches in `historical_packets` (bounded by `apply_memory_limit/2`) and re-sends the trail on the final batch, with `maybe_push_final_trail_line/1` covering the packet-cap and empty-drain exits. Nothing new in `443786d8..fb7c7079`. <!-- skippy-pr-review -->
graham merged commit 5fb4763322 into main 2026-10-19 16:45:37 +00:00
graham deleted branch feat/tracked-station-declutter 2026-10-19 16:45:37 +00:00
Sign in to join this conversation.
No description provided.