# 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.ts` is already **pure** (no Leaflet, no DOM) — `route`, `routeWithProgress`, `buildTurnInstructions`, `buildEdgePenalties` are all Web-Worker-safe as-is. - Today the user sets **A/B by clicking** inside the panel, and `routeWithProgress` yields every ~4000 nodes. - Search (`search.ts` + `geocode.ts`) already gives `{lat, lon}` + `bounds` for 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) 1. **Origin**: whatever point/POI the user most recently selected — search result, direct map click (POI or empty space), or a Saved Point. 2. **Directions button** is enabled **only when** an origin is selected. 3. 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**. 4. **"Calculate route"** enables only once both points are set. 5. On calculate, the dialog switches form to show **turn-by-turn** instructions. 6. **Avoid categories** stays but moves to a secondary **"Route options"** collapsible section. 7. **Web Worker**: route computation runs off the main thread, with visible **running / finished / errored** status. 8. **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. 9. **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: - `MapView` owns: - `origin: { lat, lon, label? } | null` — set by `onSelect`, a POI/empty-space map click, or a Saved Point pick (most recent wins). - `worker: Worker | null` for the routing graph's lifetime. - `savedPoints` layer state (from `savedPoints.ts` store). - `DirectionsPanel` (opened only when `origin` exists) owns: - `destination` (map click, POI search, or Saved Point). - `avoid` selection (collapsed **Route options**). - **"Calculate route"** button enabled when `origin` and `destination` are set. - `status: 'idle' | 'running' | 'done' | 'error'` plus an on-demand layer CTA when the graph isn't loaded. Web Worker design: - Entry `routing.worker.ts` receives a `RouteJob`; it calls `buildEdgePenalties` once, then runs a modified `route()` that periodically `postMessage({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/write `localStorage`, 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` + ``). **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 warnings - `npm test` → all pass (add routing + savedPoints edge cases) - `npm run build:static` → bundles `routing.worker.js` and `savedPoints.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.)