Skip to content

Commit 5bcb4ff

Browse files
Yerazeclaude
andauthored
feat(map): node selection & emphasis on Map Analysis (#3788 Phase 1) (#4010)
* feat(map): add unifiedNodeKey identity helper (#3788) Canonical cross-source node-identity helper for the upcoming node selection/follow feature — mt:<nodeNum> / mc:<publicKey> keying, plus isNodeEmphasized/selectionOpacity dimming helpers. Includes a drift-guard test asserting the keys match what mergeUnifiedSourceData buckets on. * refactor(dashboard): use unifiedNodeKey in mergeUnifiedSourceData (#3788) Replace the inline mt:/mc: keying block with a call to the new unifiedNodeKey() helper so the merge and the node-selection feature can never drift on identity. Behavior-preserving: unifiedNodeKey implements the exact same MeshCore publicKey/nodeId fallback and Meshtastic numeric-nodeNum checks the inline code did, including the null-key skip. * feat(map): add selectedNodeIds to MapAnalysisConfig (#3788) Persisted array of unifiedNodeKey identities driving the map's node selection/follow/dimming feature. load() coerces a missing or non-array value to [] so old persisted configs and garbage don't throw. Adds setSelectedNodeIds mirroring the existing setSources pattern. * feat(map): shared node hook + selection dimming + picker wiring (#3788) - useAnalysisNodes(): extract the markers filter (hideFromMap, search, node-type, source allow-list) into a shared hook so the picker and the map markers layer agree on identity/visibility. Moves NodeRecord out of NodeMarkersLayer and adds publicKey for unifiedNodeKey. - NodeMarkersLayer: consume useAnalysisNodes() instead of its inline filter; apply selection dimming via the Marker opacity prop only (isNodeEmphasized/selectionOpacity), leaving iconSig/divIcon and the spiderfier keyOf untouched. - NodeMultiSelect: new toolbar picker mirroring SourceMultiSelect (pill + popover, Select all / Clear). - MapAnalysisToolbar: build deduped/sorted node options from useAnalysisNodes() and mount NodeMultiSelect after NodeSearchControl. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AFBA76hsjqhsXe1BdnYub * test(map): cover useAnalysisNodes, NodeMultiSelect, marker dimming (#3788) - useAnalysisNodes.test.tsx: positioned+visible filtering, mt:/mc: key derivation, hideFromMap/unpositioned exclusion, config.sources allow-list. - NodeMultiSelect.test.tsx: pill label, checkbox toggle, Select all, Clear (only shown when a selection is active). - NodeMarkersLayer.test.tsx (spec test #6, optional stretch): thin render test mocking react-leaflet's Marker to capture the opacity prop, proving the isNodeEmphasized/selectionOpacity wiring dims unselected markers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AFBA76hsjqhsXe1BdnYub * feat(map): dim unselected node trails on the analysis map (#3788) Apply the WP-A selection-emphasis helpers (isNodeEmphasized/selectionOpacity) to PositionTrailsLayer polylines, mirroring marker dimming. Trails are Meshtastic-only (nodeNum-keyed), so identity is `mt:${nodeNum}`; an empty selection leaves opacity unchanged at 0.7. * docs(map): document node selection & emphasis; add epic plan + Phase 1 spec (#3788) - User docs: map-analysis.md gains a "Node selection & emphasis" section, a toolbar-table row, and a persistence-list mention. - Internal: commit the epic plan (MAP_FOLLOW_AUTOZOOM_EPIC.md) with Phase 1 marked done + browser-validation evidence, and the Phase 1 implementation spec (PHASE1_NODE_SELECTION_SPEC.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AFBA76hsjqhsXe1BdnYub --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e9aed2e commit 5bcb4ff

17 files changed

Lines changed: 1060 additions & 70 deletions

docs/features/map-analysis.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The toolbar runs across the top of the canvas. From left to right:
3434
| --- | --- |
3535
| **Source multi-select** | Pick which sources contribute to every layer. "All sources" is the default. |
3636
| **Search box** | Filter visible markers (and traceroute link endpoints) by name or node number. See [Node search](#node-search). |
37+
| **Node multi-select** | Pick specific nodes to emphasize. Selected nodes render at full opacity; everything else dims. "All nodes" (empty selection) is the default. See [Node selection & emphasis](#node-selection-emphasis). |
3738
| **Time slider toggle** | Show/hide the floating time-window slider. |
3839
| **Layer buttons (×8)** | Toggle each visualization layer on/off. The right-edge chevron opens a popover for layer-specific options (lookback window, sub-options). |
3940
| **Progress bar** | Shows aggregate loading state while any layer is fetching. |
@@ -63,6 +64,23 @@ The toolbar **search box** hides every marker that doesn't match the term and co
6364

6465
Clear the box to restore the full map. The search term lives in the workspace state alongside the other toolbar controls, so it composes with the source filter and every layer toggle.
6566

67+
### Node selection & emphasis
68+
69+
The toolbar **node multi-select** ("All nodes") lets you pin a specific set of
70+
nodes. The picker lists every node currently on the map — so it already
71+
reflects the source, node-type, and search filters — with **Select all** and
72+
**Clear** shortcuts.
73+
74+
With one or more nodes selected, the map keeps *every* node visible but
75+
**dims the unselected ones** (both their markers and their position trails),
76+
so your chosen nodes stand out without losing surrounding context. An empty
77+
selection is the default and dims nothing.
78+
79+
Selection is keyed on a node's stable cross-source identity (Meshtastic
80+
`nodeNum`, MeshCore public key), so a node reported by several sources stays a
81+
single entry. The set persists per-browser in the workspace config alongside
82+
the other toolbar controls.
83+
6684
## Layers
6785

6886
### Node markers
@@ -153,7 +171,7 @@ Map Analysis introduces **no new permission resource**. Page access is public (m
153171

154172
## Persistence
155173

156-
All toolbar state — layer toggles, lookback selections, source filter, time slider window, inspector visibility — is persisted to a single versioned `localStorage` key (`mapAnalysis.config.v1`). It's per-browser, not per-account. The schema is versioned for future migration to server-persisted defaults.
174+
All toolbar state — layer toggles, lookback selections, source filter, node selection, time slider window, inspector visibility — is persisted to a single versioned `localStorage` key (`mapAnalysis.config.v1`). It's per-browser, not per-account. The schema is versioned for future migration to server-persisted defaults.
157175

158176
## Limitations (v1)
159177

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Epic — Follow Nodes on the Map (Auto-center / Auto-zoom)
2+
3+
**Issue:** #3788 — "[FEAT] Ability to follow nodes on the map"
4+
**Orchestrator model:** Opus 4.8
5+
**Started:** 2026-07-08
6+
7+
## Goal
8+
9+
Let a user pick a set of sources & nodes on the existing **Map Analysis**
10+
workspace (`/analysis`) and have the map keep them in view as they move:
11+
12+
- **Follow mode** — on each position update, recenter the map to the *average
13+
center* of the selected nodes' current positions, preserving zoom.
14+
- **Auto-zoom** — on each update, fit the map to the bounding box of the
15+
selected nodes' current positions with a **15% margin**.
16+
- Selection also drives visual emphasis: selected nodes render at full
17+
emphasis, unselected nodes are **dimmed but still visible**.
18+
- Multi-source by design: current positions come from the cross-source merge
19+
(`useDashboardUnifiedData``mergeUnifiedSourceData`, dedup by
20+
`mt:<nodeNum>` / `mc:<publicKey>`), so a node's position is the freshest fix
21+
seen by any source (incl. MQTT).
22+
23+
## Interview decisions (2026-07-08)
24+
25+
1. **Home:** Extend the existing `/analysis` `MapAnalysisPage` workspace — NOT a
26+
new Dashboard panel or standalone page. Maximum reuse of `MapAnalysisContext`,
27+
`SourceMultiSelect`, `PositionTrailsLayer`, `NodeMarkersLayer`,
28+
`useMapAnalysisConfig` (localStorage persistence).
29+
2. **Selection semantics:** **Dim unselected, keep visible.** When the selection
30+
is non-empty, unselected node markers + trails are de-emphasized (reduced
31+
opacity); selected nodes stay full-emphasis. Follow/Auto-zoom operate on the
32+
selected set only. (An empty selection = today's behavior, nothing dimmed.)
33+
3. **Manual-pan conflict:** While Follow is active, a manual pan/zoom **pauses**
34+
Follow and surfaces a "Resume follow" affordance; the map does not yank back
35+
on the next update until the user resumes.
36+
4. **Phase split:** 2 phases (below).
37+
38+
## Reuse anchors (from Stage-0 survey — do not rebuild these)
39+
40+
- **Workspace shell:** `src/pages/MapAnalysisPage.tsx` (`ToastProvider > SettingsProvider > MapAnalysisProvider > [MapAnalysisToolbar, MapAnalysisCanvas, AnalysisInspectorPanel]`).
41+
- **Config + selection state:** `src/components/MapAnalysis/MapAnalysisContext.tsx` (`useMapAnalysisCtx`) spreads `useMapAnalysisConfig()` and adds `selected`/`nodeFilter`. Config persisted to `localStorage['mapAnalysis.config.v1']` via `src/hooks/useMapAnalysisConfig.ts` (`MapAnalysisConfig` at :40-52).
42+
- **Toolbar + source multi-select:** `src/components/MapAnalysis/MapAnalysisToolbar.tsx`, `SourceMultiSelect.tsx`, `NodeSearchControl.tsx`, `NodeTypeFilterControl.tsx`.
43+
- **Canvas + layers:** `src/components/MapAnalysis/MapAnalysisCanvas.tsx` (`MapContainer` at :43, pane stack :49-75). Markers: `layers/NodeMarkersLayer.tsx` (live positions via `useDashboardUnifiedData`, :145). Trails: `layers/PositionTrailsLayer.tsx` (one `Polyline` per `sourceId:nodeNum`, fed by `usePositions`).
44+
- **View controllers to mirror:** `MapBoundsUpdater` (one-shot `fitBounds`, `DashboardMap.tsx:136-155`), `src/components/MapCenterController.tsx` (`setView` to target), `src/components/ZoomHandler.tsx`, `MapPositionHandler.tsx`. All are `useMap()` children returning `null` — droppable into the Analysis `MapContainer`.
45+
- **Live position feed:** `useDashboardUnifiedData(sources, enabled)` (`src/hooks/useDashboardData.ts:436`), `refetchInterval: DASHBOARD_POLL_INTERVAL = 15_000`. Merge: `mergeUnifiedSourceData` (:332), position = newest record with both lat+lng.
46+
- **History/trajectory:** `GET /api/analysis/positions` (cross-source, cursor-paginated, `since` window) via `analysisApi.fetchPositionsPage` + `useMapAnalysisData.usePositions`.
47+
- **No backend change expected** — all data endpoints already exist. This is a frontend-only epic unless a phase surfaces a real gap.
48+
49+
## Node identity note
50+
51+
Selected-node set must key on the same identity the merge uses:
52+
`mt:<nodeNum>` for Meshtastic, `mc:<publicKey>` for MeshCore (see
53+
`mergeUnifiedSourceData`). Do NOT assume a bare `nodeNum` is unique across
54+
protocols.
55+
56+
---
57+
58+
## Phases
59+
60+
### Phase 1 — Node selection & emphasis ✅ (browser-validated; PR pending)
61+
Add an explicit node multi-select to the Analysis toolbar (scoped to the chosen
62+
sources), stored as `selectedNodeIds` in `MapAnalysisConfig` (persisted).
63+
`NodeMarkersLayer` + `PositionTrailsLayer` dim unselected nodes when the
64+
selection is non-empty; selected nodes stay full-emphasis. Empty selection =
65+
unchanged behavior.
66+
67+
**Exit criteria:**
68+
- A node multi-select control in the toolbar lists nodes from the currently
69+
selected sources (respects existing source/type/search filters), with
70+
select-all / clear.
71+
- Selecting nodes dims unselected markers **and** trails; deselecting restores.
72+
- `selectedNodeIds` persists across reload (localStorage config).
73+
- Keyed on `mt:`/`mc:` identity; MeshCore + Meshtastic both selectable.
74+
- New/extended Vitest coverage green; typecheck clean; browser-validated.
75+
- Ships as a standalone, useful feature ("pick and highlight nodes").
76+
77+
### Phase 2 — Follow & Auto-zoom ⬜
78+
Add "Follow" and "Auto-zoom" toggles to the toolbar. New `useMap()` view
79+
controller(s) that, on each 15s position update, compute the selected nodes'
80+
current positions and:
81+
- **Follow:** `map.setView(averageCenter, currentZoom)`.
82+
- **Auto-zoom:** `map.fitBounds(L.latLngBounds(points).pad(0.15))`.
83+
- **Both on:** auto-zoom governs center+zoom (fitBounds implies its own center);
84+
Follow is a no-op while Auto-zoom is on.
85+
- **Manual pan/zoom** while Follow active → pause; show "Resume follow".
86+
- Toggles persist in `MapAnalysisConfig`.
87+
88+
**Exit criteria:**
89+
- Toggles present, persisted, independently operable.
90+
- Follow recenters to average center on update, keeps zoom.
91+
- Auto-zoom fits selected nodes' current positions + 15% margin on update.
92+
- Manual pan pauses Follow with a working Resume affordance.
93+
- Empty selection or single node handled sanely (no NaN bounds, single-point =
94+
center at current zoom).
95+
- Rate-limited so rapid multi-node updates don't cause jitter.
96+
- New/extended Vitest coverage green; typecheck clean; browser-validated.
97+
98+
---
99+
100+
## Status log
101+
102+
- 2026-07-08 — Stage 0 complete: issue read, code surveyed (2 Explore agents),
103+
interview done, plan written. Next: Phase 1 Stage 1 (worktree).
104+
- 2026-07-08 — Phase 1 implemented across WP-A/B+C/D (identity helper +
105+
`selectedNodeIds` config; shared `useAnalysisNodes` hook + `NodeMultiSelect`
106+
picker + marker dimming; trail dimming). Full Vitest suite green after a
107+
side-hotfix (PR #4008) repaired unrelated reboot-deps tests broken by the
108+
#4004 unified-registry refactor on main. Browser-validated on the deployed
109+
build: picker renders in the toolbar; selecting 2 nodes drove
110+
`selectedNodeIds:["mt:4134514556","mt:3274688221"]`, dimmed 1528 markers to
111+
0.3 while keeping selected at 1.0, and the selection + dimming persisted
112+
across reload (pill "2 selected"). No Phase-1 console errors.
113+
Deviation from plan: interview chose **dim-unselected (keep visible)** over
114+
hide-unselected, so Phase 1 emphasizes rather than filters. Next: ship
115+
Phase 1 PR after #4008 merges + rebase; then Phase 2 (Follow & Auto-zoom).

0 commit comments

Comments
 (0)