From a976642183dfee96fee69e55eb58db2472854bb1 Mon Sep 17 00:00:00 2001 From: Serverlele30 <46974281+Serverlele30@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:51:09 +0200 Subject: [PATCH] Add files via upload --- CHANGELOG.md | 73 +++++++++ index.html | 14 +- js/api.js | 166 ++++++++++---------- js/app.js | 257 ++++++++++++++----------------- js/extras.js | 51 ++++++- js/livemap.js | 98 +++++++----- js/transitous.js | 379 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- service-worker.js | 24 ++- styles.css | 169 ++++++++++++++++++++- 10 files changed, 953 insertions(+), 280 deletions(-) create mode 100644 js/transitous.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a6af6..ba3a527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,79 @@ 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/), und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). +## [36.0.0] - 2026-07-06 + +### Schnellere Karte, offizielle Linienfarben, Desktop-Layout + +#### Added - Offizielle VBB-Linienfarben +- **Linienfarben kommen jetzt direkt aus der API**: MOTIS liefert routeColor/routeTextColor aus dem GTFS-Feed bei Abfahrten, Routen UND Live-Map mit - das sind die offiziellen VBB-Farben (U1 grün, U2 rot, S1, S2, ... jeweils korrekt), ohne einen einzigen Extra-Request. Lokale Farbtabellen bleiben nur als Fallback. +- **Textfarbe per Luminanz**: Ob Schwarz oder Weiß auf einem Linien-Badge, wird jetzt aus der Hintergrundfarbe berechnet statt über eine hartcodierte Liste. + +#### Changed - Karten-Ladegeschwindigkeit +- **Vorinitialisierung im Leerlauf**: Nach dem App-Start wird die Leaflet-Karte im Hintergrund angelegt und die 9 Kacheln des Standard-Ausschnitts werden vorgeladen (exakt dieselben URLs inkl. Subdomain-/Retina-Logik wie Leaflet). Beim ersten Öffnen der Live-Map ist alles im Browser-Cache - gefühlt sofort. Kostet keine API-Requests. +- **Preconnect/DNS-Prefetch** für api.transitous.org, Karten-CDN und unpkg: spart DNS+TLS-Zeit beim ersten Zugriff. +- **Leaflet mit defer** geladen: blockiert das Rendern der App nicht mehr. +- **Leaflet-Dateien im Service Worker gecacht** (cache-first, versioniert = unveränderlich): ab dem zweiten Besuch null Netzwerk für die Bibliothek. +- **Tile-Tuning**: keepBuffer 4 (Randkacheln bleiben geladen -> flüssigeres Pannen), Kacheln laden schon während der Bewegung, minZoom 8 (ganz Brandenburg sichtbar). + +#### Removed +- **Zoom-Tasten (+/-) auf der Karte entfernt** (vorher auf Desktop sichtbar). Zoomen weiterhin per Scrollrad, Doppelklick und Pinch-Geste. + +#### Changed - Desktop-Layout (Mobile First bleibt) +- Header-Inhalt und Pull-to-Refresh richten sich am zentrierten Layout aus. +- Home: Info-Karten und Favoriten als 2-Spalten-Raster ab 768px. +- Live-Map bekommt auf dem Desktop deutlich mehr Höhe. +- Ab 1100px: breiterer Inhalt (780px), luftigere Abfahrtszeilen, größere Linien-Badges, breiteres Detail-Modal. +- Konflikt bereinigt: neue Regeln ergänzen den bestehenden 768px-Block (Journey-Grid, Header-Größen) statt ihn zu doppeln. + +## [35.0.0] - 2026-07-06 + +### Transitous als einzige Datenquelle + animierte Live-Map + +#### Breaking / Changed +- **transport.rest komplett entfernt**: Alle Funktionen (Stationssuche, Abfahrten, GPS-Nearby, Trip-Details, Routensuche, Live-Map) laufen jetzt ausschließlich über Transitous (api.transitous.org, MOTIS 2). Der VBB->BVG-Failover und alle HAFAS-Codepfade sind raus - js/api.js ist dadurch deutlich schlanker (nur noch Budget, Cache, Dedupe, Timeout, Störungs-Banner). +- **Datenverarbeitung effizienter**: Fahrt-Segmente werden pro API-Poll EINMAL normalisiert (Polyline einmal dekodiert, Zeiten als ms vorberechnet); Positionen werden daraus lokal interpoliert. Abfahrten-Auto-Refresh aktualisiert Zeiten/Verspätungen in-place im DOM (kein Flackern, Scroll bleibt) und rendert nur bei geänderter Fahrten-Menge komplett neu. + +#### Added - Live-Map 2.0 +- **Flüssig animierte Fahrzeuge**: MOTIS liefert Fahrt-Segmente mit Polyline + Zeiten statt Punkt-Positionen. Ein lokaler 1s-Loop interpoliert die Position linear entlang der Strecke - die Fahrzeuge GLEITEN über die Karte (CSS-Transition passend zum Takt) statt alle 15s zu springen. Kostet null zusätzliche API-Requests. +- **API-Poll auf 30s gedrosselt** (Rücksicht auf das Freiwilligen-Projekt Transitous); die Bewegung kommt aus der lokalen Animation. +- **Berlin-Klemmung der Bounding-Box entfernt**: schnitt bisher Potsdam und ganz Brandenburg ab. +- **Live-Indikator** (pulsierender Punkt) am Fahrzeugzähler. + +#### Added - GUI-Politur +- Tabellarische Ziffern und feste Breite bei Abfahrtszeiten (keine Layout-Sprünge bei "9 min" -> "10 min"). +- Sanftes Einblenden der Vorschlags-Dropdowns, dezenteres Verspätungs-Badge. +- `prefers-reduced-motion` wird respektiert (alle Animationen abschaltbar per Systemeinstellung). + +#### Removed +- Dreistufiger API-Failover (obsolet - eine Quelle), shouldTryTransitous-Wrapper, Transitous-Direktpfade, HAFAS-URL-Bau. + +## [34.0.0] - 2026-07-06 + +### Transitous als dritte, unabhängige Failover-Stufe + +#### Added +- **Transitous-Adapter (js/transitous.js)**: Übersetzt die MOTIS-2-API von https://api.transitous.org in die HAFAS-Form der App. Transitous ist ein community-betriebener Dienst mit KOMPLETT eigener Infrastruktur - unabhängig von transport.rest. Beim Totalausfall von VBB+BVG (wie am 03.07.) übernimmt jetzt Stufe 3 für: Stationssuche (geocode), Abfahrten (stoptimes) und Routensuche (plan, inkl. Verkehrsmittel-Filter und Zeitwahl). +- **Koordinaten-basierte Stationsidentität**: Bei Stationsauswahl werden jetzt Koordinaten mitgespeichert (Suggestions, Favoriten, letzte Station). Damit funktionieren Abfahrten und Routen über Transitous OHNE Stop-ID-Mapping (stoptimes per center=lat,lon; plan per Koordinaten-fromPlace). Alte Favoriten ohne Koordinaten werden einmalig per Name geocodet und das Mapping gecacht. +- **Direktpfade für Transitous-Stationen**: Wird während eines Ausfalls eine Station aus Transitous-Vorschlägen gewählt (ID-Präfix "transitous:"), gehen Abfahrten/Routen direkt an Transitous statt sinnlos die HAFAS-APIs zu fragen. +- **Pflicht-Attribution**: Sichtbarer Link auf transitous.org/sources (inkl. OpenStreetMap-Hinweis) in der Entwickler-View, wie von der Transitous-Nutzungsrichtlinie verlangt. + +#### Ressourcen-Rücksicht (Transitous ist ein Freiwilligen-Projekt) +- Transitous wird NUR kontaktiert, wenn transport.rest (beide Endpoints) nicht antwortet. +- Bewusst KEIN Radar-Polling über Transitous - die Live-Map bleibt zweistufig (VBB->BVG) und zeigt bei Totalausfall die Störungsmeldung. +- 4xx-Fehler und eigenes Rate-Limit lösen keinen Transitous-Versuch aus. + +## [33.2.0] - 2026-07-03 + +### Sichtbare Störungsanzeige bei Anbieter-Totalausfall + +#### Context +Am 03.07.2026 lieferten sowohl v6.vbb.transport.rest als auch der Fallback v6.bvg.transport.rest HTTP 503 - beide laufen auf derselben Infrastruktur des Anbieters transport.rest. Gegen einen Totalausfall des Anbieters kann die App keine Livedaten beschaffen, aber sie zeigt ihn jetzt klar an. + +#### Added +- **Globales Störungs-Banner**: Versagen primäre UND Fallback-API, erscheint oben ein deutlicher Hinweis "Datenanbieter gestört". Das Banner verschwindet automatisch beim nächsten erfolgreichen Request. +- **Fehlerhinweis in der Stationssuche**: Statt stillem Versagen zeigen die Suchvorschläge "Suche derzeit nicht möglich - API nicht erreichbar" (Abfahrten- und Routenplaner-Felder). + ## [33.1.0] - 2026-07-03 ### Automatischer API-Failover diff --git a/index.html b/index.html index d55cd11..d880ed7 100644 --- a/index.html +++ b/index.html @@ -13,13 +13,21 @@ + + + + + + + + - @@ -337,7 +345,8 @@ - + +
Frontend:Vanilla JS, CSS3, HTML5
Maps:Leaflet.js
API:VBB Transport REST v6
API:Transitous (MOTIS 2)
Daten:Transitous-Quellen (inkl. DELFI/VBB & OpenStreetMap)
PWA:Service Worker, Manifest
Server:Node.js HTTPS
@@ -471,6 +480,7 @@ + diff --git a/js/api.js b/js/api.js index ac35ddb..706a38b 100644 --- a/js/api.js +++ b/js/api.js @@ -4,15 +4,9 @@ // VBB NETZ STATUS - Haupt-Script // ========================================== - // Primäre API + baugleicher Fallback (gleicher Betreiber, gleiche - // Routen/Stop-IDs, andere Upstream-Quelle - siehe apiFetch) - const API_BASES = [ - 'https://v6.vbb.transport.rest', - 'https://v6.bvg.transport.rest' - ]; - const API_BASE = API_BASES[0]; // Call-Sites bauen URLs immer mit der primären Basis - let activeApiBaseIndex = 0; // 0 = VBB, 1 = BVG-Fallback - let apiFallbackSince = 0; // Zeitpunkt des Failovers (für Recovery) + // Seit v35: Transitous (api.transitous.org) ist die einzige + // Datenquelle. transport.rest (VBB/BVG) wurde komplett entfernt. + // Die konkreten Endpoints kapselt js/transitous.js. let currentStationId = null; let currentStationName = null; let searchTimeout = null; @@ -24,11 +18,14 @@ // Zentral hier deklariert, damit KEIN Modul auf Variablen aus // später geladenen Dateien angewiesen ist. let currentView = 'home'; + let currentStationLat = null; // Koordinaten der gewählten Station + let currentStationLon = null; // (für Transitous-Fallback ohne ID-Mapping) let liveMap = null; let liveMapUpdateInterval = null; let activeVehicleTypes = new Set(['all']); let debounceTimer = null; - let lastVehicleMovements = []; // Letzte Radar-Daten für lokales Filtern + let liveMapSegments = []; // Fahrt-Segmente von Transitous (für lokale Animation) + let liveMapAnimationInterval = null; // 1s-Loop: Positionen lokal interpolieren const vehicleMarkerMap = new Map(); // tripId -> { marker, lineName } let activeTransportModes = new Set(['subway', 'suburban', 'tram', 'bus', 'regional', 'express']); @@ -70,21 +67,14 @@ * Wirft Error('RATE_LIMIT') wenn Budget aufgebraucht (Cache wird trotzdem genutzt). */ /** - * Zentraler API-Aufruf mit automatischem Failover. + * Zentraler API-Aufruf. + * - Budget: max. 90 Requests/Minute (Selbstschutz gegenüber Transitous) + * - Response-Cache mit TTL, Dedupe paralleler identischer Requests + * - Timeout 12s pro Request + * - Störungs-Banner bei Ausfall, automatisches Ausblenden bei Erholung + * - Bei Fehler oder leerem Budget wird notfalls abgelaufener Cache geliefert * - * Primär: v6.vbb.transport.rest - * Fallback: v6.bvg.transport.rest (gleicher Betreiber, identische - * Routen & Stop-IDs, deckt ebenfalls ganz Berlin/Brandenburg - * ab - nutzt aber eine ANDERE Upstream-Quelle. Fällt VBB - * aus, läuft die App über BVG weiter.) - * - * - Timeout 12s pro Versuch (hängende Requests blockieren nichts) - * - Failover bei Netzwerkfehler, Timeout oder HTTP 5xx - * (NICHT bei 4xx - das ist ein Anfragefehler, kein API-Ausfall) - * - Nach 10 Minuten auf dem Fallback wird die primäre API erneut probiert - * - Cache-Keys basieren auf der primären URL -> Cache übersteht Failover - * - * @param {string} url - vollständige URL (mit API_BASE gebaut) + * @param {string} url - vollständige URL * @param {object} opts - { ttl: Cache-Dauer in ms (0 = kein Cache) } */ async function apiFetch(url, { ttl = 0 } = {}) { @@ -108,73 +98,44 @@ throw new Error('RATE_LIMIT'); } + apiCallTimestamps.push(Date.now()); + const promise = (async () => { - // Recovery: Nach 10 min auf dem Fallback wieder primär versuchen - if (activeApiBaseIndex !== 0 && - Date.now() - apiFallbackSince > 10 * 60 * 1000) { - activeApiBaseIndex = 0; - console.warn('API-Failover: Versuche wieder die primäre VBB-API'); - } + try { + const response = await fetchWithTimeout(url, 12000); - const tryOrder = activeApiBaseIndex === 0 ? [0, 1] : [1, 0]; - let lastError = null; - - for (const idx of tryOrder) { - const realUrl = url.replace(API_BASES[0], API_BASES[idx]); - apiCallTimestamps.push(Date.now()); // jeder echte Versuch zählt - - try { - const response = await fetchWithTimeout(realUrl, 12000); - - if (!response.ok) { - if (response.status >= 500) { - // Serverfehler -> Failover lohnt sich - throw new Error('HTTP ' + response.status); - } - // 4xx: Anfrage ist falsch - andere API würde genauso antworten - const err = new Error('HTTP ' + response.status); - err.noFailover = true; - throw err; - } - - const data = await response.json(); - - // Basis-Wechsel dokumentieren - if (idx !== activeApiBaseIndex) { - activeApiBaseIndex = idx; - if (idx !== 0) { - apiFallbackSince = Date.now(); - console.warn(`API-Failover aktiv: ${API_BASES[idx]} (VBB-API antwortet nicht)`); - } else { - console.warn('API-Failover beendet: primäre VBB-API antwortet wieder'); - } - } - - if (ttl > 0) { - // Cache-Größe begrenzen (ältesten Eintrag entfernen) - if (apiCache.size >= API_CACHE_MAX_ENTRIES) { - const oldestKey = apiCache.keys().next().value; - apiCache.delete(oldestKey); - } - apiCache.set(url, { data, time: Date.now(), ttl }); - } - return data; - - } catch (e) { - if (e.noFailover) throw e; - lastError = e; - // Budget für den zweiten Versuch noch da? - if (apiBudgetLeft() <= 0) break; - // -> nächste Basis probieren + if (!response.ok) { + const err = new Error('HTTP ' + response.status); + // 4xx = Anfragefehler, kein API-Ausfall -> kein Banner + if (response.status >= 500) showApiOutageBanner(); + throw err; } - } - // Beide APIs down: notfalls abgelaufenen Cache liefern - if (cached) { - console.warn('Beide APIs nicht erreichbar – liefere veraltete Cache-Daten'); - return cached.data; + const data = await response.json(); + + if (ttl > 0) { + // Cache-Größe begrenzen (ältesten Eintrag entfernen) + if (apiCache.size >= API_CACHE_MAX_ENTRIES) { + const oldestKey = apiCache.keys().next().value; + apiCache.delete(oldestKey); + } + apiCache.set(url, { data, time: Date.now(), ttl }); + } + hideApiOutageBanner(); // API antwortet -> alles gut + return data; + + } catch (e) { + if (!/^HTTP 4/.test(e.message || '')) { + // Netzwerkfehler/Timeout/5xx -> Störung anzeigen, + // notfalls alten Cache liefern + showApiOutageBanner(); + if (cached) { + console.warn('API nicht erreichbar – liefere veraltete Cache-Daten'); + return cached.data; + } + } + throw e; } - throw lastError || new Error('API nicht erreichbar'); })().finally(() => apiInflight.delete(url)); apiInflight.set(url, promise); @@ -192,6 +153,33 @@ } } + // ========================================== + // GLOBALES STÖRUNGS-BANNER + // Erscheint, wenn primäre UND Fallback-API versagen + // (z.B. Wartung/Ausfall bei Transitous). + // Verschwindet automatisch beim nächsten erfolgreichen Request. + // ========================================== + function showApiOutageBanner() { + let banner = document.getElementById('apiOutageBanner'); + if (!banner) { + banner = document.createElement('div'); + banner.id = 'apiOutageBanner'; + banner.className = 'api-outage-banner'; + banner.setAttribute('role', 'alert'); + banner.innerHTML = + '⚠️ Datenanbieter gestört – Transitous ' + + '(transitous.org) ist aktuell nicht erreichbar. ' + + 'Die App versucht es automatisch weiter.'; + document.body.prepend(banner); + } + banner.style.display = ''; + } + + function hideApiOutageBanner() { + const banner = document.getElementById('apiOutageBanner'); + if (banner) banner.style.display = 'none'; + } + // HTML-Escaping für alle Strings aus der API (XSS-Schutz) function escapeHtml(str) { if (str === null || str === undefined) return ''; @@ -247,7 +235,10 @@ if (favorites.some(f => f.id === stationId)) { favorites = favorites.filter(f => f.id !== stationId); } else { - favorites.unshift({ id: stationId, name: stationName }); + favorites.unshift({ + id: stationId, name: stationName, + lat: currentStationLat, lon: currentStationLon + }); favorites = favorites.slice(0, MAX_FAVORITES); } storageSet(STORAGE_KEYS.favorites, favorites); @@ -274,6 +265,7 @@
${favorites.map(f => `