From 44fb492759a24228176197fe1de69669f926d1a2 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Mon, 18 May 2026 12:15:13 +0200 Subject: [PATCH] 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. --- apps/mobile/src/__tests__/screens.test.tsx | 6 ++ apps/mobile/src/hooks/useOriginStationWalk.ts | 4 +- apps/web/src/lib/walk-routing-client.ts | 7 ++- packages/api-client/src/client.ts | 62 ++++++++++++------- 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/apps/mobile/src/__tests__/screens.test.tsx b/apps/mobile/src/__tests__/screens.test.tsx index f0fa4f4..f8dcdc3 100644 --- a/apps/mobile/src/__tests__/screens.test.tsx +++ b/apps/mobile/src/__tests__/screens.test.tsx @@ -69,6 +69,12 @@ jest.mock('../hooks/useOriginStationWalk', () => ({ jest.mock('../services/api', () => ({ api: { + findStationByExtId: jest.fn().mockResolvedValue({ + name: 'Mödling Bahnhof', + extId: '1231701', + lat: 48.085, + lng: 16.296, + }), searchJourneys: jest.fn().mockResolvedValue([ { id: 'journey-1', diff --git a/apps/mobile/src/hooks/useOriginStationWalk.ts b/apps/mobile/src/hooks/useOriginStationWalk.ts index 7be87d8..605bed7 100644 --- a/apps/mobile/src/hooks/useOriginStationWalk.ts +++ b/apps/mobile/src/hooks/useOriginStationWalk.ts @@ -24,9 +24,9 @@ export function useOriginStationWalk(origin: Station | null) { if (origin?.lat == null || origin.lng == null) return; try { - const nearest = await api.findNearestStationByCoords(origin.lat, origin.lng); + const selectedStation = await api.findStationByExtId(origin.extId); if (!isMounted) return; - setStation(nearest); + setStation(selectedStation); } catch (err) { if (!isMounted) return; setLookupError(err instanceof Error ? err.message : 'Origin station lookup failed'); diff --git a/apps/web/src/lib/walk-routing-client.ts b/apps/web/src/lib/walk-routing-client.ts index e68f74d..22c2b2a 100644 --- a/apps/web/src/lib/walk-routing-client.ts +++ b/apps/web/src/lib/walk-routing-client.ts @@ -2,6 +2,8 @@ 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) // --------------------------------------------------------------------------- @@ -76,16 +78,17 @@ export class WalkRoutingClient { 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: s.duration, + duration: Math.max(s.duration, Math.ceil(s.distance / WALKING_METERS_PER_SECOND)), instruction: stepInstruction(s), })); return { distance: route.distance, - duration: route.duration, + duration: Math.max(route.duration, walkingDuration), steps, }; } diff --git a/packages/api-client/src/client.ts b/packages/api-client/src/client.ts index c0328fb..69170d6 100644 --- a/packages/api-client/src/client.ts +++ b/packages/api-client/src/client.ts @@ -69,6 +69,24 @@ function normalizeHafasCoordinate(value: number | undefined): number | undefined return Math.abs(value) > 1000 ? value / 1e6 : value; } +type HafasLocationWithCoords = Station & { + type: string; + lon?: number; + crd?: { + x?: number; + y?: number; + }; +}; + +function toStation(location: HafasLocationWithCoords): Station { + return { + name: location.name, + extId: location.extId, + lat: normalizeHafasCoordinate(location.lat ?? location.crd?.y), + lng: normalizeHafasCoordinate(location.lng ?? location.lon ?? location.crd?.x), + }; +} + /** * Client for the TimeToLeave server API. Proxy requests to HAFAS, OSRM, * Nominatim, and WienerLinien backends. Set `baseUrl` to your deployed app. @@ -226,7 +244,7 @@ export class ApiClient { async searchStation(query: string): Promise { const result = await this.hafasRequest<{ - svcResL?: Array<{ res?: { match?: { locL?: Array<{ type: string; name: string; extId: string }> } } }>; + svcResL?: Array<{ res?: { match?: { locL?: HafasLocationWithCoords[] } } }>; }>({ svcReqL: [ { @@ -238,7 +256,23 @@ export class ApiClient { const locL = result?.svcResL?.[0]?.res?.match?.locL ?? []; return locL .filter((l) => l.type === "S") - .map((l) => ({ name: l.name, extId: l.extId })); + .map(toStation); + } + + async findStationByExtId(extId: string): Promise { + const result = await this.hafasRequest<{ + svcResL?: Array<{ res?: { match?: { locL?: HafasLocationWithCoords[] } } }>; + }>({ + svcReqL: [ + { + meth: "LocMatch", + req: { input: { loc: { extId, type: "S" }, maxLoc: 1, field: "S" } }, + }, + ], + }); + + const station = result?.svcResL?.[0]?.res?.match?.locL?.find((location) => location.type === "S"); + return station ? toStation(station) : null; } /** @@ -247,17 +281,8 @@ export class ApiClient { * is unavailable or the base URL is empty. */ async findNearestStationByCoords(lat: number, lng: number): Promise { - interface HafasLocation extends Station { - type: string; - lon?: number; - crd?: { - x?: number; - y?: number; - }; - } - const result = await this.hafasRequest<{ - svcResL?: Array<{ res?: { match?: { locL?: HafasLocation[] } } }>; + svcResL?: Array<{ res?: { match?: { locL?: HafasLocationWithCoords[] } } }>; }>({ svcReqL: [ { @@ -279,15 +304,15 @@ export class ApiClient { ], }); - const stations: HafasLocation[] = result?.svcResL?.[0]?.res?.match?.locL ?? []; + const stations: HafasLocationWithCoords[] = result?.svcResL?.[0]?.res?.match?.locL ?? []; if (stations.length === 0) return null; // Filter to only "S" (station) type results, then pick the closest - const stationResults = stations.filter((s: HafasLocation) => s.type === "S"); + const stationResults = stations.filter((s: HafasLocationWithCoords) => s.type === "S"); if (stationResults.length === 0) return null; // Find the closest station by Euclidean distance - const closest = stationResults.reduce((best: HafasLocation, candidate: HafasLocation) => { + const closest = stationResults.reduce((best: HafasLocationWithCoords, candidate: HafasLocationWithCoords) => { const bestLat = normalizeHafasCoordinate(best.lat ?? best.crd?.y) ?? lat; const bestLng = normalizeHafasCoordinate(best.lng ?? best.lon ?? best.crd?.x) ?? lng; const candidateLat = normalizeHafasCoordinate(candidate.lat ?? candidate.crd?.y) ?? lat; @@ -297,12 +322,7 @@ export class ApiClient { return candDist < bestDist ? candidate : best; }, stationResults[0]); - return { - name: closest.name, - extId: closest.extId, - lat: normalizeHafasCoordinate(closest.lat ?? closest.crd?.y), - lng: normalizeHafasCoordinate(closest.lng ?? closest.lon ?? closest.crd?.x), - }; + return toStation(closest); } async monitorStops(stopIds: string[]): Promise {