Files
time_to_leave/apps/web/src/lib/walk-routing-client.ts
T
fegger 1851d2ed47 Add TimeToLeave feature checklist and plan (50)
Add TimeToLeave feature checklist and plan
2026-05-12 13:17:17 +02:00

107 lines
3.0 KiB
TypeScript

import type { BikeRoute, BikeStep } from "@timetoleave/core";
import { OSRM_URL } from "./constants";
import { ApiClient } from "./api-service";
// ---------------------------------------------------------------------------
// OSRM response types (internal, not exported)
// ---------------------------------------------------------------------------
interface OsrmStep {
name: string;
distance: number;
duration: number;
maneuver: { instruction?: string; type: string; modifier?: string };
}
interface OsrmRoute {
distance: number;
duration: number;
legs: Array<{ steps: OsrmStep[] }>;
}
interface OsrmResponse {
code: string;
routes: OsrmRoute[];
}
// ---------------------------------------------------------------------------
// Helper: Build human-readable instruction from OSRM step data
// ---------------------------------------------------------------------------
function stepInstruction(step: OsrmStep): string {
if (step.maneuver.instruction) return step.maneuver.instruction;
const modifier = step.maneuver.modifier ? ` ${step.maneuver.modifier}` : "";
return `${step.maneuver.type}${modifier}${step.name ? ` onto ${step.name}` : ""}`;
}
// ---------------------------------------------------------------------------
// WalkRoutingClient — Wraps the OSRM foot routing API
//
// OSRM routes are cached for 5 minutes. Walk routes between the same
// coordinates rarely change, and caching significantly reduces API load.
// ---------------------------------------------------------------------------
export class WalkRoutingClient {
private client: ApiClient;
private readonly defaultTtlMs: number;
constructor(
baseUrl: string = OSRM_URL,
ttlMs: number = 5 * 60 * 1000, // 5 minutes — routes are very stable
) {
this.client = new ApiClient({
baseUrl,
defaultTimeoutMs: 10_000,
defaultTtlMs: ttlMs,
maxRetries: 3,
});
this.defaultTtlMs = ttlMs;
}
async getWalkRoute(fromLat: number, fromLng: number, toLat: number, toLng: number): Promise<BikeRoute | null> {
const coords = `${fromLng},${fromLat};${toLng},${toLat}`;
const path = `/route/v1/foot/${coords}`;
const cacheKey = `osrm:walk:${coords}`;
const res = await this.client.get<OsrmResponse>(
path,
{ overview: "false", steps: "true" },
{
cacheKey,
ttl: this.defaultTtlMs,
},
);
if (res.code !== "Ok" || !res.routes.length) return null;
const route = res.routes[0];
const steps: BikeStep[] = (route.legs[0]?.steps ?? []).map((s) => ({
name: s.name,
distance: s.distance,
duration: s.duration,
instruction: stepInstruction(s),
}));
return {
distance: route.distance,
duration: route.duration,
steps,
};
}
/**
* Expose cache statistics for debugging/monitoring.
*/
public cacheStats() {
return this.client.cacheStats();
}
/**
* Clear the cache (useful for testing or forced refresh).
*/
public clearCache() {
this.client.clearCache();
}
}