Files
time_to_leave/apps/web/src/lib/walk-routing-client.ts
T
fegger 44fb492759 Use extId station lookup and clamp walk durations
Add ApiClient.findStationByExtId and toStation helper; use it in
useOriginStationWalk. Clamp OSRM route/step durations to a minimum
walking speed (1.25 m/s) and update tests to mock the new API call.
2026-05-18 12:15:13 +02:00

110 lines
3.2 KiB
TypeScript

import type { WalkRoute, WalkStep } from "@timetoleave/core";
import { OSRM_URL } from "./constants";
import { ApiClient } from "./api-service";
const WALKING_METERS_PER_SECOND = 1.25;
// ---------------------------------------------------------------------------
// 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<WalkRoute | 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 walkingDuration = Math.ceil(route.distance / WALKING_METERS_PER_SECOND);
const steps: WalkStep[] = (route.legs[0]?.steps ?? []).map((s) => ({
name: s.name,
distance: s.distance,
duration: Math.max(s.duration, Math.ceil(s.distance / WALKING_METERS_PER_SECOND)),
instruction: stepInstruction(s),
}));
return {
distance: route.distance,
duration: Math.max(route.duration, walkingDuration),
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();
}
}