feat(directions): run route computation in a Web Worker with live progress (off UI thread)

This commit is contained in:
hermes-explorigin 2026-08-10 15:22:01 +00:00
parent 62ddbf160c
commit 591fafc5de

View File

@ -1,12 +1,12 @@
<script lang="ts">
import 'leaflet/dist/leaflet.css';
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import type { Map as LeafletMap } from 'leaflet';
import { search, type Searchable, type SearchResult } from '$lib/search';
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';
import type { RouteGraph, AvoidRule, RouteResult } from '$lib/routing';
type FeatureCollection = {
type: string;
@ -71,6 +71,11 @@
let routeLoading = $state(false);
let routeProgress = $state(0);
let graphLabel = $derived(dumpLayers[0]?.label ?? '');
// --- Web Worker for route computation (off the UI thread) ---
let routeWorker: Worker | null = null;
let routeToken = 0;
let pendingResolve: ((r: RouteResult) => void) | null = null;
let pendingReject: ((e: Error) => void) | null = null;
// --- search state ---
const searchables: Searchable[] = [];
let query = $state('');
@ -177,24 +182,63 @@
}
}
// Run a route. Task 4 replaces this with a Web Worker call; for now a
// synchronous in-thread computation keeps the panel functional.
// Lazily create the routing worker. It is recreated on demand and kept for
// the component lifetime (destroyed on unmount).
function ensureWorker(): Worker {
if (routeWorker) return routeWorker;
const w = new Worker(new URL('../routing.worker.ts', import.meta.url), { type: 'module' });
w.onmessage = (e: MessageEvent) => {
const msg = e.data as { type: string; pct?: number; result?: RouteResult; error?: string; message?: string; token?: number };
if (msg.type === 'progress' && msg.pct !== undefined) {
routeProgress = msg.pct;
return;
}
if (msg.type === 'done' && pendingResolve) {
const r = pendingResolve; pendingResolve = null; pendingReject = null;
computingRoute = false;
r(msg.result as RouteResult);
} else if (msg.type === 'error' && pendingReject) {
const rj = pendingReject; pendingResolve = null; pendingReject = null;
computingRoute = false;
rj(new Error((msg as { message?: string }).message ?? 'Route computation failed.'));
}
};
routeWorker = w;
return w;
}
function destroyWorker() {
if (routeWorker) { routeWorker.terminate(); routeWorker = null; }
}
// Run a route in the Web Worker, reporting progress back to the panel.
async function runRoute(
from: [number, number],
to: [number, number],
avoid: AvoidRule[]
): Promise<RouteResult> {
try {
if (!routeGraph) await loadRouteGraph();
if (!routeGraph) throw new Error('Road data layer is not available.');
const w = ensureWorker();
computingRoute = true;
routeProgress = 1; // sync: no live progress in fallback
return route(routeGraph, from, to, routePois, avoid);
} finally {
computingRoute = false;
}
routeProgress = 0;
const token = ++routeToken;
return await new Promise<RouteResult>((resolve, reject) => {
pendingResolve = resolve;
pendingReject = reject;
w.postMessage({
type: 'route',
token,
graph: routeGraph,
pois: routePois,
from,
to,
avoid
});
});
}
onDestroy(destroyWorker);
function toggle(name: string) {
const g = layerGroups[name];
if (!g) return;