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 = `
    ⚠ ${escapeHtml(d.summary || 'Hinweis')}
    ${escapeHtml(d.text || '')}
    ${escapeHtml(lines)}${escapeHtml(stops)}
    `; 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 = `
    ⚠ ${r.summary || 'Hinweis'}
    ${r.text || ''}
    ${r.company || ''}
    `; 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);