const App = (() => {
const FAVORITES_KEY = 'oepnv-navi:db-favorites';
const LAST_STOP_KEY = 'oepnv-navi:db-last-stop';
const RECENT_SEARCHES_KEY = 'oepnv-navi:db-recent-searches';
let currentStop = null;
let departuresPollHandle = null;
let departuresModeFilter = null;
let journeyFrom = null;
let journeyTo = null;
let journeyArrivalMode = false;
let lastJourneyParams = null;
let remarksLoadedOnce = false;
function loadFavorites() {
try {
return JSON.parse(localStorage.getItem(FAVORITES_KEY)) || [];
} catch (_) {
return [];
}
}
function saveFavorites(favorites) {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
}
function isFavorite(stationId) {
return loadFavorites().some((f) => f.id === stationId);
}
function loadRecentSearches() {
try { return JSON.parse(localStorage.getItem(RECENT_SEARCHES_KEY)) || []; }
catch (_) { return []; }
}
function saveRecentSearch(station) {
if (!station || !station.name) return;
const items = loadRecentSearches().filter((item) => item.id !== station.id);
items.unshift({ id: station.id, name: station.name, source: station.source });
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(items.slice(0, 6)));
renderRecentSearches();
}
function renderRecentSearches() {
const container = document.getElementById('recent-searches');
if (!container) return;
const items = loadRecentSearches();
container.innerHTML = '';
if (!items.length) {
container.innerHTML = '
Noch keine Suchen.
';
return;
}
items.forEach((item) => {
const button = document.createElement('button');
button.className = 'recent-item';
button.innerHTML = `↺${escapeHtml(item.name)}`;
button.addEventListener('click', () => selectStation(item));
container.appendChild(button);
});
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&':'&', '<':'<', '>':'>', "'":''', '"':'"' }[char]));
}
function toggleFavorite(station) {
const favorites = loadFavorites();
const idx = favorites.findIndex((f) => f.id === station.id);
if (idx >= 0) {
favorites.splice(idx, 1);
} else {
favorites.push(station);
}
saveFavorites(favorites);
renderFavoriteChips();
renderRecentSearches();
updateFavoriteButton();
}
function renderFavoriteChips() {
const container = document.getElementById('favorite-chips');
const cardContainer = document.getElementById('favorite-cards');
const favorites = loadFavorites();
if (container) container.innerHTML = '';
if (cardContainer) cardContainer.innerHTML = '';
favorites.forEach((fav) => {
if (container) {
const chip = document.createElement('button');
chip.className = 'chip';
chip.textContent = fav.name;
chip.addEventListener('click', () => selectStation(fav));
container.appendChild(chip);
}
if (cardContainer) {
const card = document.createElement('button');
card.className = 'favorite-card';
card.dataset.favId = fav.id;
card.innerHTML = `
★
${escapeHtml(fav.name)}
Lade nächste Abfahrt…
→
`;
card.addEventListener('click', () => selectStation(fav));
cardContainer.appendChild(card);
}
});
const empty = document.getElementById('favorites-empty-hint');
if (empty) empty.hidden = favorites.length > 0;
const home = document.getElementById('favorites-home');
if (home) home.hidden = favorites.length === 0;
if (favorites.length > 0) loadFavoriteNextDepartures(favorites);
}
async function loadFavoriteNextDepartures(favorites) {
const stations = favorites.map((f) => ({ id: f.id, source: f.source || 'vbb', name: f.name }));
try {
const { results } = await API.getSummaryDepartures(stations);
results.forEach((r) => {
const card = document.querySelector(`.favorite-card[data-fav-id="${CSS.escape(String(r.id))}"]`);
if (!card) return;
const el = card.querySelector('[data-next-departure]');
if (!el) return;
if (r.error || !r.nextDeparture) {
el.textContent = 'Keine Abfahrt gefunden';
return;
}
const dep = r.nextDeparture;
const minutesUntil = dep.when ? Math.max(0, Math.round((new Date(dep.when) - new Date()) / 60000)) : null;
if (dep.cancelled) {
el.innerHTML = `${escapeHtml(dep.line || '')} fällt aus`;
} else {
el.textContent = `${dep.line || '?'} → ${dep.direction || ''} · ${minutesUntil != null ? minutesUntil + ' min' : ''}`;
}
});
} catch (err) {
// Stillschweigend ignorieren - die Karten zeigen dann weiterhin "Lade..."
// bzw. der Nutzer bekommt die Abfahrten trotzdem beim Antippen der Karte.
console.error('Sammel-Abfahrten-Fehler:', err.message);
}
}
function updateFavoriteButton() {
const btn = document.getElementById('favorite-toggle');
if (!currentStop) {
btn.hidden = true;
return;
}
btn.hidden = false;
const active = isFavorite(currentStop.id);
btn.textContent = active ? '★ Favorit' : '☆ Favorit';
btn.classList.toggle('is-active', active);
}
function getUserLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation wird von diesem Browser nicht unterstützt.'));
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
(err) => reject(new Error('Standort konnte nicht ermittelt werden: ' + err.message)),
{ enableHighAccuracy: true, timeout: 10000 },
);
});
}
let searchDebounce = null;
function initSearch() {
const input = document.getElementById('station-search');
const results = document.getElementById('search-results');
input.addEventListener('input', () => {
const clear = document.getElementById('search-clear');
if (clear) clear.hidden = input.value.length === 0;
clearTimeout(searchDebounce);
const query = input.value.trim();
if (query.length < 2) {
results.hidden = true;
results.innerHTML = '';
return;
}
searchDebounce = setTimeout(async () => {
try {
const stations = await API.searchAll(query);
renderSearchResults(results, stations, (station) => {
selectStation(station);
input.value = '';
results.hidden = true;
});
} catch (err) {
console.error('Suchfehler:', err.message);
}
}, 250);
});
const clear = document.getElementById('search-clear');
if (clear) clear.addEventListener('click', () => { input.value = ''; results.hidden = true; clear.hidden = true; input.focus(); });
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-box')) {
results.hidden = true;
}
});
}
// Ein Symbol pro Ergebnis-Herkunft: VBB-Haltestelle (Nahverkehr), echter
// DB-Bahnhof (bundesweit), oder Adresse/POI (nur im Routenplaner relevant).
function resultIcon(loc) {
if (loc.source === 'db') return '🚉';
if (loc.kind === 'address') return '📍';
if (loc.kind === 'poi') return '⭐';
return '🚏'; // VBB-Haltestelle (U/S/Tram/Bus)
}
function renderSearchResults(container, locations, onSelect) {
container.innerHTML = '';
if (locations.length === 0) {
container.innerHTML = 'Keine Treffer';
container.hidden = false;
return;
}
locations.forEach((loc) => {
const li = document.createElement('li');
li.className = 'search-result';
const badge = loc.source === 'db' ? 'DB · bundesweit' : '';
li.innerHTML = `${resultIcon(loc)} ${loc.name}${badge}`;
li.addEventListener('click', () => onSelect(loc));
container.appendChild(li);
});
container.hidden = false;
}
function selectStation(station) {
currentStop = station;
localStorage.setItem(LAST_STOP_KEY, JSON.stringify({ id: station.id, name: station.name, source: station.source }));
saveRecentSearch(station);
activateTab('departures');
document.getElementById('board-station-name').textContent = station.name;
document.getElementById('board-empty-state').hidden = true;
document.getElementById('welcome-panel').hidden = true;
document.getElementById('departures-view').hidden = false;
updateFavoriteButton();
// Der Modefilter (Verkehrsmittel S/U/Tram/Bus/...) gilt nur für VBB-
// Haltestellen - die bundesweite DB-Anbindung unterstützt keine Filterung.
const modeFilterEl = document.getElementById('departures-mode-filter');
if (modeFilterEl) modeFilterEl.hidden = station.source === 'db';
loadDepartures();
restartDeparturesPolling();
}
async function loadDepartures() {
if (!currentStop) return;
const board = document.getElementById('departure-board');
try {
const departures = currentStop.source === 'db'
? (await API.getDbDepartures(currentStop.id)).departures
: (await API.getDepartures(currentStop.id, {
modes: departuresModeFilter ? departuresModeFilter.getModesParam() : null,
})).departures;
renderDepartureBoard(departures);
} catch (err) {
board.innerHTML = `⚠ ${err.message}
`;
}
}
let lastRenderedSource = null;
function renderDepartureBoard(departures) {
const board = document.getElementById('departure-board');
const source = currentStop ? currentStop.source : null;
if (departures.length === 0) {
board.innerHTML = 'Keine Abfahrten für die gewählten Verkehrsmittel in den nächsten 30 Minuten.
';
lastRenderedSource = source;
return;
}
const existingRows = board.querySelectorAll('.flap-row');
// Board komplett neu aufbauen, wenn sich die Anzahl ändert ODER die
// Quelle wechselt (VBB<->DB) - sonst blieben bei zufällig gleicher
// Zeilenzahl alte Klick-Handler/Klassen vom vorherigen Backend hängen.
if (existingRows.length !== departures.length || lastRenderedSource !== source) {
board.innerHTML = '';
departures.forEach((dep, i) => {
board.appendChild(buildDepartureRow(dep, i));
});
lastRenderedSource = source;
}
departures.forEach((dep, i) => {
updateDepartureRow(board.children[i], dep);
});
}
function buildDepartureRow(dep, index) {
const row = document.createElement('div');
row.className = 'flap-row';
row.style.setProperty('--row-index', index);
row.innerHTML = `
Linie
Ziel
Gleis
Abfahrt
`;
// Klick-für-Fahrt-Details gibt es aktuell nur für VBB-Haltestellen (kein
// passender Backend-Endpunkt für DB-Streckenverlauf/Zwischenhalte).
if (currentStop && currentStop.source !== 'db') {
row.setAttribute('role', 'button');
row.setAttribute('tabindex', '0');
row.classList.add('flap-row--clickable');
const openModal = () => {
if (row.dataset.tripId) TripModal.open(row.dataset.tripId);
};
row.addEventListener('click', openModal);
row.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openModal();
}
});
}
return row;
}
function updateDepartureRow(row, dep) {
row.dataset.tripId = dep.tripId || '';
const minutesUntil = dep.when
? Math.max(0, Math.round((new Date(dep.when) - new Date()) / 60000))
: null;
const timeText = dep.cancelled
? 'FAELLT AUS'
: minutesUntil !== null
? `${String(minutesUntil).padStart(2, '0')} MIN`
: '-- MIN';
row.classList.toggle('flap-row--delayed', !!dep.delay && dep.delay > 0);
row.classList.toggle('flap-row--cancelled', !!dep.cancelled);
const lineCell = row.querySelector('[data-field="line"]');
lineCell.style.setProperty('--line-color', dep.color || '#8E99A6');
lineCell.style.setProperty('--line-text-color', dep.textColor || '#fff');
SplitFlap.render(lineCell, (dep.line || '?').toUpperCase());
SplitFlap.render(row.querySelector('[data-field="direction"]'), (dep.direction || '').toUpperCase());
SplitFlap.render(row.querySelector('[data-field="platform"]'), (dep.platform || '-').toUpperCase());
const timeCell = row.querySelector('[data-field="time"]');
if (dep.cancelled) {
const plannedTime = dep.plannedWhen
? new Date(dep.plannedWhen).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
: '';
if (timeCell.dataset.cancelledPlanned !== plannedTime) {
timeCell.dataset.cancelledPlanned = plannedTime;
timeCell.classList.add('flap-cell--time-cancelled');
timeCell.innerHTML = `
FÄLLT AUS
urspr. ${plannedTime}
`;
}
} else {
delete timeCell.dataset.cancelledPlanned;
timeCell.classList.remove('flap-cell--time-cancelled');
SplitFlap.render(timeCell, timeText);
}
}
function restartDeparturesPolling() {
if (departuresPollHandle) clearInterval(departuresPollHandle);
departuresPollHandle = setInterval(loadDepartures, 30000);
}
function initTabs() {
document.querySelectorAll('[data-tab]').forEach((tab) => {
tab.addEventListener('click', () => activateTab(tab.dataset.tab));
});
}
function activateTab(tabName) {
document.querySelectorAll('.tab-button, .mobile-nav__item').forEach((btn) => {
btn.classList.toggle('is-active', btn.dataset.tab === tabName);
});
document.querySelectorAll('.tab-panel').forEach((panel) => {
panel.hidden = panel.dataset.tabPanel !== tabName;
});
if (tabName === 'map') {
LiveMap.startPolling();
} else {
LiveMap.stopPolling();
}
if (tabName === 'disruptions' && !remarksLoadedOnce) {
loadRemarks();
}
if (tabName === 'fares' && !faresLoadedOnce) {
loadFares();
}
}
function initNearby() {
document.getElementById('welcome-nearby').addEventListener('click', () => { activateTab('nearby'); document.getElementById('nearby-locate').click(); });
document.getElementById('welcome-route').addEventListener('click', () => activateTab('journey'));
document.getElementById('nearby-home-open').addEventListener('click', () => activateTab('nearby'));
document.getElementById('nearby-locate').addEventListener('click', async () => {
const listEl = document.getElementById('nearby-list');
listEl.innerHTML = 'Standort wird ermittelt…
';
try {
const { lat, lon } = await getUserLocation();
const { locations } = await API.nearby({ lat, lon, distance: 1000, results: 15 });
renderNearbyList(locations, listEl);
renderNearbyPreview(locations);
} catch (err) {
listEl.innerHTML = `⚠ ${err.message}
`;
}
});
}
function renderNearbyPreview(locations) {
const container = document.getElementById('nearby-home-list');
if (!container) return;
container.innerHTML = '';
locations.slice(0, 3).forEach((loc) => {
const item = document.createElement('button');
item.className = 'nearby-preview__item';
item.innerHTML = `${escapeHtml(loc.name)}🚏 Haltestelle${loc.distance != null ? loc.distance + ' m' : '→'}`;
item.addEventListener('click', () => { selectStation(loc); activateTab('departures'); });
container.appendChild(item);
});
document.getElementById('nearby-home').hidden = locations.length === 0;
}
async function loadNearbyPreview() {
const home = document.getElementById('nearby-home');
const list = document.getElementById('nearby-home-list');
if (!navigator.geolocation || !home || !list) return;
list.innerHTML = 'Standort wird ermittelt…
';
try {
const { lat, lon } = await getUserLocation();
const { locations } = await API.nearby({ lat, lon, distance: 1000, results: 3 });
renderNearbyPreview(locations);
} catch (_) {
home.hidden = true;
}
}
function renderNearbyList(locations, container) {
container.innerHTML = '';
if (locations.length === 0) {
container.innerHTML = 'Keine Haltestellen in der Nähe gefunden.
';
return;
}
locations.forEach((loc) => {
const item = document.createElement('button');
item.className = 'nearby-item';
item.innerHTML = `
${loc.name}
${loc.distance != null ? loc.distance + ' m' : ''}
`;
item.addEventListener('click', () => {
selectStation(loc);
activateTab('departures');
});
container.appendChild(item);
});
}
function initNearbyDisruptions() {
const btn = document.getElementById('disruptions-locate');
if (!btn) return;
btn.addEventListener('click', async () => {
const container = document.getElementById('nearby-disruptions-list');
container.innerHTML = 'Standort wird ermittelt…
';
try {
const { lat, lon } = await getUserLocation();
container.innerHTML = 'Suche Störungen in der Nähe…
';
const { disruptions } = await API.getNearbyDisruptions({ lat, lon, distance: 1000, stops: 6 });
renderNearbyDisruptions(disruptions, container);
} catch (err) {
container.innerHTML = `⚠ ${err.message}
`;
}
});
}
function renderNearbyDisruptions(disruptions, container) {
container.innerHTML = '';
if (disruptions.length === 0) {
container.innerHTML = 'Aktuell keine Störungen in deiner Nähe bekannt.
';
return;
}
disruptions.forEach((d) => {
const card = document.createElement('div');
card.className = 'remark-card';
const lines = d.affectedLines.length ? `Linie${d.affectedLines.length > 1 ? 'n' : ''} ${d.affectedLines.join(', ')}` : '';
const stops = d.affectedStops.length ? ` · nahe ${d.affectedStops.join(', ')}` : '';
card.innerHTML = `
`;
container.appendChild(card);
});
}
let faresLoadedOnce = false;
async function loadFares() {
const content = document.getElementById('fares-content');
const companies = document.getElementById('fares-companies');
try {
const fares = await API.getFares();
faresLoadedOnce = true;
document.getElementById('fares-stand').textContent = `Stand: ${new Date(fares.stand).toLocaleDateString('de-DE')}`;
content.innerHTML = fares.gruppen.map((gruppe) => `
${escapeHtml(gruppe.titel)}
${gruppe.tickets.map((t) => `
${escapeHtml(t.name)}${escapeHtml(t.bereich)}${t.hinweis ? ' · ' + escapeHtml(t.hinweis) : ''}
${t.preis.toFixed(2).replace('.', ',')} €
`).join('')}
`).join('');
companies.innerHTML = fares.unternehmen.map((u) => `
${escapeHtml(u.name)}${escapeHtml(u.beschreibung)}
↗
`).join('');
} catch (err) {
content.innerHTML = `⚠ ${err.message}
`;
}
}
async function loadRemarks() {
const container = document.getElementById('remarks-list');
container.innerHTML = 'Lade Störungsmeldungen…
';
try {
const { remarks } = await API.getRemarks({ results: 30 });
remarksLoadedOnce = true;
renderRemarks(remarks, container);
} catch (err) {
container.innerHTML = `⚠ ${err.message}
`;
}
}
function renderRemarks(remarks, container) {
container.innerHTML = '';
if (remarks.length === 0) {
container.innerHTML = 'Aktuell keine Störungsmeldungen bekannt.
';
return;
}
remarks.forEach((r) => {
const card = document.createElement('div');
card.className = 'remark-card';
card.innerHTML = `
`;
container.appendChild(card);
});
}
function initJourneyPlanner() {
setupJourneyInput('journey-from', (loc) => (journeyFrom = loc));
setupJourneyInput('journey-to', (loc) => (journeyTo = loc));
document.getElementById('journey-use-location').addEventListener('click', async () => {
const input = document.getElementById('journey-from');
input.value = 'Standort wird ermittelt…';
try {
const { lat, lon } = await getUserLocation();
journeyFrom = { lat, lon, name: 'Mein Standort' };
input.value = 'Mein Standort';
} catch (err) {
input.value = '';
alert(err.message);
}
});
document.querySelectorAll('.when-choice').forEach((button) => {
button.addEventListener('click', () => {
document.querySelectorAll('.when-choice').forEach((item) => item.classList.remove('is-active'));
button.classList.add('is-active');
const mode = button.dataset.when;
journeyArrivalMode = mode === 'arrival';
const when = document.getElementById('journey-when');
when.hidden = mode === 'now';
if (mode !== 'now' && !when.value) {
const d = new Date(Date.now() + 10 * 60000);
d.setSeconds(0, 0);
when.value = new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0,16);
}
});
});
document.getElementById('journey-submit').addEventListener('click', async () => {
const resultsEl = document.getElementById('journey-results');
if (!journeyFrom || !journeyTo) {
resultsEl.innerHTML = 'Bitte Start und Ziel aus der Liste auswählen.
';
return;
}
resultsEl.innerHTML = 'Suche Verbindungen…
';
const whenInput = document.getElementById('journey-when').value;
const params = {
from: journeyFrom,
to: journeyTo,
when: whenInput || undefined,
arrival: journeyArrivalMode,
results: 5,
};
lastJourneyParams = params;
try {
const { journeys } = await API.getJourneys(params);
renderJourneys(journeys, resultsEl);
} catch (err) {
resultsEl.innerHTML = `⚠ ${err.message}
`;
}
});
}
function setupJourneyInput(inputId, onSelect) {
const input = document.getElementById(inputId);
const results = document.getElementById(`${inputId}-results`);
let debounce = null;
input.addEventListener('input', () => {
clearTimeout(debounce);
const query = input.value.trim();
if (query.length < 2) {
results.hidden = true;
return;
}
debounce = setTimeout(async () => {
try {
const { locations } = await API.searchLocations(query);
renderSearchResults(results, locations, (loc) => {
onSelect({ id: loc.id || undefined, lat: loc.latitude, lon: loc.longitude, name: loc.name, kind: loc.kind });
input.value = loc.name;
results.hidden = true;
});
} catch (err) {
console.error('Suchfehler:', err.message);
}
}, 250);
});
document.addEventListener('click', (e) => {
if (!e.target.closest(`#${inputId}`) && !e.target.closest(`#${inputId}-results`)) {
results.hidden = true;
}
});
}
function renderJourneys(journeys, container) {
container.innerHTML = '';
if (journeys.length === 0) {
container.innerHTML = 'Keine Verbindungen gefunden.
';
return;
}
journeys.forEach((journey, journeyIndex) => {
const card = document.createElement('div');
card.className = 'journey-card';
const legsHtml = journey.legs
.map((leg) => {
const depTime = leg.departure ? new Date(leg.departure).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) : '';
const arrTime = leg.arrival ? new Date(leg.arrival).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) : '';
const lineLabel = leg.walking ? 'Fußweg' : leg.line;
const lineStyle = leg.walking
? ''
: `style="background:${leg.color || '#8E99A6'};color:${leg.textColor || '#fff'};"`;
const trackBtn = !leg.walking && leg.tripId
? ``
: '';
const infoBtn = !leg.walking && leg.tripId
? ``
: '';
return `
${lineLabel}
${leg.origin || ''} → ${leg.destination || ''}
${depTime} – ${arrTime}
${infoBtn}
${trackBtn}
`;
})
.join('');
const fareHtml = journey.fareEstimate && journey.fareEstimate.price != null
? journey.fareEstimate.exact
? `💶 ${journey.fareEstimate.price.toFixed(2).replace('.', ',')} € (Tarifbereich ${journey.fareEstimate.zone}, Einzelfahrschein, ohne Gewähr)
`
: `💶 ca. ${journey.fareEstimate.price.toFixed(2).replace('.', ',')} € (Tarifbereich ~${journey.fareEstimate.zone}, geschätzt, ohne Gewähr)
`
: '';
card.innerHTML = `
${legsHtml}
${fareHtml}
`;
card.querySelector('.journey-map-toggle').addEventListener('click', (e) => {
toggleJourneyMiniMap(journeyIndex, journey, e.currentTarget);
});
card.querySelectorAll('.leg-track-btn[data-action="live"]').forEach((btn) => {
btn.addEventListener('click', () => {
LiveMap.trackTrip(btn.dataset.tripId);
activateTab('map');
});
});
card.querySelectorAll('.leg-track-btn[data-action="info"]').forEach((btn) => {
btn.addEventListener('click', () => TripModal.open(btn.dataset.tripId));
});
container.appendChild(card);
});
}
let openMiniMapIndex = null;
let miniMapInstance = null;
async function toggleJourneyMiniMap(journeyIndex, journeyFallback, button) {
const mapEl = document.getElementById(`journey-mini-map-${journeyIndex}`);
if (openMiniMapIndex === journeyIndex) {
mapEl.classList.remove('is-open');
if (miniMapInstance) { miniMapInstance.remove(); miniMapInstance = null; }
openMiniMapIndex = null;
button.textContent = '🗺️ Streckenverlauf anzeigen';
return;
}
if (openMiniMapIndex !== null) {
const prevMapEl = document.getElementById(`journey-mini-map-${openMiniMapIndex}`);
if (prevMapEl) prevMapEl.classList.remove('is-open');
if (miniMapInstance) { miniMapInstance.remove(); miniMapInstance = null; }
const prevButton = document.querySelector(`.journey-map-toggle[data-journey-index="${openMiniMapIndex}"]`);
if (prevButton) prevButton.textContent = '🗺️ Streckenverlauf anzeigen';
}
openMiniMapIndex = journeyIndex;
mapEl.classList.add('is-open');
button.textContent = '🗺️ Streckenverlauf ausblenden';
let journeyWithPolylines = journeyFallback;
if (lastJourneyParams) {
try {
const { journeys } = await API.getJourneys({
...lastJourneyParams,
polylines: true,
results: journeyIndex + 1,
});
if (journeys[journeys.length - 1]) journeyWithPolylines = journeys[journeys.length - 1];
} catch (_) {
// Fallback: ohne Polylines, zeigt dann nur die Marker-Punkte
}
}
setTimeout(() => {
miniMapInstance = LiveMap.createMiniMap(mapEl.id, journeyWithPolylines);
}, 200); // Wartet auf CSS-Höhen-Transition, damit Leaflet die Größe korrekt berechnet
}
function restoreLastStop() {
try {
const saved = JSON.parse(localStorage.getItem(LAST_STOP_KEY));
if (saved && saved.id) selectStation(saved);
} catch (_) {
// kein gespeicherter Stop, ignorieren
}
}
function init() {
initSearch();
initTabs();
initJourneyPlanner();
initNearby();
initNearbyDisruptions();
renderFavoriteChips();
renderRecentSearches();
departuresModeFilter = ModeFilter.create(
document.getElementById('departures-mode-filter'),
'oepnv-navi:modes:departures',
() => loadDepartures(),
);
const mapModeFilter = ModeFilter.create(
document.getElementById('map-mode-filter'),
'oepnv-navi:modes:map',
() => {},
);
LiveMap.setModeFilter(() => mapModeFilter.getModesParam());
document.getElementById('map-locate').addEventListener('click', () => LiveMap.locateUser());
document.getElementById('map-isolation-reset').addEventListener('click', () => LiveMap.clearIsolation());
document.getElementById('clear-recent').addEventListener('click', () => { localStorage.removeItem(RECENT_SEARCHES_KEY); renderRecentSearches(); });
document.querySelectorAll('[data-open-tab]').forEach((button) => button.addEventListener('click', () => activateTab(button.dataset.openTab)));
document.getElementById('favorite-toggle').addEventListener('click', () => {
if (currentStop) toggleFavorite(currentStop);
});
restoreLastStop();
}
return { init };
})();
document.addEventListener('DOMContentLoaded', App.init);