Löst die WebApp aus der eigenen OTP2-Instanz heraus und nutzt dieselbe
HAFAS-Anbindung (VBB + Deutsche Bahn) wie die iOS-App, damit beide Clients
gegen dasselbe Backend-Feature-Set laufen:
- js/api.js: getJourneys() ruft jetzt /journeys statt /otp/journeys auf,
inkl. via0-2 (Lat/Lon/Name/WaitMinutes), modes, bike, earlierRef/laterRef.
Gibt die volle Antwort zurück statt nur { journeys }.
- js/app.js: bis zu 3 Zwischenhalte mit eigener Ortssuche und Aufenthaltszeit,
Verkehrsmittel-Filter (ModeFilter-Komponente wiederverwendet), Fahrrad-
Toggle, Früher/Später-Buttons zum Nachladen. Share-Link-Format um
Vias/Modes/Bike erweitert, alter Umstiegszeit-Puffer entfernt (von HAFAS
nicht unterstützt).
- index.html/styles.css: neue Formularelemente für Zwischenhalte, Filter
und Paginierung.
- js/i18n.js: neue Übersetzungsschlüssel in DE/EN, alten sourceNote-String
korrigiert (& wurde per textContent gesetzt und wäre literal
angezeigt worden statt als &).
- README.md: Feature-Beschreibung aktualisiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01De7H8hR1Q9DdgyuS2ukN6y
202 lines
6.9 KiB
JavaScript
202 lines
6.9 KiB
JavaScript
// Schlanker Wrapper um alle Aufrufe an das ÖPNV-Navi-Backend.
|
|
// Wirft bei Fehlern verständliche Errors, die die UI direkt anzeigen kann.
|
|
|
|
const API = (() => {
|
|
const BASE = window.APP_CONFIG.API_BASE;
|
|
|
|
async function request(path, options = {}) {
|
|
let response;
|
|
try {
|
|
response = await fetch(`${BASE}${path}`, options);
|
|
} catch (networkErr) {
|
|
throw new Error('Backend nicht erreichbar. Prüfe deine Internetverbindung.');
|
|
}
|
|
|
|
if (!response.ok) {
|
|
let message = `Fehler ${response.status}`;
|
|
try {
|
|
const body = await response.json();
|
|
if (body.error) message = body.error;
|
|
} catch (_) {
|
|
// Antwort war kein JSON, Standardmeldung behalten
|
|
}
|
|
throw new Error(message);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
function postJson(path, body) {
|
|
return request(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
function deleteJson(path, body) {
|
|
return request(path, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
// Baut einen Query-String und lässt dabei undefined/null-Werte weg.
|
|
function qs(params) {
|
|
const parts = [];
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value === undefined || value === null || value === '') return;
|
|
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
});
|
|
return parts.join('&');
|
|
}
|
|
|
|
return {
|
|
searchLocations(query) {
|
|
return request(`/locations/search?query=${encodeURIComponent(query)}`);
|
|
},
|
|
|
|
// Bundesweite DB-IRIS-Anbindung: nur "echte" DB-Bahnhöfe (Fernverkehr +
|
|
// viele Regio-Stationen), OHNE U-Bahn/Tram/Bus - siehe searchAll().
|
|
searchDbStations(query) {
|
|
return request(`/db/stations/search?query=${encodeURIComponent(query)}`);
|
|
},
|
|
|
|
/**
|
|
* Kombinierte Suche: fragt VBB (U-Bahn/S-Bahn/Tram/Bus, primär Berlin/
|
|
* Brandenburg) UND die bundesweite DB-Anbindung (nur echte Bahnhöfe)
|
|
* parallel ab und liefert eine gemeinsame, quellenmarkierte Liste zurück.
|
|
* So funktioniert Nahverkehr weiterhin lokal, und man bekommt zusätzlich
|
|
* bundesweite Fernverkehrs-/Regio-Bahnhöfe.
|
|
*/
|
|
async searchAll(query) {
|
|
const [vbbResult, dbResult] = await Promise.allSettled([
|
|
this.searchLocations(query),
|
|
this.searchDbStations(query),
|
|
]);
|
|
|
|
const vbbStations = vbbResult.status === 'fulfilled'
|
|
? vbbResult.value.locations
|
|
.filter((l) => l.kind === 'stop')
|
|
.map((l) => ({ id: l.id, name: l.name, source: 'vbb' }))
|
|
: [];
|
|
|
|
const dbStations = dbResult.status === 'fulfilled'
|
|
? dbResult.value.stations.map((s) => ({ id: s.evaNumber, name: s.name, source: 'db' }))
|
|
: [];
|
|
|
|
return [...vbbStations, ...dbStations];
|
|
},
|
|
|
|
getDbDepartures(evaNumber, { hours = 2, results = 20 } = {}) {
|
|
return request(`/db/departures/${encodeURIComponent(evaNumber)}?${qs({ hours, results })}`);
|
|
},
|
|
|
|
nearby({ lat, lon, distance, results, includePoi }) {
|
|
return request(`/locations/nearby?${qs({ lat, lon, distance, results, includePoi })}`);
|
|
},
|
|
|
|
getDepartures(stationId, { duration = 30, results = 15, modes } = {}) {
|
|
return request(`/departures/${encodeURIComponent(stationId)}?${qs({ duration, results, modes })}`);
|
|
},
|
|
|
|
/**
|
|
* Nutzt dieselbe HAFAS-Anbindung (VBB + Deutsche Bahn) wie die iOS-App.
|
|
* `from`/`to` brauchen { lat, lon, name }. `vias` ist eine Liste von bis
|
|
* zu 3 Zwischenhalten ({ lat, lon, name, waitMinutes }), in Reihenfolge
|
|
* der Route. Sobald mindestens ein Via gesetzt ist, verkettet das
|
|
* Backend mehrere Einzelabfragen - dann gelten immer "Abfahrt um" sowie
|
|
* feste results=1 pro Abschnitt, und earlierRef/laterRef liefern null
|
|
* zurück (siehe routes/journeys.js im Backend).
|
|
* `earlierRef`/`laterRef` laden, aus einer vorherigen Antwort übernommen,
|
|
* die nächste bzw. vorherige Seite an Verbindungen nach (ohne Vias).
|
|
* Gibt die volle Antwort zurück ({ journeys, earlierRef, laterRef }).
|
|
*/
|
|
getJourneys({
|
|
from, to, when, arrival, polylines, results = 5,
|
|
wheelchair, modes, bike, vias = [], earlierRef, laterRef,
|
|
}) {
|
|
const params = {
|
|
fromLat: from.lat,
|
|
fromLon: from.lon,
|
|
fromName: from.name || undefined,
|
|
toLat: to.lat,
|
|
toLon: to.lon,
|
|
toName: to.name || undefined,
|
|
results,
|
|
polylines: polylines ? 'true' : undefined,
|
|
accessibility: wheelchair ? 'true' : undefined,
|
|
modes: modes || undefined,
|
|
bike: bike ? 'true' : undefined,
|
|
};
|
|
|
|
if (earlierRef) {
|
|
params.earlierRef = earlierRef;
|
|
} else if (laterRef) {
|
|
params.laterRef = laterRef;
|
|
} else if (when) {
|
|
params.when = when;
|
|
if (arrival) params.arrival = 'true';
|
|
}
|
|
|
|
vias.slice(0, 3).forEach((via, i) => {
|
|
params[`via${i}Lat`] = via.lat;
|
|
params[`via${i}Lon`] = via.lon;
|
|
if (via.name) params[`via${i}Name`] = via.name;
|
|
if (via.waitMinutes) params[`via${i}WaitMinutes`] = via.waitMinutes;
|
|
});
|
|
|
|
return request(`/journeys?${qs(params)}`);
|
|
},
|
|
|
|
getFares() {
|
|
return request('/fares');
|
|
},
|
|
|
|
getNearbyDisruptions({ lat, lon, distance, stops }) {
|
|
return request(`/disruptions/nearby?${qs({ lat, lon, distance, stops })}`);
|
|
},
|
|
|
|
getTrip(tripId, { polyline = false } = {}) {
|
|
return request(`/trip/${encodeURIComponent(tripId)}?${qs({ polyline: polyline ? 'true' : undefined })}`);
|
|
},
|
|
|
|
getRemarks({ modes, results = 30 } = {}) {
|
|
return request(`/remarks?${qs({ modes, results })}`);
|
|
},
|
|
|
|
getRadar({ north, west, south, east, modes, tripId }) {
|
|
return request(`/radar?${qs({ north, west, south, east, modes, tripId })}`);
|
|
},
|
|
|
|
// Nächste Abfahrt für mehrere Haltestellen gleichzeitig (z.B. für die
|
|
// Favoriten-Übersicht auf der Startseite) - eine Anfrage statt N.
|
|
getSummaryDepartures(stations) {
|
|
return postJson('/summary/departures', { stations });
|
|
},
|
|
|
|
// Verspätungsstatistik pro Linie (siehe backend/src/utils/delayStats.js) -
|
|
// "sufficientData: false" bedeutet: noch zu wenige Datenpunkte gesammelt.
|
|
getLineDelayStats(line, days = 14) {
|
|
return request(`/stats/delays/${encodeURIComponent(line)}?${qs({ days })}`);
|
|
},
|
|
|
|
// Web-Push (siehe js/push.js + backend/src/routes/webPush.js)
|
|
getPushPublicKey() {
|
|
return request('/webpush/public-key');
|
|
},
|
|
subscribePush(subscription, favorites) {
|
|
return postJson('/webpush/subscribe', { subscription, favorites });
|
|
},
|
|
unsubscribePush(endpoint) {
|
|
return postJson('/webpush/unsubscribe', { endpoint });
|
|
},
|
|
setPushTripAlarm(endpoint, alarm) {
|
|
return postJson('/webpush/trip-alarm', { endpoint, ...alarm });
|
|
},
|
|
clearPushTripAlarm(endpoint) {
|
|
return deleteJson('/webpush/trip-alarm', { endpoint });
|
|
},
|
|
};
|
|
})(); |