Files
time_to_leave/src/lib/hafas-client.ts
T
2026-05-09 22:47:17 +02:00

127 lines
3.6 KiB
TypeScript

import type { Journey, Station } from "@/types";
import { HAFAS_URL, HAFAS_TIMEOUT_MS } from "./constants";
import { parseHafasTime, hafasDateTime } from "./hafas-time";
interface HafasLocation {
type: "S" | "A" | "P";
name: string;
extId: string;
}
interface HafasJourney {
ctxRecon?: string;
secL?: Array<{
dep?: { dTimeS?: string; dTimeR?: string; dPlatfS?: string };
arr?: { aTimeS?: string; aTimeR?: string };
jny?: {
prodX?: number;
stopL?: Array<{ name: string }>;
dlySum?: number;
isCncl?: boolean;
};
chgDurR?: number;
}>;
}
export class HafasClient {
private baseUrl: string;
private timeoutMs: number;
constructor(baseUrl: string = HAFAS_URL, timeoutMs: number = HAFAS_TIMEOUT_MS) {
this.baseUrl = baseUrl;
this.timeoutMs = timeoutMs;
}
async searchStation(query: string): Promise<Station[]> {
const body = {
svcReqL: [
{
meth: "LocMatch",
req: { input: { loc: { name: query, type: "S" }, maxLoc: 5 } },
},
],
};
const res = await fetch(this.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(this.timeoutMs),
});
if (!res.ok) throw new Error(`HAFAS station search failed: ${res.status}`);
const json = await res.json();
const match = json?.svcResL?.[0]?.res?.match?.locL ?? [];
return (match as HafasLocation[]).filter((l) => l.type === "S").map((l) => ({ name: l.name, extId: l.extId }));
}
async fetchJourneys(from: Station, to: Station, date: Date): Promise<Journey[]> {
const { date: hafasDate, time: hafasTime } = hafasDateTime(date);
const body = {
svcReqL: [
{
meth: "TripSearch",
req: {
depLocL: [{ type: "S", extId: from.extId }],
arrLocL: [{ type: "S", extId: to.extId }],
outDate: hafasDate,
outTime: hafasTime,
numF: 5,
},
},
],
};
const res = await fetch(this.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(this.timeoutMs),
});
if (!res.ok) throw new Error(`HAFAS trip search failed: ${res.status}`);
const json = await res.json();
const outConL: HafasJourney[] = json?.svcResL?.[0]?.res?.outConL ?? [];
return outConL.map((con, i): Journey => {
const first = con.secL?.[0];
const last = con.secL?.[con.secL.length - 1];
const dep = first?.dep;
const arr = last?.arr;
const sD = dep?.dTimeS ? parseHafasTime(hafasDate, dep.dTimeS) : date;
const rD = dep?.dTimeR ? parseHafasTime(hafasDate, dep.dTimeR) : sD;
const sA = arr?.aTimeS ? parseHafasTime(hafasDate, arr.aTimeS) : sD;
const rA = arr?.aTimeR ? parseHafasTime(hafasDate, arr.aTimeR) : sA;
const delayMs = rD.getTime() - sD.getTime();
const delay = Math.max(0, Math.round(delayMs / 60000));
const trains = (con.secL ?? [])
.filter((s) => s.jny)
.map((s) => s.jny?.stopL?.[0]?.name ?? "")
.filter(Boolean);
const cancelled = (con.secL ?? []).some((s) => s.jny?.isCncl === true);
const changes = Math.max(0, (con.secL ?? []).filter((s) => s.jny).length - 1);
const platform = first?.dep?.dPlatfS ?? "";
return {
id: con.ctxRecon ?? `journey-${i}`,
sD,
rD,
sA,
rA,
delay,
platform,
changes,
trains,
cancelled,
};
});
}
}