Add files via upload
This commit is contained in:
1 parent
fa38de2a0a
commit
693e208cfd
12 files changed
+949
-191
No files matched your search
@@ -5,6 +5,56 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert
|
|||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
|
||||||
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
## [37.2.0] - 2026-07-07
|
||||||
|
|
||||||
|
### Favoriten-Chips in der Abfahrten-View
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- **Favoriten wohnen jetzt in der Abfahrten-View**: Über dem Suchfeld sitzt eine wischbare Chip-Reihe (Pillenform) mit ALLEN Favoriten - die aktive Station ist gelb markiert, ein Tap wechselt die Station ohne Umweg über die Startseite. Auf Mobile gleiche Wisch-Mechanik wie die Filter-Chips.
|
||||||
|
- **Startseite aufgeräumt**: zeigt nur noch kompakt die Top 3 Favoriten plus "Alle N Stationen anzeigen" (springt in die Abfahrten-View), statt bis zu 10 Einträge zu stapeln.
|
||||||
|
- Stationswechsel frischt die Aktiv-Markierung der Chips automatisch auf.
|
||||||
|
|
||||||
|
## [37.1.1] - 2026-07-07
|
||||||
|
|
||||||
|
### Komplette Route + Fokus-Modus, Entwickler-Tab, eine Versionsnummer
|
||||||
|
|
||||||
|
#### Added - Live-Map
|
||||||
|
- **Komplette Route beim Fahrzeug-Tap**: map/trips liefert nur das Segment im Kartenausschnitt - beim Antippen wird jetzt sofort das lokale Segment gezeichnet und im Hintergrund die GANZE Strecke über /v6/trip nachgeladen (gleicher 60s-Cache wie die Abfahrts-Details: Details + Route = maximal 1 Request).
|
||||||
|
- **Fokus-Modus**: Solange das Popup offen ist, werden alle anderen Fahrzeuge ausgeblendet - nur die angetippte Fahrt mit ihrer Route bleibt sichtbar. Der Zähler zeigt "🎯 [Linie] im Fokus". Popup schließen stellt alles wieder her.
|
||||||
|
- Polyline-Decoder generalisiert (Precision 5/6/7 je Endpoint, legGeometry liefert precision als Feld).
|
||||||
|
|
||||||
|
#### Fixed - Filter auf Mobile
|
||||||
|
- **Alle Filtergruppen vereinheitlicht** (Abfahrten, Routenplaner, Live-Map): Label auf eigener Zeile, Buttons als EINE horizontal wischbare Chip-Reihe statt zerklüftetem Umbruch. Die Routenplaner-Buttons hatten zudem eine 20px-Sondergröße (doppelt so groß wie alle anderen Filter) - jetzt Einheitsgröße mit 40px-Touch-Zielen.
|
||||||
|
|
||||||
|
#### Changed - Entwickler-Bereich & Version
|
||||||
|
- **Eine Versionsnummer für alles**: APP_VERSION (js/api.js) ist die einzige Quelle; Startseite, Entwickler-Tab und Cache-Anzeige werden daraus befüllt. Ein neuer Smoke-Test erzwingt, dass package.json, APP_VERSION und SW-Cache-Name übereinstimmen - Versions-Drift wie bisher (v30/v33 auf der Website) fällt ab jetzt in der CI auf.
|
||||||
|
- **Entwickler-Tab aktualisiert**: veraltete Angaben korrigiert (15s-Radar -> 30s+Animation, HAFAS-Endpoints -> MOTIS, BVG-Farbtabelle -> GTFS-Farben, "Push geplant" -> Favoriten/Offline), Statistik-Box durch Architektur-Fakten ersetzt.
|
||||||
|
- **Entwickler-Kachel auf der Startseite**: vierte Kachel im Feature-Raster (neben Live-Map) mit Versionsnummer, öffnet den Entwickler-Bereich. Füllt nebenbei die leere Rasterlücke im 2-Spalten-Layout.
|
||||||
|
|
||||||
|
## [37.1.0] - 2026-07-07
|
||||||
|
|
||||||
|
### Desktop-Aufräumaktion (nach Screenshots)
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
- **Doppelte Beschriftung im Menü**: Die per JS injizierten "Nav-Quick-Infos" standen ohne CSS als zweites Label neben jedem Menüpunkt ("Start | Startseite"). Komplett entfernt - ebenso die Search-Hints ("Tippe mindestens 2 Zeichen"), die als permanenter Rohtext unter den Suchfeldern klebten, und tote data-tooltip-Attribute ohne CSS.
|
||||||
|
- **Abfahrten auf Desktop unsymmetrisch**: Das 2-Spalten- (ab 768px) bzw. 3-Spalten-Grid (ab 1600px) erzeugte ungleich hohe, unruhige Karten. Jetzt überall eine ruhige einspaltige Liste; die Breite regelt der zentrierte Container. Die 1600px-Stufe nutzt Lesebreite (820px) statt 1400px Riesenfläche.
|
||||||
|
- **Detail-Modal auf Desktop kaputt**: Eine alte Regel machte das Vollbild-Overlay selbst 600px schmal und links verankert - die Seite schaute rechts durch, mit eigener Scrollbar in der Mitte. Jetzt: Overlay bleibt Vollbild (nichts scheint durch), nur der Inhalt ist auf Lesebreite zentriert.
|
||||||
|
|
||||||
|
#### Hinweis zur OS-Frage
|
||||||
|
Layout-Anpassung läuft bewusst über Viewport-Breite und hover/pointer-Media-Queries statt User-Agent/OS-Erkennung: Touch-Laptops und große Tablets brechen jede OS-Annahme, und Browser reduzieren den User-Agent zunehmend. Die Media-Queries waren richtig - nur an drei Stellen kaputt.
|
||||||
|
|
||||||
|
## [37.0.0] - 2026-07-07
|
||||||
|
|
||||||
|
### Tests im Repo, funktionierender Abfahrten-Filter, Strecken-Anzeige, Mehr-laden
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
- **Tests + CI im Repo**: 19 Adapter-Tests und der komplette Smoke-Test liegen jetzt unter tests/ (`npm test`), plus GitHub Action (.github/workflows/test.yml), die bei jedem Push Syntax-Checks und beide Test-Suiten ausführt.
|
||||||
|
- **Strecken-Anzeige auf der Live-Map**: Fahrzeug antippen -> die komplette Route der Fahrt wird in der Linienfarbe eingezeichnet (Popup schließen -> Linie verschwindet). Die Polylines liegen bereits im Speicher - kostet null API-Requests.
|
||||||
|
- **"Mehr Abfahrten laden"**: Button unter der Abfahrtsliste erhöht die Anzahl schrittweise (20 -> 40 -> 60). Bewusst über den n-Parameter statt Cursor gelöst: die erweiterte Liste bleibt so auch beim 30s-Auto-Refresh erhalten. Stationswechsel setzt auf 20 zurück.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
- **Abfahrten-Filter repariert und entpatcht**: Der Filter (U-Bahn/S-Bahn/Tram/Bus/Regional über den Abfahrten) hing an fragilen Monkey-Patches, die loadDepartures/displayDepartures zur Laufzeit überschrieben - und hatte einen echten Bug: Bei aktivem Filter hat der Auto-Refresh alle 30 Sekunden die UNGEFILTERTE Liste gerendert (der tripId-Vergleich lief gegen die gefilterte DOM-Sicht und schlug immer fehl). Jetzt: displayDepartures merkt sich die volle Liste und filtert intern, der Auto-Refresh vergleicht gegen die gefilterte Sicht, die Monkey-Patches sind entfernt.
|
||||||
|
|
||||||
## [36.0.0] - 2026-07-06
|
## [36.0.0] - 2026-07-06
|
||||||
|
|
||||||
### Schnellere Karte, offizielle Linienfarben, Desktop-Layout
|
### Schnellere Karte, offizielle Linienfarben, Desktop-Layout
|
||||||
|
|||||||
+32
-15
@@ -102,7 +102,7 @@
|
|||||||
<div class="home-hero-title">
|
<div class="home-hero-title">
|
||||||
<h1>VBB NETZ STATUS</h1>
|
<h1>VBB NETZ STATUS</h1>
|
||||||
<p>Live-Nahverkehr für Berlin & Brandenburg</p>
|
<p>Live-Nahverkehr für Berlin & Brandenburg</p>
|
||||||
<div class="home-version">v36.0</div>
|
<div class="home-version" id="homeVersion"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -132,6 +132,14 @@
|
|||||||
<p>Fahrzeuge in Echtzeit verfolgen</p>
|
<p>Fahrzeuge in Echtzeit verfolgen</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="home-info-card home-info-card-btn" id="homeDevCard"
|
||||||
|
aria-label="Entwickler-Bereich mit Changelog öffnen">
|
||||||
|
<div class="home-info-icon">👨💻</div>
|
||||||
|
<div class="home-info-text">
|
||||||
|
<strong>Für Entwickler</strong>
|
||||||
|
<p>Changelog & Infos – <span id="homeDevVersion"></span></p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -151,6 +159,10 @@
|
|||||||
<!-- View: Abfahrten -->
|
<!-- View: Abfahrten -->
|
||||||
<div class="view-container" id="view-departures">
|
<div class="view-container" id="view-departures">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<!-- Favoriten-Chips: 1-Tap-Wechsel zwischen deinen Stationen -->
|
||||||
|
<div class="departure-favorites" id="departureFavorites" style="display: none;"
|
||||||
|
role="group" aria-label="Favoriten-Stationen"></div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
@@ -176,12 +188,14 @@
|
|||||||
<!-- Filter für Linientypen -->
|
<!-- Filter für Linientypen -->
|
||||||
<div class="departure-filters" id="departureFilters" style="display: none;">
|
<div class="departure-filters" id="departureFilters" style="display: none;">
|
||||||
<div class="filter-label">Filter:</div>
|
<div class="filter-label">Filter:</div>
|
||||||
|
<div class="filter-chips">
|
||||||
<button class="filter-btn active" data-filter="all">Alle</button>
|
<button class="filter-btn active" data-filter="all">Alle</button>
|
||||||
<button class="filter-btn" data-filter="subway">🚇 U-Bahn</button>
|
<button class="filter-btn" data-filter="subway">🚇 U-Bahn</button>
|
||||||
<button class="filter-btn" data-filter="suburban">🚆 S-Bahn</button>
|
<button class="filter-btn" data-filter="suburban">🚆 S-Bahn</button>
|
||||||
<button class="filter-btn" data-filter="tram">🚊 Tram</button>
|
<button class="filter-btn" data-filter="tram">🚊 Tram</button>
|
||||||
<button class="filter-btn" data-filter="bus">🚌 Bus</button>
|
<button class="filter-btn" data-filter="bus">🚌 Bus</button>
|
||||||
<button class="filter-btn" data-filter="regional">🚄 Regional</button>
|
<button class="filter-btn" data-filter="regional">🚄 Regional</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -264,12 +278,14 @@
|
|||||||
<!-- Verkehrsmittel-Filter -->
|
<!-- Verkehrsmittel-Filter -->
|
||||||
<div class="journey-transport-filters">
|
<div class="journey-transport-filters">
|
||||||
<div class="filter-label">Verkehrsmittel:</div>
|
<div class="filter-label">Verkehrsmittel:</div>
|
||||||
|
<div class="filter-chips">
|
||||||
<button class="transport-filter-btn active" data-transport="subway">🚇 U-Bahn</button>
|
<button class="transport-filter-btn active" data-transport="subway">🚇 U-Bahn</button>
|
||||||
<button class="transport-filter-btn active" data-transport="suburban">🚆 S-Bahn</button>
|
<button class="transport-filter-btn active" data-transport="suburban">🚆 S-Bahn</button>
|
||||||
<button class="transport-filter-btn active" data-transport="tram">🚊 Tram</button>
|
<button class="transport-filter-btn active" data-transport="tram">🚊 Tram</button>
|
||||||
<button class="transport-filter-btn active" data-transport="bus">🚌 Bus</button>
|
<button class="transport-filter-btn active" data-transport="bus">🚌 Bus</button>
|
||||||
<button class="transport-filter-btn active" data-transport="regional">🚄 Regional</button>
|
<button class="transport-filter-btn active" data-transport="regional">🚄 Regional</button>
|
||||||
<button class="transport-filter-btn active" data-transport="express">🚅 Express</button>
|
<button class="transport-filter-btn active" data-transport="express">🚅 Express</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -324,8 +340,8 @@
|
|||||||
<div class="dev-info-box">
|
<div class="dev-info-box">
|
||||||
<h3>🚀 Version Info</h3>
|
<h3>🚀 Version Info</h3>
|
||||||
<table class="dev-info-table">
|
<table class="dev-info-table">
|
||||||
<tr><td>Version:</td><td><strong>30.0</strong></td></tr>
|
<tr><td>Version:</td><td><strong id="devVersion"></strong></td></tr>
|
||||||
<tr><td>Release:</td><td>10.02.2026</td></tr>
|
<tr><td>Release:</td><td id="devRelease"></td></tr>
|
||||||
<tr><td>Lizenz:</td><td>MIT</td></tr>
|
<tr><td>Lizenz:</td><td>MIT</td></tr>
|
||||||
<tr><td>Status:</td><td><span class="status-badge-dev">✅ Stabil</span></td></tr>
|
<tr><td>Status:</td><td><span class="status-badge-dev">✅ Stabil</span></td></tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -353,12 +369,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dev-info-box">
|
<div class="dev-info-box">
|
||||||
<h3>📊 Statistiken</h3>
|
<h3>📊 Architektur</h3>
|
||||||
<table class="dev-info-table">
|
<table class="dev-info-table">
|
||||||
<tr><td>HTML:</td><td>407 Zeilen</td></tr>
|
<tr><td>Module:</td><td>6 (api, transitous, app, livemap, changelog, extras)</td></tr>
|
||||||
<tr><td>CSS:</td><td>2689 Zeilen</td></tr>
|
<tr><td>Datenquelle:</td><td>Transitous / MOTIS 2</td></tr>
|
||||||
<tr><td>JavaScript:</td><td>2161 Zeilen</td></tr>
|
<tr><td>Tests:</td><td>Adapter- + Smoke-Suite (GitHub Actions)</td></tr>
|
||||||
<tr><td>Gesamt:</td><td>~5.300 Zeilen Code</td></tr>
|
<tr><td>Code:</td><td>~8.500 Zeilen (HTML, CSS, JS)</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -385,7 +401,7 @@
|
|||||||
<li>Linien-Filter</li>
|
<li>Linien-Filter</li>
|
||||||
<li>Detail-Ansicht mit Zwischenhalten</li>
|
<li>Detail-Ansicht mit Zwischenhalten</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="feature-api"><strong>API:</strong> <code>GET /stops/:id/departures</code></p>
|
<p class="feature-api"><strong>API:</strong> <code>GET /v6/stoptimes</code></p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -401,7 +417,7 @@
|
|||||||
<li>Verkehrsmittel-Filter</li>
|
<li>Verkehrsmittel-Filter</li>
|
||||||
<li>Detail-Ansicht mit allen Haltestellen</li>
|
<li>Detail-Ansicht mit allen Haltestellen</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="feature-api"><strong>API:</strong> <code>GET /journeys</code></p>
|
<p class="feature-api"><strong>API:</strong> <code>GET /v6/plan</code></p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -413,11 +429,12 @@
|
|||||||
<li>Leaflet.js Kartenintegration</li>
|
<li>Leaflet.js Kartenintegration</li>
|
||||||
<li>Dynamische Bounding Box</li>
|
<li>Dynamische Bounding Box</li>
|
||||||
<li>Fahrzeug-Filter (U-Bahn, S-Bahn, Bus, Tram)</li>
|
<li>Fahrzeug-Filter (U-Bahn, S-Bahn, Bus, Tram)</li>
|
||||||
<li>BVG-Linienfarben</li>
|
<li>Offizielle Linienfarben aus dem GTFS-Feed</li>
|
||||||
<li>Auto-Update alle 15s</li>
|
<li>Strecken-Anzeige + Fokus-Modus per Fahrzeug-Tap</li>
|
||||||
|
<li>API-Poll alle 30s + lokale 1s-Animation</li>
|
||||||
<li>Zoom & Pan optimiert</li>
|
<li>Zoom & Pan optimiert</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="feature-api"><strong>API:</strong> <code>GET /radar</code></p>
|
<p class="feature-api"><strong>API:</strong> <code>GET /v6/map/trips</code></p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -429,10 +446,10 @@
|
|||||||
<li>Installierbar (Add to Home Screen)</li>
|
<li>Installierbar (Add to Home Screen)</li>
|
||||||
<li>Service Worker mit Caching</li>
|
<li>Service Worker mit Caching</li>
|
||||||
<li>Offline-Modus</li>
|
<li>Offline-Modus</li>
|
||||||
<li>Push-Benachrichtigungen (geplant)</li>
|
<li>Favoriten & Offline-Abfahrten (localStorage)</li>
|
||||||
<li>App-Manifest</li>
|
<li>App-Manifest</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="feature-api"><strong>Cache:</strong> <code>vbb-status-v36</code></p>
|
<p class="feature-api"><strong>Cache:</strong> <code id="devCacheName"></code></p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
// VBB NETZ STATUS - Haupt-Script
|
// VBB NETZ STATUS - Haupt-Script
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
|
// ZENTRALE VERSIONSNUMMER - einzige Stelle, die bei Releases
|
||||||
|
// angepasst wird. Alle Anzeigen (Startseite, Entwickler-Tab)
|
||||||
|
// lesen von hier; package.json + SW-Cache-Name manuell mitziehen.
|
||||||
|
const APP_VERSION = '37.2.0';
|
||||||
|
const APP_RELEASE_DATE = '07.07.2026';
|
||||||
|
|
||||||
// Seit v35: Transitous (api.transitous.org) ist die einzige
|
// Seit v35: Transitous (api.transitous.org) ist die einzige
|
||||||
// Datenquelle. transport.rest (VBB/BVG) wurde komplett entfernt.
|
// Datenquelle. transport.rest (VBB/BVG) wurde komplett entfernt.
|
||||||
// Die konkreten Endpoints kapselt js/transitous.js.
|
// Die konkreten Endpoints kapselt js/transitous.js.
|
||||||
@@ -18,6 +24,7 @@
|
|||||||
// Zentral hier deklariert, damit KEIN Modul auf Variablen aus
|
// Zentral hier deklariert, damit KEIN Modul auf Variablen aus
|
||||||
// später geladenen Dateien angewiesen ist.
|
// später geladenen Dateien angewiesen ist.
|
||||||
let currentView = 'home';
|
let currentView = 'home';
|
||||||
|
let departuresCount = 20; // Wie viele Abfahrten laden ("Mehr laden" erhöht)
|
||||||
let currentStationLat = null; // Koordinaten der gewählten Station
|
let currentStationLat = null; // Koordinaten der gewählten Station
|
||||||
let currentStationLon = null; // (für Transitous-Fallback ohne ID-Mapping)
|
let currentStationLon = null; // (für Transitous-Fallback ohne ID-Mapping)
|
||||||
let liveMap = null;
|
let liveMap = null;
|
||||||
@@ -247,11 +254,40 @@
|
|||||||
return isFavorite(stationId);
|
return isFavorite(stationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Favoriten auf dem Home-Screen rendern
|
// Favoriten rendern - an ZWEI Stellen:
|
||||||
|
// 1) Abfahrten-View: ALLE Favoriten als wischbare Chip-Reihe
|
||||||
|
// (1-Tap-Wechsel zwischen Stationen, aktive Station markiert)
|
||||||
|
// 2) Startseite: kompakt nur die Top 3 + "Alle anzeigen"
|
||||||
function renderFavorites() {
|
function renderFavorites() {
|
||||||
|
const favorites = getFavorites();
|
||||||
|
|
||||||
|
// --- Abfahrten-View: Chips ---
|
||||||
|
const chipsContainer = document.getElementById('departureFavorites');
|
||||||
|
if (chipsContainer) {
|
||||||
|
if (favorites.length === 0) {
|
||||||
|
chipsContainer.innerHTML = '';
|
||||||
|
chipsContainer.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
chipsContainer.style.display = 'block';
|
||||||
|
chipsContainer.innerHTML = `
|
||||||
|
<div class="filter-label">⭐ Deine Stationen</div>
|
||||||
|
<div class="filter-chips">
|
||||||
|
${favorites.map(f => `
|
||||||
|
<button class="fav-chip ${f.id === currentStationId ? 'active' : ''}"
|
||||||
|
data-id="${escapeHtml(f.id)}" data-name="${escapeHtml(f.name)}"
|
||||||
|
data-lat="${f.lat != null ? f.lat : ''}" data-lon="${f.lon != null ? f.lon : ''}"
|
||||||
|
aria-label="Abfahrten für ${escapeHtml(f.name)} anzeigen">
|
||||||
|
${escapeHtml(f.name)}
|
||||||
|
</button>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Startseite: kompakte Top 3 ---
|
||||||
const container = document.getElementById('homeFavorites');
|
const container = document.getElementById('homeFavorites');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const favorites = getFavorites();
|
|
||||||
|
|
||||||
if (favorites.length === 0) {
|
if (favorites.length === 0) {
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
@@ -259,11 +295,14 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const top = favorites.slice(0, 3);
|
||||||
|
const rest = favorites.length - top.length;
|
||||||
|
|
||||||
container.style.display = '';
|
container.style.display = '';
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="home-favorites-title">⭐ Deine Stationen</div>
|
<div class="home-favorites-title">⭐ Deine Stationen</div>
|
||||||
<div class="home-favorites-list">
|
<div class="home-favorites-list">
|
||||||
${favorites.map(f => `
|
${top.map(f => `
|
||||||
<button class="home-favorite-item" data-id="${escapeHtml(f.id)}" data-name="${escapeHtml(f.name)}"
|
<button class="home-favorite-item" data-id="${escapeHtml(f.id)}" data-name="${escapeHtml(f.name)}"
|
||||||
data-lat="${f.lat != null ? f.lat : ''}" data-lon="${f.lon != null ? f.lon : ''}"
|
data-lat="${f.lat != null ? f.lat : ''}" data-lon="${f.lon != null ? f.lon : ''}"
|
||||||
aria-label="Abfahrten für ${escapeHtml(f.name)} anzeigen">
|
aria-label="Abfahrten für ${escapeHtml(f.name)} anzeigen">
|
||||||
@@ -271,6 +310,12 @@
|
|||||||
<span class="home-favorite-arrow" aria-hidden="true">→</span>
|
<span class="home-favorite-arrow" aria-hidden="true">→</span>
|
||||||
</button>
|
</button>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
|
${rest > 0 ? `
|
||||||
|
<button class="home-favorite-item home-favorites-more" id="homeShowAllFavs"
|
||||||
|
aria-label="Alle Favoriten in der Abfahrten-Ansicht zeigen">
|
||||||
|
<span class="home-favorite-name">Alle ${favorites.length} Stationen anzeigen</span>
|
||||||
|
<span class="home-favorite-arrow" aria-hidden="true">→</span>
|
||||||
|
</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -516,7 +516,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// "Mehr Abfahrten laden" (Event-Delegation: Button wird bei jedem
|
||||||
|
// Render neu erzeugt, der Handler hier überlebt das)
|
||||||
|
departuresContainer.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('#loadMoreDepartures')) {
|
||||||
|
departuresCount = Math.min(60, departuresCount + 20);
|
||||||
|
if (navigator.vibrate) navigator.vibrate(10);
|
||||||
|
loadDepartures();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function selectStation(stationId, stationName, lat = null, lon = null) {
|
function selectStation(stationId, stationName, lat = null, lon = null) {
|
||||||
|
departuresCount = 20; // Neue Station -> wieder mit 20 Abfahrten starten
|
||||||
currentStationId = stationId;
|
currentStationId = stationId;
|
||||||
currentStationName = stationName;
|
currentStationName = stationName;
|
||||||
currentStationLat = lat;
|
currentStationLat = lat;
|
||||||
@@ -529,6 +540,7 @@
|
|||||||
// Station für den nächsten App-Start merken
|
// Station für den nächsten App-Start merken
|
||||||
storageSet(STORAGE_KEYS.lastStation, { id: stationId, name: stationName, lat, lon });
|
storageSet(STORAGE_KEYS.lastStation, { id: stationId, name: stationName, lat, lon });
|
||||||
updateFavoriteButton();
|
updateFavoriteButton();
|
||||||
|
renderFavorites(); // Aktiv-Markierung der Favoriten-Chips auffrischen
|
||||||
|
|
||||||
// Haptic feedback
|
// Haptic feedback
|
||||||
if (navigator.vibrate) {
|
if (navigator.vibrate) {
|
||||||
@@ -591,7 +603,7 @@
|
|||||||
const data = await transitousDepartures({
|
const data = await transitousDepartures({
|
||||||
id: currentStationId, name: currentStationName,
|
id: currentStationId, name: currentStationName,
|
||||||
lat: currentStationLat, lon: currentStationLon
|
lat: currentStationLat, lon: currentStationLon
|
||||||
});
|
}, departuresCount);
|
||||||
displayDepartures(data.departures || []);
|
displayDepartures(data.departures || []);
|
||||||
saveDeparturesCache(currentStationId, data.departures || []);
|
saveDeparturesCache(currentStationId, data.departures || []);
|
||||||
noteRefreshSuccess();
|
noteRefreshSuccess();
|
||||||
@@ -636,13 +648,18 @@
|
|||||||
const data = await transitousDepartures({
|
const data = await transitousDepartures({
|
||||||
id: currentStationId, name: currentStationName,
|
id: currentStationId, name: currentStationName,
|
||||||
lat: currentStationLat, lon: currentStationLon
|
lat: currentStationLat, lon: currentStationLon
|
||||||
});
|
}, departuresCount);
|
||||||
const departures = data.departures || [];
|
const fullDepartures = data.departures || [];
|
||||||
|
allDepartures = fullDepartures;
|
||||||
|
const departures = (typeof applyDepartureFilter === 'function')
|
||||||
|
? applyDepartureFilter(fullDepartures) : fullDepartures;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// In-Place-Update: nur Zeiten/Verspätungen im bestehenden DOM
|
// In-Place-Update: nur Zeiten/Verspätungen im bestehenden DOM
|
||||||
// aktualisieren (kein Flackern, Scroll-Position bleibt).
|
// aktualisieren (kein Flackern, Scroll-Position bleibt).
|
||||||
// Hat sich die Fahrten-Menge geändert -> voller Re-Render.
|
// WICHTIG: Vergleich gegen die GEFILTERTE Sicht - vorher hat
|
||||||
|
// ein aktiver Filter den Vergleich immer scheitern lassen und
|
||||||
|
// der Auto-Refresh hat alle 30s die UNGEFILTERTE Liste gerendert.
|
||||||
const items = departuresContainer.querySelectorAll('.departure-item');
|
const items = departuresContainer.querySelectorAll('.departure-item');
|
||||||
const domTripIds = Array.from(items).map(i => i.dataset.tripId);
|
const domTripIds = Array.from(items).map(i => i.dataset.tripId);
|
||||||
const newTripIds = departures.map(d => String(d.tripId));
|
const newTripIds = departures.map(d => String(d.tripId));
|
||||||
@@ -651,7 +668,7 @@
|
|||||||
domTripIds.every((id, i) => id === newTripIds[i]);
|
domTripIds.every((id, i) => id === newTripIds[i]);
|
||||||
|
|
||||||
if (!sameSet) {
|
if (!sameSet) {
|
||||||
displayDepartures(departures);
|
displayDepartures(fullDepartures);
|
||||||
} else {
|
} else {
|
||||||
departures.forEach((dep, i) => {
|
departures.forEach((dep, i) => {
|
||||||
const item = items[i];
|
const item = items[i];
|
||||||
@@ -673,7 +690,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveDeparturesCache(currentStationId, departures);
|
saveDeparturesCache(currentStationId, fullDepartures);
|
||||||
noteRefreshSuccess();
|
noteRefreshSuccess();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -682,8 +699,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayDepartures(departures) {
|
function displayDepartures(fullDepartures) {
|
||||||
if (departures.length === 0) {
|
// Volle Liste für den Abfahrten-Filter merken (Zustand in extras.js)
|
||||||
|
allDepartures = fullDepartures;
|
||||||
|
|
||||||
|
// Filterleiste nur zeigen, wenn es überhaupt Abfahrten gibt
|
||||||
|
const filtersEl = document.getElementById('departureFilters');
|
||||||
|
if (filtersEl) {
|
||||||
|
filtersEl.style.display = fullDepartures.length > 0 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullDepartures.length === 0) {
|
||||||
departuresContainer.innerHTML = `
|
departuresContainer.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-state-icon">🚉</div>
|
<div class="empty-state-icon">🚉</div>
|
||||||
@@ -693,6 +719,20 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aktiven Verkehrsmittel-Filter anwenden
|
||||||
|
const departures = (typeof applyDepartureFilter === 'function')
|
||||||
|
? applyDepartureFilter(fullDepartures) : fullDepartures;
|
||||||
|
|
||||||
|
if (departures.length === 0) {
|
||||||
|
departuresContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-state-icon">🔍</div>
|
||||||
|
<div>Keine Abfahrten mit diesem Filter</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const stationInfoHTML = `
|
const stationInfoHTML = `
|
||||||
<div class="station-info">
|
<div class="station-info">
|
||||||
@@ -746,7 +786,14 @@
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
departuresContainer.innerHTML = stationInfoHTML + departuresHTML;
|
// "Mehr laden": erhöht n (max. 60) und lädt neu - so bleibt die
|
||||||
|
// erweiterte Liste auch beim Auto-Refresh erhalten
|
||||||
|
const loadMoreHTML = departuresCount < 60 ? `
|
||||||
|
<button class="btn btn-full-width load-more-btn" id="loadMoreDepartures">
|
||||||
|
⏬ Mehr Abfahrten laden
|
||||||
|
</button>` : '';
|
||||||
|
|
||||||
|
departuresContainer.innerHTML = stationInfoHTML + departuresHTML + loadMoreHTML;
|
||||||
|
|
||||||
// Event-Listener für Departure-Items
|
// Event-Listener für Departure-Items
|
||||||
departuresContainer.querySelectorAll('.departure-item').forEach(item => {
|
departuresContainer.querySelectorAll('.departure-item').forEach(item => {
|
||||||
|
|||||||
+56
-113
@@ -101,25 +101,21 @@ document.addEventListener('click', (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reine Filterfunktion - wird von displayDepartures (app.js) beim
|
||||||
|
// Rendern angewendet. Der Filter-Zustand lebt hier.
|
||||||
|
function applyDepartureFilter(departures) {
|
||||||
|
if (activeDepartureFilters.has('all')) return departures;
|
||||||
|
return departures.filter(dep => {
|
||||||
|
const product = dep.line?.product?.toLowerCase() || '';
|
||||||
|
return activeDepartureFilters.has(product);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter geändert -> mit der vollen Liste neu rendern
|
||||||
|
// (displayDepartures filtert selbst; so geht nie Datenbestand verloren)
|
||||||
function filterDepartures() {
|
function filterDepartures() {
|
||||||
if (!allDepartures || allDepartures.length === 0) return;
|
if (!allDepartures || allDepartures.length === 0) return;
|
||||||
|
displayDepartures(allDepartures);
|
||||||
let filtered = allDepartures;
|
|
||||||
|
|
||||||
if (!activeDepartureFilters.has('all')) {
|
|
||||||
filtered = allDepartures.filter(dep => {
|
|
||||||
const product = dep.line?.product?.toLowerCase() || '';
|
|
||||||
return activeDepartureFilters.has(product);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container neu rendern
|
|
||||||
const container = document.getElementById('departuresContainer');
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #FFED00;">Keine Abfahrten mit diesem Filter</div>';
|
|
||||||
} else {
|
|
||||||
displayDepartures(filtered);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -167,34 +163,6 @@ function getActiveTransportModes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// PATCHES FÜR FILTER-INTEGRATION
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
// Original loadDepartures überschreiben
|
|
||||||
const originalLoadDepartures = loadDepartures;
|
|
||||||
loadDepartures = async function() {
|
|
||||||
await originalLoadDepartures();
|
|
||||||
|
|
||||||
// Filter-UI einblenden wenn Abfahrten geladen
|
|
||||||
const filtersEl = document.getElementById('departureFilters');
|
|
||||||
if (filtersEl && allDepartures.length > 0) {
|
|
||||||
filtersEl.style.display = 'flex';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// displayDepartures erweitern um allDepartures zu speichern
|
|
||||||
const originalDisplayDepartures = displayDepartures;
|
|
||||||
displayDepartures = function(departures) {
|
|
||||||
// Speichere alle Abfahrten für Filter
|
|
||||||
if (!activeDepartureFilters || activeDepartureFilters.has('all')) {
|
|
||||||
allDepartures = departures;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normale Anzeige
|
|
||||||
originalDisplayDepartures(departures);
|
|
||||||
};
|
|
||||||
|
|
||||||
// HINWEIS: Der frühere "searchJourneys"-Patch wurde entfernt.
|
// HINWEIS: Der frühere "searchJourneys"-Patch wurde entfernt.
|
||||||
// Die Funktion existierte nie und verursachte einen ReferenceError,
|
// Die Funktion existierte nie und verursachte einen ReferenceError,
|
||||||
// der das restliche Script abgebrochen hat.
|
// der das restliche Script abgebrochen hat.
|
||||||
@@ -440,6 +408,23 @@ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|||||||
// APP-START: GESPEICHERTEN ZUSTAND WIEDERHERSTELLEN
|
// APP-START: GESPEICHERTEN ZUSTAND WIEDERHERSTELLEN
|
||||||
// ==========================================
|
// ==========================================
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Versionsnummer überall aus der EINEN Quelle (APP_VERSION) befüllen
|
||||||
|
const setText = (id, text) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = text;
|
||||||
|
};
|
||||||
|
setText('homeVersion', 'v' + APP_VERSION);
|
||||||
|
setText('homeDevVersion', 'v' + APP_VERSION);
|
||||||
|
setText('devVersion', APP_VERSION);
|
||||||
|
setText('devRelease', APP_RELEASE_DATE);
|
||||||
|
setText('devCacheName', 'vbb-status-v' + APP_VERSION);
|
||||||
|
|
||||||
|
// Entwickler-Karte auf der Startseite -> Entwickler-View
|
||||||
|
const devCard = document.getElementById('homeDevCard');
|
||||||
|
if (devCard) {
|
||||||
|
devCard.addEventListener('click', () => switchView('developer'));
|
||||||
|
}
|
||||||
|
|
||||||
// Favoriten auf dem Home-Screen anzeigen
|
// Favoriten auf dem Home-Screen anzeigen
|
||||||
renderFavorites();
|
renderFavorites();
|
||||||
|
|
||||||
@@ -466,8 +451,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
checkJourneySearchReady();
|
checkJourneySearchReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Klick auf Favorit (Home-Screen) -> direkt zu den Abfahrten
|
// Klick auf Favorit:
|
||||||
|
// - "Alle anzeigen" (Home) -> nur zur Abfahrten-View wechseln
|
||||||
|
// - Favorit auf Home -> Abfahrten-View + Station laden
|
||||||
|
// - Favoriten-Chip in der Abfahrten-View -> Station direkt wechseln
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('#homeShowAllFavs')) {
|
||||||
|
switchView('departures');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chip = e.target.closest('.fav-chip');
|
||||||
|
if (chip) {
|
||||||
|
if (chip.dataset.id === currentStationId) return; // schon aktiv
|
||||||
|
selectStation(
|
||||||
|
chip.dataset.id, chip.dataset.name,
|
||||||
|
chip.dataset.lat ? parseFloat(chip.dataset.lat) : null,
|
||||||
|
chip.dataset.lon ? parseFloat(chip.dataset.lon) : null
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const favItem = e.target.closest('.home-favorite-item');
|
const favItem = e.target.closest('.home-favorite-item');
|
||||||
if (favItem) {
|
if (favItem) {
|
||||||
switchView('departures');
|
switchView('departures');
|
||||||
@@ -531,75 +535,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// HOVER-EXTRAS (bereinigt)
|
// HOVER-EXTRAS: KOMPLETT ENTFERNT (v37.1)
|
||||||
// Frühere Version zeigte FAKE-Daten (zufällige Gleisnummern,
|
// - Nav-Quick-Infos: standen als doppelte Beschriftung im Menü
|
||||||
// erfundene "Genauigkeit ±10m", Alert-Attrappen für Favorit/Teilen/
|
// - Search-Hints ("Tippe mindestens 2 Zeichen"): hatten kein CSS und
|
||||||
// Export/Kalender) und hatte einen Scope-Bug im Search-Hint,
|
// klebten als permanenter Rohtext unter den Suchfeldern
|
||||||
// der bei jedem Tastendruck einen ReferenceError warf.
|
// - data-tooltip-Attribute: hatten ebenfalls kein CSS -> tote Attribute
|
||||||
|
// Die title-Attribute der Buttons liefern native Browser-Tooltips.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (window.matchMedia('(hover: hover)').matches) {
|
|
||||||
|
|
||||||
// Navigation Items - Quick Info hinzufügen
|
|
||||||
const navQuickInfos = [
|
|
||||||
{ selector: '[data-view="home"]', info: 'Startseite' },
|
|
||||||
{ selector: '[data-view="departures"]', info: 'Live-Abfahrten' },
|
|
||||||
{ selector: '[data-view="journey"]', info: 'Routenplaner' },
|
|
||||||
{ selector: '[data-view="livemap"]', info: 'Fahrzeuge live' },
|
|
||||||
{ selector: '[data-view="developer"]', info: 'Changelog & Infos' }
|
|
||||||
];
|
|
||||||
|
|
||||||
navQuickInfos.forEach(item => {
|
|
||||||
const el = document.querySelector(item.selector);
|
|
||||||
if (el && !el.querySelector('.nav-quick-info')) {
|
|
||||||
const info = document.createElement('div');
|
|
||||||
info.className = 'nav-quick-info';
|
|
||||||
info.textContent = item.info;
|
|
||||||
el.appendChild(info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Search Hints (Scope-Bug behoben: hint jetzt im richtigen Scope)
|
|
||||||
document.querySelectorAll('.search-input').forEach(input => {
|
|
||||||
let hint = input.parentElement.querySelector('.search-hint');
|
|
||||||
if (!hint) {
|
|
||||||
hint = document.createElement('div');
|
|
||||||
hint.className = 'search-hint';
|
|
||||||
hint.textContent = 'Tippe mindestens 2 Zeichen';
|
|
||||||
input.parentElement.appendChild(hint);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
const len = input.value.length;
|
|
||||||
if (len === 0) {
|
|
||||||
hint.textContent = 'Tippe mindestens 2 Zeichen';
|
|
||||||
} else if (len === 1) {
|
|
||||||
hint.textContent = 'Noch 1 Zeichen...';
|
|
||||||
} else {
|
|
||||||
hint.textContent = 'Suche aktiv';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rich Tooltips (nur belegbare Aussagen)
|
|
||||||
const tooltips = {
|
|
||||||
'#refreshBtn': 'Lädt die neuesten Abfahrtszeiten. Auto-Refresh alle 30 Sekunden.',
|
|
||||||
'#searchJourneyBtn': 'Sucht Routen zwischen zwei Stationen mit Echtzeit-Daten.',
|
|
||||||
'#locationBtn': 'Findet die nächstgelegene Station per GPS.',
|
|
||||||
'#swapBtn': 'Vertauscht Start- und Zielstation.'
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(tooltips).forEach(([selector, tooltip]) => {
|
|
||||||
const el = document.querySelector(selector);
|
|
||||||
if (el) {
|
|
||||||
el.setAttribute('data-tooltip', tooltip);
|
|
||||||
el.style.position = 'relative';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Quick Actions an Abfahrten: ECHTE Funktionen statt Alert-Attrappen
|
// Quick Actions an Abfahrten: ECHTE Funktionen statt Alert-Attrappen
|
||||||
// (Event-Delegation statt MutationObserver - robuster und günstiger)
|
// (Event-Delegation statt MutationObserver - robuster und günstiger)
|
||||||
document.addEventListener('click', async (e) => {
|
document.addEventListener('click', async (e) => {
|
||||||
|
|||||||
+77
-4
@@ -242,6 +242,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// STRECKEN-ANZEIGE BEIM FAHRZEUG-KLICK
|
||||||
|
// Die Polylines liegen bereits dekodiert in liveMapSegments -
|
||||||
|
// die Route zu zeichnen kostet also NULL API-Requests.
|
||||||
|
// ==========================================
|
||||||
|
let activeRouteLine = null;
|
||||||
|
let focusedVehicleTripId = null; // Fokus-Modus: nur diese Fahrt zeigen
|
||||||
|
|
||||||
|
async function showVehicleRoute(tripId) {
|
||||||
|
hideVehicleRoute();
|
||||||
|
focusedVehicleTripId = tripId;
|
||||||
|
|
||||||
|
const seg = liveMapSegments.find(s => s.tripId === tripId);
|
||||||
|
const color = seg?.color || getBVGLineColor(seg?.lineName || '');
|
||||||
|
|
||||||
|
// 1) SOFORT das lokale Segment zeichnen (null Wartezeit)...
|
||||||
|
if (seg && seg.points && seg.points.length >= 2) {
|
||||||
|
activeRouteLine = L.polyline(seg.points, {
|
||||||
|
color, weight: 5, opacity: 0.75,
|
||||||
|
lineCap: 'round', interactive: false
|
||||||
|
}).addTo(liveMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fokus-Modus aktivieren: andere Fahrzeuge ausblenden
|
||||||
|
renderVehiclesFromSegments();
|
||||||
|
|
||||||
|
// 2) ...dann die KOMPLETTE Route nachladen (map/trips liefert nur
|
||||||
|
// das Segment im Ausschnitt; die ganze Strecke steckt in /trip).
|
||||||
|
// Gleicher Cache wie die Abfahrts-Details -> max. 1 Request.
|
||||||
|
try {
|
||||||
|
const route = await transitousTripGeometry(tripId);
|
||||||
|
// Nutzer könnte inzwischen woanders geklickt haben
|
||||||
|
if (route && focusedVehicleTripId === tripId) {
|
||||||
|
if (activeRouteLine) activeRouteLine.remove();
|
||||||
|
activeRouteLine = L.polyline(route.points, {
|
||||||
|
color: route.color || color, weight: 5, opacity: 0.75,
|
||||||
|
lineCap: 'round', interactive: false
|
||||||
|
}).addTo(liveMap);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Volle Route nicht verfügbar -> Segment bleibt stehen
|
||||||
|
console.warn('Komplette Route nicht ladbar:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideVehicleRoute() {
|
||||||
|
if (activeRouteLine) {
|
||||||
|
activeRouteLine.remove();
|
||||||
|
activeRouteLine = null;
|
||||||
|
}
|
||||||
|
if (focusedVehicleTripId) {
|
||||||
|
focusedVehicleTripId = null;
|
||||||
|
renderVehiclesFromSegments(); // alle Fahrzeuge wieder zeigen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Aus den gespeicherten Segmenten die AKTUELLEN Positionen berechnen
|
// Aus den gespeicherten Segmenten die AKTUELLEN Positionen berechnen
|
||||||
// und rendern. Läuft jede Sekunde lokal - null API-Kosten.
|
// und rendern. Läuft jede Sekunde lokal - null API-Kosten.
|
||||||
function renderVehiclesFromSegments() {
|
function renderVehiclesFromSegments() {
|
||||||
@@ -250,6 +306,8 @@
|
|||||||
const movements = [];
|
const movements = [];
|
||||||
|
|
||||||
for (const seg of liveMapSegments) {
|
for (const seg of liveMapSegments) {
|
||||||
|
// Fokus-Modus: nur die angetippte Fahrt rendern
|
||||||
|
if (focusedVehicleTripId && seg.tripId !== focusedVehicleTripId) continue;
|
||||||
const pos = segmentPositionAt(seg, nowMs);
|
const pos = segmentPositionAt(seg, nowMs);
|
||||||
if (!pos) continue; // Fahrzeug (noch) nicht auf diesem Segment unterwegs
|
if (!pos) continue; // Fahrzeug (noch) nicht auf diesem Segment unterwegs
|
||||||
movements.push({
|
movements.push({
|
||||||
@@ -278,12 +336,18 @@
|
|||||||
return activeVehicleTypes.has(vehicleType);
|
return activeVehicleTypes.has(vehicleType);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update Counter mit Live-Indikator
|
// Update Counter mit Live-Indikator (bzw. Fokus-Hinweis)
|
||||||
const countEl = document.getElementById('vehicleCount');
|
const countEl = document.getElementById('vehicleCount');
|
||||||
if (countEl) {
|
if (countEl) {
|
||||||
countEl.innerHTML =
|
if (focusedVehicleTripId) {
|
||||||
`<span class="live-dot" aria-hidden="true"></span> ` +
|
const focusName = filteredMovements[0]?.line?.name || '';
|
||||||
`<span style="color: #fff;">${filteredMovements.length}</span> Fahrzeuge live`;
|
countEl.innerHTML =
|
||||||
|
`🎯 <span style="color: #fff;">${escapeHtml(focusName)}</span> im Fokus – Popup schließen zum Beenden`;
|
||||||
|
} else {
|
||||||
|
countEl.innerHTML =
|
||||||
|
`<span class="live-dot" aria-hidden="true"></span> ` +
|
||||||
|
`<span style="color: #fff;">${filteredMovements.length}</span> Fahrzeuge live`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seenTripIds = new Set();
|
const seenTripIds = new Set();
|
||||||
@@ -342,6 +406,9 @@
|
|||||||
|
|
||||||
const marker = L.marker([lat, lon], { icon: icon });
|
const marker = L.marker([lat, lon], { icon: icon });
|
||||||
marker.bindPopup(popupContent);
|
marker.bindPopup(popupContent);
|
||||||
|
// Popup auf = Strecke dieser Fahrt einzeichnen (in Linienfarbe)
|
||||||
|
marker.on('popupopen', () => showVehicleRoute(tripId));
|
||||||
|
marker.on('popupclose', hideVehicleRoute);
|
||||||
marker.addTo(liveMap);
|
marker.addTo(liveMap);
|
||||||
vehicleMarkerMap.set(tripId, { marker, lineName });
|
vehicleMarkerMap.set(tripId, { marker, lineName });
|
||||||
});
|
});
|
||||||
@@ -354,6 +421,12 @@
|
|||||||
vehicleMarkerMap.delete(tripId);
|
vehicleMarkerMap.delete(tripId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route eines nicht mehr existierenden Fahrzeugs mit aufräumen
|
||||||
|
if (activeRouteLine && ![...vehicleMarkerMap.values()]
|
||||||
|
.some(e => e.marker.isPopupOpen && e.marker.isPopupOpen())) {
|
||||||
|
hideVehicleRoute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter Buttons mit Event Delegation
|
// Filter Buttons mit Event Delegation
|
||||||
|
|||||||
+37
-5
@@ -135,10 +135,10 @@ async function transitousStopQuery({ id, name, lat, lon }) {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// ABFAHRTEN (stoptimes -> HAFAS-departures-Form)
|
// ABFAHRTEN (stoptimes -> HAFAS-departures-Form)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
async function transitousDepartures(station) {
|
async function transitousDepartures(station, n = 20) {
|
||||||
const stopQuery = await transitousStopQuery(station);
|
const stopQuery = await transitousStopQuery(station);
|
||||||
// 15s Cache: schützt vor Refresh-Spam
|
// 15s Cache: schützt vor Refresh-Spam
|
||||||
const data = await transitousFetch(`/v6/stoptimes?${stopQuery}&n=20`, 15000);
|
const data = await transitousFetch(`/v6/stoptimes?${stopQuery}&n=${n}`, 15000);
|
||||||
|
|
||||||
const departures = (data.stopTimes || []).map(st => {
|
const departures = (data.stopTimes || []).map(st => {
|
||||||
const p = st.place || {};
|
const p = st.place || {};
|
||||||
@@ -294,6 +294,31 @@ async function transitousTrip(tripId) {
|
|||||||
return { trip: { stopovers } };
|
return { trip: { stopovers } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kompletter Streckenverlauf einer Fahrt (für die Live-Map).
|
||||||
|
// map/trips liefert nur das Segment im Kartenausschnitt - die GANZE
|
||||||
|
// Route steckt in der legGeometry des /trip-Endpoints. Gleiche URL wie
|
||||||
|
// transitousTrip -> geteilter 60s-Cache, Details+Route = 1 Request.
|
||||||
|
async function transitousTripGeometry(tripId) {
|
||||||
|
const rawId = tripId.startsWith(TRANSITOUS_ID_PREFIX)
|
||||||
|
? tripId.slice(TRANSITOUS_ID_PREFIX.length) : tripId;
|
||||||
|
const itinerary = await transitousFetch(
|
||||||
|
`/v6/trip?tripId=${encodeURIComponent(rawId)}`,
|
||||||
|
60000
|
||||||
|
);
|
||||||
|
|
||||||
|
const leg = (itinerary.legs || []).find(l => l.mode !== 'WALK') || (itinerary.legs || [])[0];
|
||||||
|
if (!leg || !leg.legGeometry || !leg.legGeometry.points) return null;
|
||||||
|
|
||||||
|
const precision = leg.legGeometry.precision || 6;
|
||||||
|
const points = decodePolyline(leg.legGeometry.points, precision);
|
||||||
|
if (points.length < 2) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
points,
|
||||||
|
color: motisColor(leg.routeColor)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// RADAR / LIVE-MAP (map/trips -> HAFAS-radar-Form)
|
// RADAR / LIVE-MAP (map/trips -> HAFAS-radar-Form)
|
||||||
//
|
//
|
||||||
@@ -303,8 +328,10 @@ async function transitousTrip(tripId) {
|
|||||||
// Transitous-Web-Karte selbst auch).
|
// Transitous-Web-Karte selbst auch).
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
// Google-Polyline dekodieren (Precision 5 laut MOTIS-Spec für map/trips)
|
// Google-Polyline dekodieren. Die Precision variiert je Endpoint:
|
||||||
function decodePolyline5(encoded) {
|
// map/trips liefert 5, legGeometry (trip/plan) liefert precision als Feld mit.
|
||||||
|
function decodePolyline(encoded, precision = 5) {
|
||||||
|
const factor = Math.pow(10, precision);
|
||||||
const points = [];
|
const points = [];
|
||||||
let index = 0, lat = 0, lon = 0;
|
let index = 0, lat = 0, lon = 0;
|
||||||
while (index < encoded.length) {
|
while (index < encoded.length) {
|
||||||
@@ -318,11 +345,16 @@ function decodePolyline5(encoded) {
|
|||||||
const delta = (result & 1) ? ~(result >> 1) : (result >> 1);
|
const delta = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||||
if (which === 0) lat += delta; else lon += delta;
|
if (which === 0) lat += delta; else lon += delta;
|
||||||
}
|
}
|
||||||
points.push([lat / 1e5, lon / 1e5]);
|
points.push([lat / factor, lon / factor]);
|
||||||
}
|
}
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kompatibilitäts-Alias (Tests + map/trips)
|
||||||
|
function decodePolyline5(encoded) {
|
||||||
|
return decodePolyline(encoded, 5);
|
||||||
|
}
|
||||||
|
|
||||||
async function transitousRadarSegments(bounds, zoom) {
|
async function transitousRadarSegments(bounds, zoom) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const path = `/v6/map/trips?zoom=${Math.round(zoom)}` +
|
const path = `/v6/map/trips?zoom=${Math.round(zoom)}` +
|
||||||
|
|||||||
+7
-3
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "vbb-netz-status",
|
"name": "vbb-netz-status",
|
||||||
"version": "36.0.0",
|
"version": "37.2.0",
|
||||||
"description": "Live-Nahverkehr für Berlin & Brandenburg - Progressive Web App (PWA) für Echtzeit-Abfahrten, Routenplanung und Live-Fahrzeugverfolgung im VBB-Gebiet",
|
"description": "Live-Nahverkehr für Berlin & Brandenburg - Progressive Web App (PWA) für Echtzeit-Abfahrten, Routenplanung und Live-Fahrzeugverfolgung im VBB-Gebiet",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "node server.js"
|
"dev": "node server.js",
|
||||||
|
"test": "node tests/adapter.test.js && node tests/smoke.test.js",
|
||||||
|
"test:syntax": "node --check js/api.js && node --check js/transitous.js && node --check js/app.js && node --check js/livemap.js && node --check js/changelog.js && node --check js/extras.js && node --check service-worker.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"vbb",
|
"vbb",
|
||||||
@@ -33,7 +35,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-types": "^2.1.35"
|
"mime-types": "^2.1.35"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {
|
||||||
|
"jsdom": "^24.0.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0",
|
"node": ">=16.0.0",
|
||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = 'vbb-status-v36';
|
const CACHE_NAME = 'vbb-status-v37.2.0';
|
||||||
|
|
||||||
// App-Shell: nur Dateien, die wirklich existieren!
|
// App-Shell: nur Dateien, die wirklich existieren!
|
||||||
// (Der alte SW listete eine nicht existierende Font-Datei – dadurch
|
// (Der alte SW listete eine nicht existierende Font-Datei – dadurch
|
||||||
|
|||||||
+144
-38
@@ -1017,11 +1017,6 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-modal {
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pull-to-refresh styles */
|
/* Pull-to-refresh styles */
|
||||||
@@ -1352,17 +1347,6 @@
|
|||||||
padding: 30px 40px;
|
padding: 30px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Abfahrten in 2 Spalten */
|
|
||||||
#departuresContainer {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.station-info {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Journey auch größer */
|
/* Journey auch größer */
|
||||||
.journey-result {
|
.journey-result {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
@@ -1376,17 +1360,9 @@
|
|||||||
/* Extra Large Desktop */
|
/* Extra Large Desktop */
|
||||||
@media (min-width: 1600px) {
|
@media (min-width: 1600px) {
|
||||||
.container {
|
.container {
|
||||||
max-width: 1400px;
|
max-width: 820px; /* Lesebreite statt Riesenfläche */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3 Spalten für Abfahrten */
|
|
||||||
#departuresContainer {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.station-info {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Optimierungen */
|
/* Mobile Optimierungen */
|
||||||
@@ -2252,10 +2228,6 @@
|
|||||||
========================================== */
|
========================================== */
|
||||||
|
|
||||||
.departure-filters, .journey-transport-filters {
|
.departure-filters, .journey-transport-filters {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background-color: #0a0a0a;
|
background-color: #0a0a0a;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -2266,7 +2238,14 @@
|
|||||||
color: #FFED00;
|
color: #FFED00;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 5px;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Einheitliche Chip-Reihe für ALLE Filtergruppen */
|
||||||
|
.filter-chips {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-btn, .transport-filter-btn {
|
.filter-btn, .transport-filter-btn {
|
||||||
@@ -2282,10 +2261,8 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transport-filter-btn {
|
/* (Sondergröße entfernt - Journey-Chips waren doppelt so groß
|
||||||
font-size: 20px;
|
wie alle anderen Filter und brachen unruhig um) */
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-btn.active, .transport-filter-btn.active {
|
.filter-btn.active, .transport-filter-btn.active {
|
||||||
background-color: #FFED00;
|
background-color: #FFED00;
|
||||||
@@ -3953,11 +3930,140 @@ select:focus-visible {
|
|||||||
min-width: 76px;
|
min-width: 76px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-modal {
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livemap-container {
|
.livemap-container {
|
||||||
height: calc(100vh - 100px);
|
height: calc(100vh - 100px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* v37: "Mehr Abfahrten laden"-Button */
|
||||||
|
.load-more-btn {
|
||||||
|
margin-top: 14px;
|
||||||
|
background: rgba(255, 237, 0, 0.08);
|
||||||
|
border: 1px dashed rgba(255, 237, 0, 0.4);
|
||||||
|
color: #FFED00;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn:hover,
|
||||||
|
.load-more-btn:focus-visible {
|
||||||
|
background: rgba(255, 237, 0, 0.15);
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
v37.1: Detail-Modal auf Desktop
|
||||||
|
Overlay bleibt Vollbild (nichts scheint durch),
|
||||||
|
Inhalt zentriert auf Lesebreite.
|
||||||
|
========================================== */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.detail-content {
|
||||||
|
max-width: 680px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
padding-left: calc(50% - 340px);
|
||||||
|
padding-right: calc(50% - 340px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v37.1.1: Entwickler-Kachel im Quick-Info-Raster
|
||||||
|
(Button erbt die Kachel-Optik von .home-info-card, hier nur
|
||||||
|
der Button-Reset + Klick-Feedback) */
|
||||||
|
.home-info-card-btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-info-card-btn:hover,
|
||||||
|
.home-info-card-btn:focus-visible {
|
||||||
|
background: rgba(255, 237, 0, 0.08);
|
||||||
|
border-color: rgba(255, 237, 0, 0.5);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-info-card-btn .home-info-text p span {
|
||||||
|
color: #FFED00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
v37.1.1: Filter als wischbare Chip-Reihe auf Mobile
|
||||||
|
Gilt einheitlich für Abfahrten-, Routenplaner- und Live-Map-Filter.
|
||||||
|
========================================== */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.filter-chips,
|
||||||
|
.livemap-filters {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-bottom: 4px; /* Platz, falls Scrollbar auftaucht */
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chips::-webkit-scrollbar,
|
||||||
|
.livemap-filters::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari */
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn,
|
||||||
|
.transport-filter-btn,
|
||||||
|
.fav-chip {
|
||||||
|
white-space: nowrap; /* Chips brechen nie intern um */
|
||||||
|
min-height: 40px; /* Touch-Ziel */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
v37.2: Favoriten-Chips in der Abfahrten-View
|
||||||
|
(nutzt .filter-chips -> auf Mobile wischbare Reihe wie die Filter)
|
||||||
|
========================================== */
|
||||||
|
.departure-favorites {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
border: 1px solid rgba(255, 237, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fav-chip {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 2px solid #333;
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 999px; /* Pillenform: unterscheidet Stationen von Filtern */
|
||||||
|
font-family: var(--font-stack);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fav-chip:hover,
|
||||||
|
.fav-chip:focus-visible {
|
||||||
|
border-color: #FFED00;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fav-chip.active {
|
||||||
|
background-color: #FFED00;
|
||||||
|
color: #000;
|
||||||
|
border-color: #FFED00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Alle anzeigen" auf der Startseite dezent abheben */
|
||||||
|
.home-favorites-more {
|
||||||
|
border-style: dashed;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// Transitous-Adapter: Normalisierung MOTIS -> HAFAS-Form testen
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const api = fs.readFileSync(path.join(__dirname, '..', 'js', 'api.js'), 'utf8');
|
||||||
|
const adapter = fs.readFileSync(path.join(__dirname, '..', 'js', 'transitous.js'), 'utf8');
|
||||||
|
|
||||||
|
// Minimale Umgebung
|
||||||
|
global.document = { getElementById: () => null, createElement: () => ({ style: {}, setAttribute(){}, prepend(){} }), body: { prepend(){} } };
|
||||||
|
global.localStorage = { _d: {}, getItem(k){ return this._d[k] ?? null; }, setItem(k,v){ this._d[k]=v; }, };
|
||||||
|
global.AbortController = class { constructor(){ this.signal = {}; } abort(){} };
|
||||||
|
|
||||||
|
// MOTIS-Fixtures (Struktur laut OpenAPI v2.10.2)
|
||||||
|
const fixtures = {
|
||||||
|
geocode: [{ type: 'STOP', name: 'S+U Alexanderplatz', id: 'de-DELFI_de:11000:900100003', lat: 52.5215, lon: 13.4112, tokens: [], areas: [], score: 1 }],
|
||||||
|
stoptimes: { stopTimes: [{
|
||||||
|
place: { name: 'S+U Alexanderplatz', stopId: 'de-DELFI_de:11000:900100003', lat: 52.52, lon: 13.41,
|
||||||
|
departure: '2026-07-06T10:05:00+02:00', scheduledDeparture: '2026-07-06T10:03:00+02:00', track: '4' },
|
||||||
|
mode: 'SUBWAY', realTime: true, headsign: 'U Ruhleben', routeShortName: 'U2', tripId: 'trip123',
|
||||||
|
agencyName: 'BVG', cancelled: false, tripCancelled: false
|
||||||
|
}], place: {}, previousPageCursor: '', nextPageCursor: '' },
|
||||||
|
plan: { itineraries: [{
|
||||||
|
duration: 1200, startTime: '2026-07-06T10:00:00+02:00', endTime: '2026-07-06T10:20:00+02:00', transfers: 0, id: 'it1',
|
||||||
|
legs: [
|
||||||
|
{ mode: 'WALK', from: { name: 'Start', lat: 0, lon: 0 }, to: { name: 'S Südkreuz', lat: 0, lon: 0 },
|
||||||
|
startTime: '2026-07-06T10:00:00+02:00', endTime: '2026-07-06T10:04:00+02:00',
|
||||||
|
scheduledStartTime: '2026-07-06T10:00:00+02:00', scheduledEndTime: '2026-07-06T10:04:00+02:00',
|
||||||
|
realTime: false, scheduled: true, duration: 240, legGeometry: {} },
|
||||||
|
{ mode: 'SUBURBAN', from: { name: 'S Südkreuz', track: '2', lat: 0, lon: 0 }, to: { name: 'S+U Alexanderplatz', lat: 0, lon: 0 },
|
||||||
|
startTime: '2026-07-06T10:06:00+02:00', endTime: '2026-07-06T10:20:00+02:00',
|
||||||
|
scheduledStartTime: '2026-07-06T10:05:00+02:00', scheduledEndTime: '2026-07-06T10:19:00+02:00',
|
||||||
|
realTime: true, scheduled: false, duration: 840, routeShortName: 'S2', headsign: 'S Bernau', legGeometry: {} }
|
||||||
|
]
|
||||||
|
}] }
|
||||||
|
};
|
||||||
|
|
||||||
|
function encodePolyline5(points) {
|
||||||
|
let out = '', prevLat = 0, prevLon = 0;
|
||||||
|
const enc = (v) => {
|
||||||
|
let x = v < 0 ? ~(v << 1) : (v << 1), s = '';
|
||||||
|
while (x >= 0x20) { s += String.fromCharCode((0x20 | (x & 0x1f)) + 63); x >>= 5; }
|
||||||
|
return s + String.fromCharCode(x + 63);
|
||||||
|
};
|
||||||
|
for (const [lat, lon] of points) {
|
||||||
|
const la = Math.round(lat * 1e5), lo = Math.round(lon * 1e5);
|
||||||
|
out += enc(la - prevLat) + enc(lo - prevLon);
|
||||||
|
prevLat = la; prevLon = lo;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixtures.mapStops = [
|
||||||
|
{ name: 'U Amrumer Str.', stopId: 'de-DELFI_de:11000:900009202', lat: 52.5423, lon: 13.3495 },
|
||||||
|
{ name: 'S Westhafen', stopId: 'de-DELFI_de:11000:900001201', lat: 52.5360, lon: 13.3430 }
|
||||||
|
];
|
||||||
|
fixtures.trip = { duration: 600, startTime: 'x', endTime: 'x', transfers: 0, id: 't', legs: [{
|
||||||
|
mode: 'SUBWAY', realTime: true, scheduled: true, duration: 600, legGeometry: {},
|
||||||
|
startTime: 'x', endTime: 'x', scheduledStartTime: 'x', scheduledEndTime: 'x',
|
||||||
|
from: { name: 'U Ruhleben', stopId: 'A', scheduledDeparture: '2026-07-06T10:00:00+02:00', departure: '2026-07-06T10:01:00+02:00' },
|
||||||
|
to: { name: 'S+U Pankow', stopId: 'B', scheduledArrival: '2026-07-06T10:40:00+02:00', arrival: '2026-07-06T10:41:00+02:00' },
|
||||||
|
intermediateStops: [
|
||||||
|
{ name: 'U Theodor-Heuss-Platz', stopId: 'C', scheduledDeparture: '2026-07-06T10:05:00+02:00', departure: '2026-07-06T10:06:30+02:00' }
|
||||||
|
]
|
||||||
|
}] };
|
||||||
|
// Segment: 2 Punkte, Fahrt läuft JETZT (Start vor 30s, Ende in 30s)
|
||||||
|
const segNow = Date.now();
|
||||||
|
fixtures.mapTrips = [{
|
||||||
|
trips: [{ tripId: 'RT1', routeShortName: 'M10', displayName: 'M10' }],
|
||||||
|
mode: 'TRAM', distance: 1000,
|
||||||
|
from: { name: 'A', lat: 52.5, lon: 13.3 }, to: { name: 'B Endstation', lat: 52.6, lon: 13.4 },
|
||||||
|
departure: new Date(segNow - 30000).toISOString(),
|
||||||
|
arrival: new Date(segNow + 30000).toISOString(),
|
||||||
|
scheduledDeparture: new Date(segNow - 90000).toISOString(),
|
||||||
|
scheduledArrival: new Date(segNow - 30000).toISOString(),
|
||||||
|
realTime: true,
|
||||||
|
polyline: encodePolyline5([[52.5, 13.3], [52.6, 13.4]])
|
||||||
|
}];
|
||||||
|
|
||||||
|
const requestedUrls = [];
|
||||||
|
global.fetch = async (url) => {
|
||||||
|
requestedUrls.push(url);
|
||||||
|
const body = url.includes('/geocode') ? fixtures.geocode
|
||||||
|
: url.includes('/stoptimes') ? fixtures.stoptimes
|
||||||
|
: url.includes('/plan') ? fixtures.plan
|
||||||
|
: url.includes('/map/stops') ? fixtures.mapStops
|
||||||
|
: url.includes('/map/trips') ? fixtures.mapTrips
|
||||||
|
: url.includes('/trip') ? fixtures.trip : {};
|
||||||
|
return { ok: true, json: async () => body };
|
||||||
|
};
|
||||||
|
|
||||||
|
// api.js-Ausschnitte + Adapter in EINEM Scope evaluieren
|
||||||
|
const combined =
|
||||||
|
api.slice(api.indexOf('const API_SOFT_LIMIT'), api.indexOf('/**'))
|
||||||
|
+ api.slice(api.indexOf('async function apiFetch'), api.indexOf('// ==========================================\n // GLOBALES'))
|
||||||
|
+ api.slice(api.indexOf('function showApiOutageBanner'), api.indexOf('// HTML-Escaping'))
|
||||||
|
+ api.slice(api.indexOf('function storageGet'), api.indexOf('// --- Favoriten ---'))
|
||||||
|
+ adapter
|
||||||
|
+ ';global.__a = { transitousLocations, transitousDepartures, transitousJourneys, motisModeToProduct, productsToMotisModes, transitousNearby, transitousTrip, transitousRadarSegments, segmentPositionAt, decodePolyline5, apiCache };';
|
||||||
|
eval(combined);
|
||||||
|
const A = global.__a;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let fails = 0;
|
||||||
|
const t = (name, ok) => { console.log((ok ? '✅' : '❌'), name); if (!ok) fails++; };
|
||||||
|
|
||||||
|
// 1) Locations-Normalisierung
|
||||||
|
const locs = await A.transitousLocations('alex');
|
||||||
|
t('geocode -> hafas locations', locs[0].type === 'stop'
|
||||||
|
&& locs[0].id === 'transitous:de-DELFI_de:11000:900100003'
|
||||||
|
&& locs[0].name === 'S+U Alexanderplatz'
|
||||||
|
&& locs[0].location.latitude === 52.5215);
|
||||||
|
|
||||||
|
// 2) Departures über Transitous-ID
|
||||||
|
const deps = await A.transitousDepartures({ id: 'transitous:de-DELFI_de:11000:900100003', name: 'Alex' });
|
||||||
|
const d = deps.departures[0];
|
||||||
|
t('stoptimes -> hafas departure (Linie/Richtung)', d.line.name === 'U2' && d.line.product === 'subway' && d.direction === 'U Ruhleben');
|
||||||
|
t('Verspätung korrekt (120s)', d.delay === 120);
|
||||||
|
t('when/plannedWhen/Gleis', d.when.includes('10:05') && d.plannedWhen.includes('10:03') && d.platform === '4');
|
||||||
|
|
||||||
|
// 3) Departures über Koordinaten (kein ID-Mapping nötig)
|
||||||
|
requestedUrls.length = 0;
|
||||||
|
await A.transitousDepartures({ id: '900100003', name: 'Alex', lat: 52.52, lon: 13.41 });
|
||||||
|
t('HAFAS-ID + Koordinaten -> center-Query statt Geocode', requestedUrls.length === 1 && requestedUrls[0].includes('center=52.52,13.41'));
|
||||||
|
|
||||||
|
// 4) Departures nur mit Name (alter Favorit) -> Geocode + Cache
|
||||||
|
A.apiCache.clear();
|
||||||
|
requestedUrls.length = 0;
|
||||||
|
await A.transitousDepartures({ id: '900xyz', name: 'S Südkreuz' });
|
||||||
|
t('Nur Name -> geocode + stoptimes (2 Requests)', requestedUrls.length === 2 && requestedUrls[0].includes('/geocode'));
|
||||||
|
A.apiCache.clear();
|
||||||
|
requestedUrls.length = 0;
|
||||||
|
await A.transitousDepartures({ id: '900xyz', name: 'S Südkreuz' });
|
||||||
|
t('ID-Mapping gecacht (nur noch 1 Request)', requestedUrls.length === 1);
|
||||||
|
|
||||||
|
// 5) Journeys-Normalisierung inkl. Filter und Zeit
|
||||||
|
requestedUrls.length = 0;
|
||||||
|
const j = await A.transitousJourneys(
|
||||||
|
{ id: 'x', name: 'Start', lat: 52.47, lon: 13.36 },
|
||||||
|
{ id: 'y', name: 'Ziel', lat: 52.52, lon: 13.41 },
|
||||||
|
{ timeISO: '2026-07-06T08:00:00.000Z', arriveBy: true, products: new Set(['suburban', 'subway']) }
|
||||||
|
);
|
||||||
|
const leg = j.journeys[0].legs[1];
|
||||||
|
t('plan -> hafas journey (Leg-Felder)', leg.line.name === 'S2' && leg.line.product === 'suburban'
|
||||||
|
&& leg.origin.name === 'S Südkreuz' && leg.departurePlatform === '2');
|
||||||
|
t('Walk-Leg markiert', j.journeys[0].legs[0].walking === true && j.journeys[0].legs[0].line === undefined);
|
||||||
|
const planUrl = requestedUrls.find(u => u.includes('/plan'));
|
||||||
|
t('Koordinaten als fromPlace', planUrl.includes('fromPlace=52.47%2C13.36'));
|
||||||
|
t('arriveBy + Zeit in URL', planUrl.includes('arriveBy=true') && planUrl.includes('time='));
|
||||||
|
t('transitModes mit Fähre', planUrl.includes('SUBURBAN') && planUrl.includes('SUBWAY') && planUrl.includes('FERRY') && !planUrl.includes('TRAM'));
|
||||||
|
|
||||||
|
// 6) Nearby: map/stops -> sortiert nach Distanz
|
||||||
|
const near = await A.transitousNearby(52.5423, 13.3495);
|
||||||
|
t('Nearby: nächste Station zuerst + Distanz', near[0].name === 'U Amrumer Str.' && near[0].distance < near[1].distance);
|
||||||
|
|
||||||
|
// 7) Trip-Details: stopovers in hafas-Form
|
||||||
|
const tripRes = await A.transitousTrip('transitous:RT1');
|
||||||
|
const so = tripRes.trip.stopovers;
|
||||||
|
t('Trip: from + intermediate + to = 3 Stopovers', so.length === 3 && so[1].stop.name === 'U Theodor-Heuss-Platz');
|
||||||
|
t('Trip: Verspätung am Zwischenhalt (90s)', so[1].departureDelay === 90);
|
||||||
|
|
||||||
|
// 8) Polyline-Decoder (Roundtrip-Referenzwerte)
|
||||||
|
const pts = A.decodePolyline5('_p~iF~ps|U_ulLnnqC_mqNvxq`@');
|
||||||
|
t('Polyline-Decoder (Google-Referenzbeispiel)',
|
||||||
|
Math.abs(pts[0][0] - 38.5) < 1e-9 && Math.abs(pts[0][1] + 120.2) < 1e-9 && pts.length === 3);
|
||||||
|
|
||||||
|
// 9) Radar-Segmente + Interpolation: Fahrt zur Hälfte -> Mittelpunkt
|
||||||
|
const segs = await A.transitousRadarSegments({ north: 52.7, south: 52.4, west: 13.2, east: 13.6 }, 12);
|
||||||
|
t('Radar: Segment normalisiert', segs.length === 1 && segs[0].lineName === 'M10' && segs[0].product === 'tram');
|
||||||
|
const pos = A.segmentPositionAt(segs[0], Date.now());
|
||||||
|
t('Interpolation: Position ~Mittelpunkt der Strecke',
|
||||||
|
pos && Math.abs(pos.latitude - 52.55) < 0.01 && Math.abs(pos.longitude - 13.35) < 0.01);
|
||||||
|
t('Interpolation: außerhalb des Zeitfensters -> null',
|
||||||
|
A.segmentPositionAt(segs[0], Date.now() + 120000) === null);
|
||||||
|
|
||||||
|
console.log(fails === 0 ? '\n🎉 TRANSITOUS-ADAPTER-TESTS BESTANDEN' : `\n❌ ${fails} fehlgeschlagen`);
|
||||||
|
process.exit(fails ? 1 : 0);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { JSDOM } = require('jsdom');
|
||||||
|
|
||||||
|
const dir = path.join(__dirname, '..');
|
||||||
|
const html = fs.readFileSync(path.join(dir, 'index.html'), 'utf8');
|
||||||
|
const moduleFiles = ['js/api.js', 'js/transitous.js', 'js/app.js', 'js/livemap.js', 'js/changelog.js', 'js/extras.js'];
|
||||||
|
|
||||||
|
const cleanHtml = html
|
||||||
|
.replace(/<script[^>]*src=[^>]*><\/script>/g, '')
|
||||||
|
.replace(/<link[^>]*>/g, '');
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const dom = new JSDOM(cleanHtml, {
|
||||||
|
url: 'https://localhost:3000/',
|
||||||
|
runScripts: 'outside-only',
|
||||||
|
pretendToBeVisual: true
|
||||||
|
});
|
||||||
|
const { window } = dom;
|
||||||
|
window.addEventListener('error', (e) => errors.push('window.error: ' + e.message));
|
||||||
|
|
||||||
|
// --- Mocks ---
|
||||||
|
let lastJourneyUrl = null;
|
||||||
|
window.fetch = (url) => {
|
||||||
|
const u = String(url);
|
||||||
|
if (u.includes('/plan')) lastJourneyUrl = u;
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(
|
||||||
|
u.includes('/geocode') ? []
|
||||||
|
: u.includes('/stoptimes') ? { stopTimes: [], place: {} }
|
||||||
|
: u.includes('/plan') ? { itineraries: [] }
|
||||||
|
: u.includes('/map/') ? []
|
||||||
|
: {}
|
||||||
|
),
|
||||||
|
text: () => Promise.resolve('# Changelog\n## v35')
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.navigator.vibrate = () => true;
|
||||||
|
const mm = () => ({ matches: false, addEventListener(){}, removeEventListener(){}, addListener(){}, removeListener(){} });
|
||||||
|
window.matchMedia = mm;
|
||||||
|
const fakeLayer = {
|
||||||
|
addTo: () => fakeLayer, remove: () => {}, bindPopup: () => fakeLayer,
|
||||||
|
on: () => fakeLayer, setLatLng: () => fakeLayer, setPopupContent: () => fakeLayer
|
||||||
|
};
|
||||||
|
window.L = {
|
||||||
|
polyline: () => fakeLayer,
|
||||||
|
map: () => ({
|
||||||
|
setView(){ return this; }, on(){ return this; }, invalidateSize(){}, getZoom: () => 12,
|
||||||
|
getBounds: () => ({ getNorth:()=>52.6, getSouth:()=>52.4, getEast:()=>13.6, getWest:()=>13.2 })
|
||||||
|
}),
|
||||||
|
tileLayer: () => fakeLayer, marker: () => fakeLayer, divIcon: () => ({})
|
||||||
|
};
|
||||||
|
Object.defineProperty(window.navigator, 'serviceWorker', {
|
||||||
|
value: { register: () => Promise.resolve({}) }, configurable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Module in Ladereihenfolge ausführen ---
|
||||||
|
// Verkettet ausgeführt: entspricht dem gemeinsamen globalen Scope
|
||||||
|
// klassischer <script>-Tags. Test-Hook (__t) nutzt direktes eval,
|
||||||
|
// um auf den Modul-Scope zugreifen zu können.
|
||||||
|
const combined = moduleFiles
|
||||||
|
.map(f => fs.readFileSync(path.join(dir, f), 'utf8'))
|
||||||
|
.join('\n;\n') + '\n;window.__t = (c) => eval(c);';
|
||||||
|
try {
|
||||||
|
window.eval(combined);
|
||||||
|
console.log('✅ Alle 5 Module geladen (verketteter Scope)');
|
||||||
|
} catch (e) {
|
||||||
|
errors.push('Module: ' + e.message);
|
||||||
|
console.log('❌ Module:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.document.dispatchEvent(new window.Event('DOMContentLoaded', { bubbles: true }));
|
||||||
|
console.log('✅ DOMContentLoaded ohne Fehler');
|
||||||
|
} catch (e) {
|
||||||
|
errors.push('DOMContentLoaded: ' + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const doc = window.document;
|
||||||
|
|
||||||
|
// 1) Views durchklicken
|
||||||
|
doc.querySelectorAll('.nav-item').forEach(item => {
|
||||||
|
item.click();
|
||||||
|
const target = doc.getElementById('view-' + item.dataset.view);
|
||||||
|
if (!target?.classList.contains('active')) errors.push('View kaputt: ' + item.dataset.view);
|
||||||
|
});
|
||||||
|
console.log('✅ Alle 5 Views schaltbar');
|
||||||
|
|
||||||
|
// 2) Favoriten: toggle + Rendering auf Home
|
||||||
|
window.__t(`
|
||||||
|
currentStationId = '900000100003';
|
||||||
|
currentStationName = 'S+U Alexanderplatz';
|
||||||
|
toggleFavorite(currentStationId, currentStationName);
|
||||||
|
`);
|
||||||
|
const favItems = doc.querySelectorAll('.home-favorite-item');
|
||||||
|
if (favItems.length !== 1) errors.push('Favorit nicht gerendert');
|
||||||
|
else console.log('✅ Favorit gespeichert & auf Home gerendert:', favItems[0].dataset.name);
|
||||||
|
|
||||||
|
// 3) Persistenz: localStorage-Inhalte prüfen
|
||||||
|
const stored = JSON.parse(window.localStorage.getItem('vbb_favorites') || '[]');
|
||||||
|
if (stored.length !== 1 || stored[0].name !== 'S+U Alexanderplatz') errors.push('localStorage Favoriten falsch');
|
||||||
|
else console.log('✅ localStorage enthält Favorit');
|
||||||
|
|
||||||
|
// 4) Favorit-Klick -> Abfahrten-View + Station gesetzt
|
||||||
|
favItems[0]?.click();
|
||||||
|
if (!doc.getElementById('view-departures').classList.contains('active')) errors.push('Favorit-Klick öffnet Abfahrten nicht');
|
||||||
|
else console.log('✅ Favorit-Klick öffnet Abfahrten-View');
|
||||||
|
|
||||||
|
// 5) Journey: Filter + Zeitwahl in der URL
|
||||||
|
window.__t(`
|
||||||
|
journeyFromStation = { id: 'A1', name: 'Start', lat: 52.47, lon: 13.36 };
|
||||||
|
journeyToStation = { id: 'B2', name: 'Ziel', lat: 52.52, lon: 13.41 };
|
||||||
|
activeTransportModes.delete('bus');
|
||||||
|
document.getElementById('journeyTimeInput').value = '2026-07-04T10:30';
|
||||||
|
document.getElementById('journeyTimeMode').value = 'arrival';
|
||||||
|
document.getElementById('searchJourneyBtn').disabled = false;
|
||||||
|
`);
|
||||||
|
doc.getElementById('searchJourneyBtn').click();
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (!lastJourneyUrl) errors.push('Journey-Suche feuerte nicht');
|
||||||
|
else {
|
||||||
|
if (lastJourneyUrl.includes('BUS') || !lastJourneyUrl.includes('SUBWAY')) errors.push('Transport-Filter fehlt/falsch in plan-URL');
|
||||||
|
else console.log('✅ Verkehrsmittel-Filter in plan-URL (ohne BUS)');
|
||||||
|
if (!lastJourneyUrl.includes('arriveBy=true') || !lastJourneyUrl.includes('time=')) errors.push('Zeitwahl fehlt in plan-URL');
|
||||||
|
else console.log('✅ Zeitwahl in plan-URL (arriveBy=true)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Offline-Cache: Abfahrten-Cache in localStorage nach Fehler nutzbar?
|
||||||
|
const cacheOk = window.__t(`
|
||||||
|
saveDeparturesCache('TEST', [{when: new Date().toISOString(), line: {name: 'U2'}}]);
|
||||||
|
const c = loadDeparturesCache('TEST');
|
||||||
|
c && c.departures.length === 1;
|
||||||
|
`);
|
||||||
|
if (!cacheOk) errors.push('Offline-Cache defekt');
|
||||||
|
else console.log('✅ Offline-Abfahrten-Cache funktioniert');
|
||||||
|
|
||||||
|
// 7) Totalausfall: beide APIs down -> Banner erscheint
|
||||||
|
const origFetch = window.fetch;
|
||||||
|
window.fetch = () => Promise.reject(new TypeError('NetworkError'));
|
||||||
|
window.__t(`apiCache.clear();`);
|
||||||
|
window.__t(`apiFetch('https://v6.vbb.transport.rest/locations?query=test&results=1', { ttl: 0 }).catch(() => {})`);
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
const banner = doc.getElementById('apiOutageBanner');
|
||||||
|
if (!banner || banner.style.display === 'none') errors.push('Outage-Banner erscheint nicht');
|
||||||
|
else console.log('✅ Totalausfall beider APIs -> Störungs-Banner erscheint');
|
||||||
|
|
||||||
|
// 8) API erholt sich -> Banner verschwindet
|
||||||
|
window.fetch = () => Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||||
|
window.__t(`apiFetch('https://v6.vbb.transport.rest/locations?query=test2&results=1', { ttl: 0 }).catch(() => {})`);
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
if (doc.getElementById('apiOutageBanner').style.display !== 'none') errors.push('Banner verschwindet nicht nach Erholung');
|
||||||
|
else console.log('✅ API-Erholung -> Banner verschwindet automatisch');
|
||||||
|
window.fetch = origFetch;
|
||||||
|
|
||||||
|
// 9) Abfahrten über Transitous (einzige Datenquelle)
|
||||||
|
window.fetch = (url) => {
|
||||||
|
const u = String(url);
|
||||||
|
if (u.includes('api.transitous.org') && u.includes('/stoptimes')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ stopTimes: [{
|
||||||
|
place: { name: 'Testhalt', departure: '2026-07-06T10:05:00+02:00',
|
||||||
|
scheduledDeparture: '2026-07-06T10:03:00+02:00', track: '1' },
|
||||||
|
mode: 'TRAM', headsign: 'Testziel', routeShortName: 'M10',
|
||||||
|
tripId: 'tt1', realTime: true, cancelled: false, tripCancelled: false
|
||||||
|
}], place: {} }) });
|
||||||
|
}
|
||||||
|
return Promise.reject(new TypeError('NetworkError'));
|
||||||
|
};
|
||||||
|
window.__t(`
|
||||||
|
apiCache.clear();
|
||||||
|
currentStationId = '900100003'; currentStationName = 'Teststation';
|
||||||
|
currentStationLat = 52.52; currentStationLon = 13.41;
|
||||||
|
`);
|
||||||
|
window.__t('loadDepartures()');
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
const depHtml = doc.getElementById('departuresContainer').innerHTML;
|
||||||
|
if (!depHtml.includes('M10') || !depHtml.includes('Testziel')) {
|
||||||
|
errors.push('Abfahrten über Transitous werden nicht gerendert');
|
||||||
|
} else console.log('✅ Abfahrten kommen über Transitous (stoptimes -> Renderer)');
|
||||||
|
|
||||||
|
// 10) Stationssuche über Transitous (geocode)
|
||||||
|
window.__t(`apiCache.clear();`);
|
||||||
|
window.fetch = (url) => {
|
||||||
|
const u = String(url);
|
||||||
|
if (u.includes('/geocode')) return Promise.resolve({ ok: true, json: () => Promise.resolve([
|
||||||
|
{ type: 'STOP', name: 'Amrumer Str.', id: 'de-DELFI_de:11000:900009202', lat: 52.542, lon: 13.349, tokens: [], areas: [], score: 1 }
|
||||||
|
]) });
|
||||||
|
return Promise.reject(new TypeError('NetworkError'));
|
||||||
|
};
|
||||||
|
doc.getElementById('stationSearch').value = 'Amrumer';
|
||||||
|
doc.getElementById('stationSearch').dispatchEvent(new window.Event('input', { bubbles: true }));
|
||||||
|
await new Promise(r => setTimeout(r, 600));
|
||||||
|
const sugHtml = doc.getElementById('suggestions').innerHTML;
|
||||||
|
if (!sugHtml.includes('Amrumer')) errors.push('Suche über Transitous liefert keine Vorschläge');
|
||||||
|
else console.log('✅ Stationssuche über Transitous (geocode -> Vorschläge)');
|
||||||
|
window.fetch = origFetch;
|
||||||
|
|
||||||
|
// 11) Abfahrten-Filter: filtert wirklich + Auto-Refresh respektiert Filter
|
||||||
|
window.__t(`
|
||||||
|
allDepartures = [];
|
||||||
|
displayDepartures([
|
||||||
|
{ tripId: 't1', direction: 'A', when: new Date(Date.now()+300000).toISOString(), line: { name: 'U2', product: 'subway' } },
|
||||||
|
{ tripId: 't2', direction: 'B', when: new Date(Date.now()+400000).toISOString(), line: { name: '100', product: 'bus' } }
|
||||||
|
]);
|
||||||
|
`);
|
||||||
|
const filterBar = doc.getElementById('departureFilters');
|
||||||
|
if (filterBar.style.display === 'none') errors.push('Filterleiste erscheint nicht');
|
||||||
|
else console.log('✅ Filterleiste erscheint bei Abfahrten');
|
||||||
|
|
||||||
|
doc.querySelector('.departure-filters .filter-btn[data-filter="subway"]').click();
|
||||||
|
await new Promise(r => setTimeout(r, 50));
|
||||||
|
const shown = doc.querySelectorAll('#departuresContainer .departure-item');
|
||||||
|
const onlySubway = shown.length === 1 && shown[0].dataset.tripId === 't1';
|
||||||
|
if (!onlySubway) errors.push('Abfahrten-Filter filtert nicht (U-Bahn-Filter zeigt ' + shown.length + ' Items)');
|
||||||
|
else console.log('✅ Abfahrten-Filter: U-Bahn-Filter zeigt nur U2');
|
||||||
|
|
||||||
|
// Mehr-laden-Button vorhanden?
|
||||||
|
if (!doc.getElementById('loadMoreDepartures')) errors.push('Mehr-laden-Button fehlt');
|
||||||
|
else console.log('✅ "Mehr Abfahrten laden"-Button gerendert');
|
||||||
|
|
||||||
|
// 12) Versions-Konsistenz: APP_VERSION == package.json == SW-Cache + Anzeigen befüllt
|
||||||
|
const pkgVersion = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
|
||||||
|
const appVersion = window.__t('APP_VERSION');
|
||||||
|
const swSrc = fs.readFileSync(path.join(dir, 'service-worker.js'), 'utf8');
|
||||||
|
if (appVersion !== pkgVersion) errors.push(`Version inkonsistent: APP_VERSION=${appVersion} vs package.json=${pkgVersion}`);
|
||||||
|
else if (!swSrc.includes(`vbb-status-v${appVersion}`)) errors.push('SW-Cache-Name passt nicht zu APP_VERSION');
|
||||||
|
else console.log('✅ Version konsistent überall:', appVersion);
|
||||||
|
const hv = doc.getElementById('homeVersion');
|
||||||
|
if (!hv || hv.textContent !== 'v' + appVersion) errors.push('homeVersion nicht befüllt');
|
||||||
|
else console.log('✅ Versions-Anzeigen aus APP_VERSION befüllt');
|
||||||
|
|
||||||
|
// 13) Favoriten-Chips in der Abfahrten-View + kompakte Startseite
|
||||||
|
window.__t(`
|
||||||
|
localStorage.setItem('vbb_favorites', JSON.stringify([
|
||||||
|
{ id: 'f1', name: 'Station Eins' }, { id: 'f2', name: 'Station Zwei' },
|
||||||
|
{ id: 'f3', name: 'Station Drei' }, { id: 'f4', name: 'Station Vier' }
|
||||||
|
]));
|
||||||
|
currentStationId = 'f2';
|
||||||
|
renderFavorites();
|
||||||
|
`);
|
||||||
|
const chips = doc.querySelectorAll('#departureFavorites .fav-chip');
|
||||||
|
const activeChip = doc.querySelector('#departureFavorites .fav-chip.active');
|
||||||
|
if (chips.length !== 4) errors.push(`Abfahrten-Chips: ${chips.length} statt 4`);
|
||||||
|
else if (!activeChip || activeChip.dataset.id !== 'f2') errors.push('Aktiver Chip nicht markiert');
|
||||||
|
else console.log('✅ Favoriten-Chips in Abfahrten-View (4 Chips, aktive markiert)');
|
||||||
|
|
||||||
|
const homeItems = doc.querySelectorAll('#homeFavorites .home-favorite-item:not(.home-favorites-more)');
|
||||||
|
const moreBtn = doc.getElementById('homeShowAllFavs');
|
||||||
|
if (homeItems.length !== 3 || !moreBtn) errors.push('Startseite nicht kompakt (Top 3 + Alle anzeigen)');
|
||||||
|
else console.log('✅ Startseite kompakt: Top 3 + "Alle anzeigen"-Button');
|
||||||
|
|
||||||
|
console.log('✅ Struktur-Checks abgeschlossen');
|
||||||
|
|
||||||
|
if (errors.length === 0) {
|
||||||
|
console.log('\n🎉 ALLE TESTS BESTANDEN');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log('\n❌ FEHLER:');
|
||||||
|
errors.forEach(e => console.log(' -', e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 300);
|
||||||
Reference in new issue
Block a user