12 KiB
Directions Rework — Implementation Plan
For Hermes: Planning only. Do not implement yet.
Goal: Rebuild the Navigator Directions feature around a clear user flow: pick an origin (point or POI) → open Directions → pick a destination (map click or POI via search) → compute the route → show turn-by-turn directions. Route calculation runs in a Web Worker so the UI never blocks, with explicit running/finished/errored feedback. Adds a browser-local Saved Points layer and on-demand layer loading.
Architecture: A single DirectionsFlow state machine lives in MapView.svelte. The origin is "whatever point/POI the user most recently selected" (search result, direct map click on a POI, or a click on empty space that creates a Saved Point). The DirectionsPanel becomes a destination-picker + options + results area. All route math stays in the pure routing.ts and is invoked from a dedicated Web Worker (routing.worker.ts), which posts progress + final results back to the UI.
Tech Stack: Svelte 5 (runes), SvelteKit static adapter, Leaflet, Vite (native worker support), routing.ts (pure, unchanged math), prefs.ts (unit system), localStorage (Saved Points).
Current state
routing.tsis already pure (no Leaflet, no DOM) —route,routeWithProgress,buildTurnInstructions,buildEdgePenaltiesare all Web-Worker-safe as-is.- Today the user sets A/B by clicking inside the panel, and
routeWithProgressyields every ~4000 nodes. - Search (
search.ts+geocode.ts) already gives{lat, lon}+boundsfor local POIs and online results. Reuse for destination selection. - Graph + POIs are loaded lazily from the first dump layer's
graphPath/poiPath. - Unit system now lives only in
prefs. "Avoid categories" currently sits inline in the panel.
Confirmed product decisions (from discussion)
- Origin: whatever point/POI the user most recently selected — search result, direct map click (POI or empty space), or a Saved Point.
- Directions button is enabled only when an origin is selected.
- Clicking Directions opens a destination-picker dialog:
- pick destination by map click, or
- search + select a POI (local or online geocode), or
- select an existing Saved Point.
- "Calculate route" enables only once both points are set.
- On calculate, the dialog switches form to show turn-by-turn instructions.
- Avoid categories stays but moves to a secondary "Route options" collapsible section.
- Web Worker: route computation runs off the main thread, with visible running / finished / errored status.
- Saved Points layer (new, browser-local): clicking a non-POI spot creates a Saved Point stored in
localStorage. A dedicated "Saved Points" layer appears in the legend and toggles on/off. The Preferences dialog gains Export / Import for this layer. Saved points are valid origins/destinations. - On-demand layer loading: if the routable graph (dump layer) isn't loaded, the app prompts the user to load it with a clear call-to-action, then enables routing.
Non-goals (YAGNI)
- No live re-routing while dragging.
- No server-side routing / turn lane guidance / spoken directions.
- No persistence of last route.
- Saved Points live only in the browser (no cloud sync).
Proposed approach
Single-page state machine in MapView.svelte:
src/lib/routing.ts // pure logic (unchanged math)
src/lib/routing.worker.ts // NEW: {graph,pois,from,to,avoid} -> posts {progress|result|error}
src/lib/savedPoints.ts // NEW: localStorage-backed CRUD + export/import
src/lib/components/DirectionsPanel.svelte // REWORK destination-picker + options + results + status
src/lib/components/MapView.svelte // origin selection, Saved Points layer, worker lifecycle
src/routes/+page.svelte // host worker / pass origin (likely minimal)
Data flow:
MapViewowns:origin: { lat, lon, label? } | null— set byonSelect, a POI/empty-space map click, or a Saved Point pick (most recent wins).worker: Worker | nullfor the routing graph's lifetime.savedPointslayer state (fromsavedPoints.tsstore).
DirectionsPanel(opened only whenoriginexists) owns:destination(map click, POI search, or Saved Point).avoidselection (collapsed Route options).- "Calculate route" button enabled when
originanddestinationare set. status: 'idle' | 'running' | 'done' | 'error'plus an on-demand layer CTA when the graph isn't loaded.
Web Worker design:
- Entry
routing.worker.tsreceives aRouteJob; it callsbuildEdgePenaltiesonce, then runs a modifiedroute()that periodicallypostMessage({type:'progress', pct})(yielding inside the worker; UI never blocks). - Sends
{type:'done', result}or{type:'error', message}. - Worker created when the graph loads; jobs serialized (one at a time); a stale run token lets us ignore an obsolete in-flight job.
Step-by-step plan (bite-sized, TDD where practical)
Task 1 — Add a Web Worker entry for routing
Files:
- Create:
src/lib/routing.worker.ts
Objective: A worker that runs route() off-thread and posts progress/results/errors.
Step 1: Add worker with a self-describing message protocol (run job, post progress/done/error).
Step 2: Wire Vite worker import: new Worker(new URL('./routing.worker.ts', import.meta.url), { type: 'module' }).
Step 3: Verify the worker runs (log a dummy job).
Task 2 — Origin selection in MapView
Files:
- Modify:
src/lib/components/MapView.svelte
Objective: Any search selection or map-click sets origin (most recent wins) and shows a candidate pin.
Step 1: Add let origin = $state<{lat:number; lon:number; label?:string}|null>(null).
Step 2: In onSelect(r) set origin = { lat, lon, label }.
Step 3: Add a leaflet click handler (when not picking destination): on empty space → create a Saved Point (see Task 5b) and set origin; on an existing POI → set origin.
Step 4: Enable the Directions button only when origin != null.
Task 3 — DirectionsPanel → destination picker + options + status
Files:
- Rewrite:
src/lib/components/DirectionsPanel.svelte
Objective: Rebuild the dialog: show origin, pick destination (map click, POI search, or Saved Point), hold avoid categories in a collapsed "Route options", gate "Calculate route" on both points.
Step 1: Props: origin, setPickingDestination(), onDestinationSelected, loaded graph/pois, saved points list.
Step 2: Destination picker: map-click mode + search dropdown (reuse search + geocode) + Saved Point list.
Step 3: Collapsed "Route options" with the 6 avoid checkboxes.
Step 4: "Calculate route" button disabled unless both points are set.
Task 4 — Run route in the worker with status feedback
Files:
- Modify:
src/routes/+page.svelte(host worker) - Modify:
src/lib/components/DirectionsPanel.svelte
Objective: Post job to worker; show live progress %, then turn-by-turn, or an error.
Step 1: Lift worker lifecycle into MapView (create after graph loads; destroy on unmount).
Step 2: calculate() posts {graph, pois, from, to, avoid} to the worker.
Step 3: Map worker messages → status: running (pct) → done (turn list + drawn route) / error (message).
Step 4: Visible status line: "Calculating route… 42%", "Route ready", or error text.
Task 5 — Wire state machine end-to-end
Files:
- Modify:
MapView.svelte,DirectionsPanel.svelte,src/routes/+page.svelte
Objective: Full flow works: select origin → Directions enabled → pick destination → Calculate → turn-by-turn.
Step 1: CSS for origin pin, destination pin, active route polyline (reuse drawResult).
Step 2: "Clear"/close resets worker + pins.
Task 5b — Browser-local "Saved Points" layer
Files:
- Create:
src/lib/savedPoints.ts(store: read/writelocalStorage, CRUD, export/import helpers) - Modify:
src/lib/components/MapView.svelte(click-on-empty → create Saved Point; register layer) - Modify:
src/lib/components/DirectionsPanel.svelte(Saved Points selectable as origin/destination)
Objective: Clicking a non-POI spot creates a persistent, browser-local Saved Point; a toggleable "Saved Points" layer shows them.
Step 1: savedPoints.ts store backed by localStorage (key navigator.savedPoints.v1); {points: Array<{id, lat, lon, label?}>}.
Step 2: In MapView, when a map click lands on empty space: prompt for an optional label, then add(point) and set it as origin.
Step 3: Register a "Saved Points" layer in the legend/layers list (toggled independently; distinct marker color).
Step 4: Saved Points are valid origin and destination candidates.
Task 5c — Export / Import of Saved Points (in Preferences dialog)
Files:
- Modify:
src/lib/components/MapView.svelte(Preferences dialog) - Modify:
src/lib/savedPoints.ts(export/import helpers)
Objective: Users can back up and restore their Saved Points.
Step 1: exportJson() serializes the store → browser download (Blob + <a download>).
Step 2: importJson(file) validates the shape, merges into the store (dedupe by id, keep existing ids), re-renders the layer.
Step 3: Add "Export Saved Points" and "Import Saved Points" buttons to the Preferences dialog.
Task 6 — On-demand layer loading (prompt)
Files:
- Modify:
src/lib/components/DirectionsPanel.svelte,src/lib/components/MapView.svelte
Objective: If the routable graph (dump layer) isn't loaded, prompt the user instead of silently disabling.
Step 1: Detect whether a dump layer with graphPath/poiPath is present in the loaded manifest. If not, the Directions panel shows a generic CTA: "Road data layer needed for route computation" + a button (no hardcoded location name — scale generically to whatever dump source is configured).
Step 2: The button loads the configured dump layer on demand (fetch graph+pois, register layer, enable Directions).
Step 3: Only after the graph is available does "Calculate route" become possible.
Task 7 — Docs & commit
Files:
- Modify:
README.md
Objective: Document the Directions flow, Web Worker routing, Saved Points layer, on-demand loading, options placement.
Step 1: Update README.md Directions section + add Saved Points docs.
Step 2: Commit after each green task; final commit closes the feature.
Files likely to change
| File | Change |
|---|---|
src/lib/routing.worker.ts |
New — worker entry |
src/lib/savedPoints.ts |
New — localStorage store + CRUD + export/import |
src/lib/routing.ts |
minor: progress-callback route used by worker (or reuse routeWithProgress inside worker) |
src/lib/components/DirectionsPanel.svelte |
Rewrite (destination picker, options, status, CTA) |
src/lib/components/MapView.svelte |
Origin selection, Saved Points layer, worker lifecycle, Preferences export/import |
src/routes/+page.svelte |
Host worker / pass origin |
scripts/test.mjs |
Add routing + savedPoints regression checks |
README.md |
Document flow + worker + Saved Points |
Tests / validation
npm run check→ 0 errors, 0 warningsnpm test→ all pass (add routing + savedPoints edge cases)npm run build:static→ bundlesrouting.worker.jsandsavedPoints.ts- Manual: select POI via search → Directions enabled → pick destination by map click → Calculate → live progress → turn-by-turn; click empty space → Saved Point created and toggled; Export/Import round-trips in Preferences; missing-dump layer shows CTA.
Risks, tradeoffs & open questions
- Web Worker + static adapter: Vite bundles workers fine; verify the worker chunk emits to
build/and resolves in the prerendered static site. - Saved Points persistence: only
localStorage— erased if site data is cleared; mitigated by Export/Import. - Graph load timing: handled by the on-demand prompt; loading a large graph may take a moment (show a loading state on the CTA).
- Avoid categories in a collapsed section — slightly more clicks; acceptable for a cleaner primary flow.
- Reimport strategy: confirmed merge — merge imported points into the existing store, dedupe by id, and skip ids already present. (No replace/merge toggle.)