Files
Live_Navi/index.html
T
Claude 32c594bc4d Routenplanung auf HAFAS (/journeys) umstellen, Zwischenhalte, Filter, Paginierung
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
2026-09-28 11:49:14 +02:00

262 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>ÖPNV Navi – Live-Abfahrten für Berlin &amp; Brandenburg</title>
<meta name="description" content="Live-Abfahrten, Live-Karte und Routenplaner für den VBB-Verbund." />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="ÖPNV Navi" />
<meta name="theme-color" content="#0C1116" />
<link rel="manifest" href="manifest.json" />
<link rel="icon" href="icons/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16.png" />
<link rel="apple-touch-icon" href="icons/icon-180.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Sans+Condensed:wght@600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<a class="skip-link" href="#main-content" data-i18n="a11y.skipLink">Zum Inhalt springen</a>
<header class="app-header">
<button class="brand" data-tab="departures" aria-label="Zur Startseite">
<span class="brand__mark">ÖPNV</span><span class="brand__mark brand__mark--accent">NAVI</span>
</button>
<div class="search-box search-box--global">
<span class="search-box__icon" aria-hidden="true">⌕</span>
<input type="text" id="station-search" placeholder="Haltestelle, Ort oder Ziel suchen…" data-i18n-placeholder="search.placeholder" autocomplete="off" role="combobox" aria-expanded="false" aria-controls="search-results" aria-autocomplete="list" aria-label="Haltestelle, Ort oder Ziel suchen" data-i18n-aria-label="search.placeholder" />
<button id="search-clear" class="search-clear" type="button" aria-label="Suche leeren" data-i18n-aria-label="search.clear" hidden>×</button>
<ul id="search-results" class="search-results" role="listbox" aria-label="Suchergebnisse" data-i18n-aria-label="search.resultsLabel" hidden></ul>
</div>
<button id="header-favorites-btn" class="header-icon-btn" data-tab="favorites" type="button" title="Favoriten" data-i18n-title="nav.favorites" aria-label="Favoriten" data-i18n-aria-label="nav.favorites">★</button>
</header>
<nav class="tab-nav" aria-label="Hauptnavigation">
<div role="tablist" aria-label="Hauptnavigation" class="tab-nav__list">
<button class="tab-button is-active" role="tab" aria-selected="true" aria-controls="panel-departures" id="tab-departures" tabindex="0" data-tab="departures"><span class="tab-button__icon" aria-hidden="true">🚏</span> <span data-i18n="nav.departures">Abfahrten</span></button>
<button class="tab-button" role="tab" aria-selected="false" aria-controls="panel-journey" id="tab-journey" tabindex="-1" data-tab="journey"><span class="tab-button__icon" aria-hidden="true">🧭</span> <span data-i18n="nav.route">Route</span></button>
<button class="tab-button" role="tab" aria-selected="false" aria-controls="panel-map" id="tab-map" tabindex="-1" data-tab="map"><span class="tab-button__icon" aria-hidden="true">🗺</span> <span data-i18n="nav.map">Karte</span></button>
<button class="tab-button" role="tab" aria-selected="false" aria-controls="panel-more" id="tab-more" tabindex="-1" data-tab="more"><span class="tab-button__icon" aria-hidden="true">☰</span> <span data-i18n="nav.more">Mehr</span></button>
</div>
</nav>
<main class="app-main" id="main-content">
<section class="tab-panel" data-tab-panel="departures" id="panel-departures" role="tabpanel" aria-labelledby="tab-departures mobile-tab-departures" tabindex="-1">
<div class="welcome-panel" id="welcome-panel">
<div class="welcome-panel__eyebrow">LIVE · VBB</div>
<h1 data-i18n="welcome.title">Was fährt jetzt?</h1>
<p data-i18n="welcome.text">Suche eine Haltestelle oder nutze deinen Standort, um die nächsten Abfahrten zu sehen.</p>
<div class="welcome-actions">
<button id="welcome-nearby" class="action-card action-card--primary" type="button">
<span class="action-card__icon" aria-hidden="true">📍</span>
<span><strong data-i18n="welcome.nearbyTitle">In meiner Nähe</strong><small data-i18n="welcome.nearbyText">Haltestellen im Umkreis finden</small></span>
</button>
<button id="welcome-route" class="action-card" type="button">
<span class="action-card__icon" aria-hidden="true">🧭</span>
<span><strong data-i18n="welcome.routeTitle">Route planen</strong><small data-i18n="welcome.routeText">Von A nach B mit Bus &amp; Bahn</small></span>
</button>
</div>
</div>
<div id="nearby-home" class="home-section" hidden>
<div class="section-heading">
<div><span class="section-heading__eyebrow" data-i18n="common.optional">OPTIONAL</span><h2 data-i18n="nearby.homeTitle">Haltestellen in der Nähe</h2></div>
<button id="nearby-home-open" class="text-button" type="button" data-i18n="common.showAll">Alle anzeigen</button>
</div>
<div id="nearby-home-list" class="nearby-preview"></div>
</div>
<div id="departures-view" hidden>
<div class="board-header">
<div>
<span class="section-heading__eyebrow" data-i18n="departures.eyebrow">LIVE-ABFAHRTEN</span>
<h1 id="board-station-name" class="board-station-name" data-i18n="departures.noStation">Keine Haltestelle ausgewählt</h1>
</div>
<button id="favorite-toggle" class="favorite-toggle" hidden>☆ <span data-i18n="favorites.favorite">Favorit</span></button>
</div>
<div id="board-status" class="board-status" aria-live="polite"><span class="status-dot" aria-hidden="true"></span> <span data-i18n="departures.refreshInfo">Aktualisierung alle 30 Sekunden</span></div>
<div id="departures-mode-filter" class="mode-filter"></div>
<div id="departure-board" class="departure-board" role="list" aria-label="Abfahrtstafel"></div>
<div id="board-empty-state" class="board-empty-state" data-i18n="departures.emptyState">Suche oben eine Haltestelle, um Live-Abfahrten zu sehen.</div>
<div id="elevator-status" class="elevator-status" aria-live="polite" hidden></div>
<div id="station-disruption" class="station-disruption" aria-live="polite" hidden></div>
</div>
</section>
<section class="tab-panel" data-tab-panel="map" id="panel-map" role="tabpanel" aria-labelledby="tab-map mobile-tab-map" tabindex="-1" hidden>
<div class="panel-heading"><div><span class="section-heading__eyebrow" data-i18n="common.live">LIVE</span><h1 data-i18n="map.title">Karte</h1><p data-i18n="map.text">Fahrzeuge, Routen und deine aktuelle Fahrt live verfolgen.</p></div></div>
<div class="map-controls">
<div id="map-mode-filter" class="mode-filter"></div>
<button id="map-locate" class="map-locate-btn" type="button">📍 <span data-i18n="map.locate">Mein Standort</span></button>
</div>
<div id="map-isolation-banner" class="map-isolation-banner" aria-live="polite">
<span><span data-i18n="map.following">Verfolge Linie</span> <span id="map-isolation-line" class="map-isolation-banner__line"></span> <span data-i18n="map.live">live</span></span>
<button id="map-isolation-reset" type="button" data-i18n="map.showAllLines">Alle Linien anzeigen</button>
</div>
<div id="live-map" class="live-map" role="application" aria-label="Live-Karte mit Fahrzeugpositionen. Für Screenreader-Nutzer eingeschränkt zugänglich - Details zu einzelnen Fahrten sind über die Abfahrtstafel und den Routenplaner verfügbar." data-i18n-aria-label="map.a11yDescription"></div>
</section>
<section class="tab-panel" data-tab-panel="journey" id="panel-journey" role="tabpanel" aria-labelledby="tab-journey mobile-tab-journey" tabindex="-1" hidden>
<div class="panel-heading"><div><span class="section-heading__eyebrow" data-i18n="journey.eyebrow">VERBINDUNG SUCHEN</span><h1 data-i18n="journey.title">Wohin möchtest du?</h1><p data-i18n="journey.text">Starte mit deinem Standort oder suche einen Ausgangspunkt.</p></div></div>
<div class="journey-card-form">
<div class="journey-input-row">
<div class="journey-input-marker journey-input-marker--start" aria-hidden="true">●</div>
<div class="search-box">
<input type="text" id="journey-from" placeholder="Mein Standort oder Start…" data-i18n-placeholder="journey.fromPlaceholder" aria-label="Start der Route" data-i18n-aria-label="journey.fromPlaceholder" role="combobox" aria-expanded="false" aria-controls="journey-from-results" aria-autocomplete="list" autocomplete="off" />
<ul id="journey-from-results" class="search-results" role="listbox" aria-label="Vorschläge für Start" hidden></ul>
</div>
<button id="journey-use-location" class="location-btn" title="Meinen Standort verwenden" data-i18n-title="journey.useLocation" aria-label="Meinen Standort verwenden" data-i18n-aria-label="journey.useLocation">📍</button>
</div>
<div class="journey-connector" aria-hidden="true"></div>
<div class="journey-input-row">
<div class="journey-input-marker journey-input-marker--end" aria-hidden="true">●</div>
<div class="search-box">
<input type="text" id="journey-to" placeholder="Wohin?" data-i18n-placeholder="journey.toPlaceholder" aria-label="Ziel der Route" data-i18n-aria-label="journey.toPlaceholder" role="combobox" aria-expanded="false" aria-controls="journey-to-results" aria-autocomplete="list" autocomplete="off" />
<ul id="journey-to-results" class="search-results" role="listbox" aria-label="Vorschläge für Ziel" hidden></ul>
</div>
</div>
</div>
<div class="journey-when" role="group" aria-label="Zeitpunkt der Fahrt">
<button class="when-choice is-active" data-when="now" type="button" aria-pressed="true">● <span data-i18n="journey.now">Jetzt</span></button>
<button class="when-choice" data-when="departure" type="button" aria-pressed="false" data-i18n="journey.departureAt">Abfahrt um</button>
<button class="when-choice" data-when="arrival" type="button" aria-pressed="false" data-i18n="journey.arrivalAt">Ankunft um</button>
<input type="datetime-local" id="journey-when" class="journey-datetime" aria-label="Datum und Uhrzeit" hidden />
</div>
<div id="journey-vias" class="journey-vias"></div>
<button id="journey-add-via" class="text-button journey-add-via" type="button">+ <span data-i18n="journey.addVia">Zwischenhalt hinzufügen</span></button>
<label class="wheelchair-toggle">
<input type="checkbox" id="journey-wheelchair" />
<span><span aria-hidden="true">♿</span> <span data-i18n="journey.wheelchair">Nur rollstuhlgerechte Verbindungen</span></span>
</label>
<label class="wheelchair-toggle" id="journey-bike-row">
<input type="checkbox" id="journey-bike" />
<span><span aria-hidden="true">🚲</span> <span data-i18n="journey.bike">Fahrrad mitnehmen</span></span>
</label>
<div class="journey-modes-field" id="journey-modes-field">
<span class="journey-modes-field__label" data-i18n="journey.modes">Verkehrsmittel</span>
<div id="journey-mode-filter" class="mode-filter"></div>
</div>
<div class="journey-options-row">
<button id="journey-submit" class="journey-submit" type="button"><span data-i18n="journey.submit">Verbindungen suchen</span> <span aria-hidden="true">→</span></button>
</div>
<p class="journey-source-note" data-i18n="journey.sourceNote">🛰 Routenplanung mit Echtzeitdaten von VBB &amp; Deutscher Bahn</p>
<div id="journey-share-row" class="journey-share-row" hidden>
<button id="journey-share-btn" class="text-button" type="button"><span aria-hidden="true">🔗</span> <span data-i18n="journey.share">Route teilen</span></button>
</div>
<div id="journey-results" class="journey-results" aria-live="polite"></div>
<div id="journey-load-more-row" class="journey-load-more-row" hidden>
<button id="journey-earlier-btn" class="journey-load-more-btn" type="button" disabled>↑ <span data-i18n="journey.earlier">Früher</span></button>
<button id="journey-later-btn" class="journey-load-more-btn" type="button" disabled>↓ <span data-i18n="journey.later">Später</span></button>
</div>
</section>
<section class="tab-panel" data-tab-panel="nearby" hidden>
<div class="panel-heading"><div><span class="section-heading__eyebrow" data-i18n="nearby.eyebrow">STANDORT</span><h1 data-i18n="nearby.title">In der Nähe</h1><p data-i18n="nearby.text">Finde Haltestellen rund um deinen aktuellen Standort.</p></div></div>
<button id="nearby-locate" class="location-btn location-btn--wide">📍 <span data-i18n="nearby.locate">Meinen Standort verwenden</span></button>
<div id="nearby-list" class="nearby-list"><div class="board-empty-state" data-i18n="nearby.emptyState">Nutze deinen Standort, um Haltestellen in deiner Nähe zu finden.</div></div>
</section>
<section class="tab-panel" data-tab-panel="favorites" hidden>
<div class="panel-heading">
<div><span class="section-heading__eyebrow" data-i18n="favorites.eyebrow">SCHNELLZUGRIFF</span><h1 data-i18n="favorites.title">Meine Haltestellen</h1><p data-i18n="favorites.text">Deine gespeicherten Haltestellen mit Live-Abfahrten und Störungshinweisen.</p></div>
</div>
<div id="favorites-home" class="home-section">
<div id="notify-callout" class="notify-callout" hidden>
<div class="notify-callout__icon" aria-hidden="true">🔔</div>
<div class="notify-callout__text">
<strong data-i18n="favorites.enableNotifyTitle">Störungen sofort mitbekommen</strong>
<small data-i18n="favorites.enableNotifyText">Auch wenn die App gerade nicht offen ist.</small>
</div>
<button id="notify-toggle" class="notify-callout__btn" type="button" data-i18n="favorites.enableNotifyShort">Aktivieren</button>
</div>
<p id="favorites-empty-hint" class="favorites-empty-hint" data-i18n="favorites.emptyHint">Noch keine Favoriten - tippe bei einer Haltestelle auf ☆, um sie hier anzupinnen.</p>
<div id="favorite-cards" class="favorite-cards" role="list"></div>
</div>
</section>
<section class="tab-panel" data-tab-panel="disruptions" hidden>
<div class="panel-heading"><div><span class="section-heading__eyebrow" data-i18n="common.current">AKTUELL</span><h1 data-i18n="disruptions.title">Störungen</h1><p data-i18n="disruptions.text">Ortsbezogene und netzweite Hinweise.</p></div></div>
<div class="disruption-section">
<div class="section-heading"><div><h2 data-i18n="disruptions.nearbyTitle">In deiner Nähe</h2></div></div>
<button id="disruptions-locate" class="location-btn location-btn--wide">📍 <span data-i18n="disruptions.useLocation">Standort verwenden</span></button>
<div id="nearby-disruptions-list" class="remarks-list" aria-live="polite"><div class="board-empty-state" data-i18n="disruptions.nearbyEmpty">Nutze deinen Standort für ortsbezogene Störungen.</div></div>
</div>
<div class="disruption-section">
<div class="section-heading"><div><h2 data-i18n="disruptions.networkTitle">Netzweit</h2></div></div>
<div id="remarks-list" class="remarks-list" aria-live="polite"><div class="board-empty-state" data-i18n="disruptions.loading">Störungsmeldungen werden geladen…</div></div>
</div>
</section>
<section class="tab-panel" data-tab-panel="fares" hidden>
<div class="panel-heading"><div><span class="section-heading__eyebrow" data-i18n="fares.eyebrow">PREISE</span><h1 data-i18n="fares.title">Tickets</h1><p data-i18n="fares.text">Übliche VBB-Fahrpreise auf einen Blick.</p></div></div>
<div class="disclaimer-banner">
<span class="disclaimer-banner__icon" aria-hidden="true">⚠</span>
<span class="disclaimer-banner__text"><span data-i18n="fares.disclaimer">Angaben ohne Gewähr – Preise können sich ändern.</span> <span id="fares-stand"></span></span>
</div>
<div id="fares-content"><div class="board-empty-state" data-i18n="fares.loading">Lade Fahrpreise…</div></div>
<div class="section-heading"><div><h2 data-i18n="fares.buyTitle">Tickets kaufen</h2></div></div>
<div id="fares-companies" class="company-links"></div>
</section>
<section class="tab-panel" data-tab-panel="more" id="panel-more" role="tabpanel" aria-labelledby="tab-more mobile-tab-more" tabindex="-1" hidden>
<div class="panel-heading"><div><span class="section-heading__eyebrow" data-i18n="more.eyebrow">WEITERE FUNKTIONEN</span><h1 data-i18n="nav.more">Mehr</h1><p data-i18n="more.text">Alles, was nicht bei jeder Fahrt gebraucht wird.</p></div></div>
<div class="more-grid">
<button class="more-card" data-open-tab="favorites" type="button"><span aria-hidden="true">★</span><strong data-i18n="favorites.title">Favoriten</strong><small data-i18n="more.favoritesText">Deine gespeicherten Haltestellen</small></button>
<button class="more-card" data-open-tab="nearby" type="button"><span aria-hidden="true">📍</span><strong data-i18n="nearby.title">In der Nähe</strong><small data-i18n="more.nearbyText">Haltestellen in deiner Umgebung</small></button>
<button class="more-card" data-open-tab="disruptions" type="button"><span aria-hidden="true">⚠</span><strong data-i18n="disruptions.title">Störungen</strong><small data-i18n="more.disruptionsText">Ortsbezogen &amp; netzweit</small></button>
<button class="more-card" data-open-tab="fares" type="button"><span aria-hidden="true">🎫</span><strong data-i18n="fares.title">Tickets</strong><small data-i18n="more.faresText">Preise &amp; wo kaufen</small></button>
</div>
<div class="recent-section">
<div class="section-heading"><div><span class="section-heading__eyebrow" data-i18n="more.historyEyebrow">VERLAUF</span><h2 data-i18n="more.historyTitle">Zuletzt gesucht</h2></div><button id="clear-recent" class="text-button" type="button" data-i18n="common.clear">Leeren</button></div>
<div id="recent-searches" class="recent-searches" role="list"></div>
</div>
<div class="recent-section">
<div class="section-heading"><div><span class="section-heading__eyebrow" data-i18n="more.langEyebrow">SPRACHE / LANGUAGE</span><h2 data-i18n="more.langTitle">Sprache</h2></div></div>
<div class="lang-switch" role="group" aria-label="Sprache / Language">
<button id="lang-de" class="lang-switch__btn" type="button" data-lang="de" aria-pressed="true">Deutsch</button>
<button id="lang-en" class="lang-switch__btn" type="button" data-lang="en" aria-pressed="false">English</button>
</div>
</div>
</section>
</main>
<div class="global-disclaimer">
<span class="global-disclaimer__icon" aria-hidden="true">ℹ</span>
<span data-i18n="common.footer">Live-Daten von VBB · Angaben ohne Gewähr</span>
</div>
<nav class="mobile-nav" aria-label="Hauptnavigation mobil">
<div role="tablist" aria-label="Hauptnavigation mobil" class="mobile-nav__list">
<button class="mobile-nav__item is-active" role="tab" aria-selected="true" aria-controls="panel-departures" id="mobile-tab-departures" tabindex="0" data-tab="departures"><span aria-hidden="true">🚏</span><small data-i18n="nav.departures">Abfahrten</small></button>
<button class="mobile-nav__item" role="tab" aria-selected="false" aria-controls="panel-journey" id="mobile-tab-journey" tabindex="-1" data-tab="journey"><span aria-hidden="true">🧭</span><small data-i18n="nav.route">Route</small></button>
<button class="mobile-nav__item" role="tab" aria-selected="false" aria-controls="panel-map" id="mobile-tab-map" tabindex="-1" data-tab="map"><span aria-hidden="true">🗺</span><small data-i18n="nav.map">Karte</small></button>
<button class="mobile-nav__item" role="tab" aria-selected="false" aria-controls="panel-more" id="mobile-tab-more" tabindex="-1" data-tab="more"><span aria-hidden="true">☰</span><small data-i18n="nav.more">Mehr</small></button>
</div>
</nav>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/api.js"></script>
<script src="js/splitflap.js"></script>
<script src="js/modefilter.js"></script>
<script src="js/livemap.js"></script>
<script src="js/tripalarm.js"></script>
<script src="js/tripmodal.js"></script>
<script src="js/push.js"></script>
<script src="js/app.js"></script>
<div id="toast-container" class="toast-container" aria-live="polite"></div>
</body>
</html>