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 { 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 { 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, }; }); } }