Directions
- {#if loadError}
-
Routing unavailable: {loadError}
- {:else if !graph}
-
{loadingData ? 'Loading road network…' : 'No network source. Load a dump layer to enable directions.'}
+
+
A · {origin.label || `${origin.lat.toFixed(4)}, ${origin.lon.toFixed(4)}`}
+
+ {#if !hasGraph}
+
+
Road data layer needed for route computation
+
The {graphLabel} isn't loaded yet. Load it to enable routing.
+
+
{/if}
- {#if graph}
- {#if from}
-
A · {from[0].toFixed(4)}, {from[1].toFixed(4)}
+ {#if hasGraph}
+
+ {#if destination}
+
B · {destination.label || `${destination.lat.toFixed(4)}, ${destination.lon.toFixed(4)}`}
+
{:else}
-
- {/if}
-
- {#if to}
-
B · {to[0].toFixed(4)}, {to[1].toFixed(4)}
- {:else}
-
- {/if}
-
-
- Avoid
- {#each CATEGORIES as c (c.key)}
-
- {/each}
-
-
-
-
-
-
+
+
+ {#if destHint}
{destHint}
{/if}
+
+
{ showDestResults = true; void inputDestSearch(); }}
+ onfocus={() => (showDestResults = true)}
+ />
+ {#if showDestResults && (destSearchResults.length || destGeoResults.length || destSearching)}
+
+ {#if destSearchResults.length}
+ - From your data
+ {#each destSearchResults as r (r.item.id)}
+
+ {/each}
+ {/if}
+ {#if destSearching}
+ - Searching the web for locations…
+ {:else if destGeoResults.length}
+ - Online
+ {#each destGeoResults as g, i (g.osmId + '-' + i)}
+
+ {/each}
+ {/if}
+
+ {/if}
+
+ {/if}
+
+
+
+
+ {#if optionsOpen}
+
+
+ Avoid
+ {#each CATEGORIES as c (c.key)}
+
+ {/each}
+
+
+
+
+
+
+ {/if}
- {#if from && to}
- {#if computing}
-
-
Computing route… {Math.round(progress * 100)}%
-
-
- {:else}
-
- {/if}
+
+
+
+ {#if computing}
+
+
Computing route… {Math.round(progress * 100)}%
+
+
{/if}
- {#if result}
- {#if result.found}
-
-
- Distance
- {formatDistance(result.distanceM)}
-
- {#if formatAlt(result.distanceM)}
{formatAlt(result.distanceM)}
{/if}
-
- Est. time
- {formatTime(result.distanceM)}
-
-
{mode === 'drive' ? 'driving' : 'walking'} · {result.nodesVisited.toLocaleString()} nodes searched
- {#if result.reason}
{result.reason}
{/if}
-
- {:else if result.partial}
-
- No complete route. {result.reason ?? 'Showing the closest reachable point on the network.'}
-
- {:else}
-
No route found. {result.reason ?? 'The route may be disconnected or blocked.'}
- {/if}
- {/if}
+ {#if status === 'error'}
+
No complete route. {errorMsg}
+ {:else if result}
+
+
Distance{formatDistance(result.distanceM)}
+
{formatAlt(result.distanceM)}
+
Est. time{formatTime(result.distanceM)}
+
{mode === 'drive' ? 'driving' : 'walking'} · {result.nodesVisited.toLocaleString()} nodes searched
+ {#if result.reason}
{result.reason}
{/if}
+
- {#if turnsShown}
-
-
- Turn-by-turn
-
-
- {#if showTurns}
+ {#if turns}
+
+
Turn-by-turn
- {#each turnsShown.list as t (t.bearing + '-' + t.distanceM + '-' + t.instruction)}
- -
- {formatDistance(t.distanceM)}
- {t.instruction}
-
+ {#each turns.list as t, i (i + '-' + t.instruction)}
+ - {formatDistance(t.distanceM)}{t.instruction}
{/each}
-
{turnsShown.list.length} steps · {formatDistance(turnsShown.total)} total
- {/if}
-
- {/if}
+
{turns.list.length} steps · {formatDistance(turns.total)} total
+
+ {/if}
- {#if result && (result.found || result.partial)}
{/if}
-
-
Set A & B by clicking the map, pick categories to avoid, then compute.
{/if}
+
+
Pick a destination (map click or search), then calculate the route.
+
\ No newline at end of file
diff --git a/src/lib/components/MapView.svelte b/src/lib/components/MapView.svelte
index efb661c..e7198d0 100644
--- a/src/lib/components/MapView.svelte
+++ b/src/lib/components/MapView.svelte
@@ -6,6 +6,7 @@
import { geocode, type GeocodedPlace } from '$lib/geocode';
import DirectionsPanel from '$lib/components/DirectionsPanel.svelte';
import { prefs } from '$lib/prefs';
+ import { route, type RouteGraph, type AvoidRule, type RouteResult } from '$lib/routing';
type FeatureCollection = {
type: string;
@@ -64,6 +65,12 @@
// --- preferences dialog state ---
let showPrefs = $state(false);
let computingRoute = $state(false);
+ // --- route graph + POIs loaded from a dump layer (for directions) ---
+ let routeGraph = $state
(null);
+ let routePois = $state([]);
+ let routeLoading = $state(false);
+ let routeProgress = $state(0);
+ let graphLabel = $derived(dumpLayers[0]?.label ?? '');
// --- search state ---
const searchables: Searchable[] = [];
let query = $state('');
@@ -149,6 +156,45 @@
}
}
+ // Load the routable graph + POIs from the first dump layer on demand.
+ async function loadRouteGraph(fresh = false) {
+ if ((routeGraph && !fresh) || routeLoading) return;
+ const layer = dumpLayers[0];
+ if (!layer || !layer.graphPath || !layer.poiPath) { routeGraph = null; return; }
+ routeLoading = true;
+ try {
+ const [g, p] = await Promise.all([fetch(layer.graphPath), fetch(layer.poiPath)]);
+ if (!g.ok) throw new Error(`graph HTTP ${g.status}`);
+ if (!p.ok) throw new Error(`poi HTTP ${p.status}`);
+ routeGraph = await g.json();
+ const pc: { features: GeoJSON.Feature[] } = await p.json();
+ routePois = pc.features;
+ } catch (e) {
+ routeGraph = null;
+ console.warn('route graph load failed:', e);
+ } finally {
+ routeLoading = false;
+ }
+ }
+
+ // Run a route. Task 4 replaces this with a Web Worker call; for now a
+ // synchronous in-thread computation keeps the panel functional.
+ async function runRoute(
+ from: [number, number],
+ to: [number, number],
+ avoid: AvoidRule[]
+ ): Promise {
+ try {
+ if (!routeGraph) await loadRouteGraph();
+ if (!routeGraph) throw new Error('Road data layer is not available.');
+ computingRoute = true;
+ routeProgress = 1; // sync: no live progress in fallback
+ return route(routeGraph, from, to, routePois, avoid);
+ } finally {
+ computingRoute = false;
+ }
+ }
+
function toggle(name: string) {
const g = layerGroups[name];
if (!g) return;
@@ -402,9 +448,19 @@
{/each}
- {#if showDirections && dumpLayers.length}
+ {#if showDirections && origin}
-
+ loadRouteGraph(true)}
+ searchables={searchables}
+ progress={routeProgress}
+ computing={computingRoute}
+ {runRoute}
+ />
{/if}