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.
This commit is contained in:
2026-05-18 12:15:13 +02:00
parent ce1fa4972c
commit 44fb492759
4 changed files with 54 additions and 25 deletions
+41 -21
View File
@@ -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<Station[]> {
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<Station | null> {
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<Station | null> {
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<WienerLinienDeparture[]> {