From 97a156ce8fa0304af9d005a624d29c62d2573ba6 Mon Sep 17 00:00:00 2001
From: Serverlele30 <46974281+Serverlele30@users.noreply.github.com>
Date: Fri, 10 Jul 2026 11:34:25 +0200
Subject: [PATCH] Add files via upload
---
CHANGELOG.md | 21 ++++++
index.html | 12 +++-
js/api.js | 120 ++++++++++++++++++++++++++++++-
js/app.js | 77 ++++++++++++++++++--
js/extras.js | 23 +++++-
js/livemap.js | 163 +++++++++++++++++++++++++++++++++++++++++-
js/transitous.js | 60 +++++++++++++---
package.json | 2 +-
service-worker.js | 2 +-
styles.css | 143 ++++++++++++++++++++++++++++++++++++
tests/adapter.test.js | 49 +++++++++++--
tests/smoke.test.js | 20 ++++++
12 files changed, 666 insertions(+), 26 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9ba43f..d7efd6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,27 @@ 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/).
+## [37.4.0] - 2026-07-08
+
+### Favoriten-Routen, Standort auf der Karte, smarterer Geocode
+
+#### Added
+- **Favoriten-Routen für Pendler**: Nach einer Suche merkt "☆ Route merken" die Verbindung (max. 5). Gespeicherte Routen erscheinen als Chips über dem Routenplaner-Formular - 1 Tap füllt Start/Ziel und sucht sofort. Chips zeigen Kurznamen ("Alexanderplatz → Zoolog. Garten" statt der vollen Stationsnamen).
+- **Eigener Standort auf der Live-Map**: 📍-Button in der Filterleiste setzt einen blauen Punkt auf die eigene Position und zentriert die Karte (Marker wird bei erneutem Tap bewegt, nicht neu erzeugt).
+- **Geocode-Bias per GPS**: Die Stationssuche sortiert Treffer jetzt nach der letzten bekannten Position (aus GPS-Nutzung, max. 1h alt, lokal gespeichert) statt hart nach Berlin-Mitte - spürbar bessere Vorschläge in Brandenburg.
+
+#### Fixed
+- **localStorage räumt jetzt auf**: Abfahrten-Caches älter als 7 Tage und Geocode-Mappings älter als 30 Tage werden beim App-Start gelöscht (wuchsen bisher unbegrenzt). Alte Mapping-Einträge werden automatisch ins neue Format mit Zeitstempel migriert.
+
+## [37.3.0] - 2026-07-07
+
+### Blättern, Zwischenhalte, Stationen auf der Karte
+
+#### Added
+- **"Frühere / Spätere Verbindungen"**: Die Routensuche blättert jetzt per pageCursor durch Alternativverbindungen - Buttons oberhalb und unterhalb der Ergebnisliste, mit sanftem Scroll nach oben beim Blättern.
+- **Zwischenhalte ausklappbar**: In der Routen-Detailansicht zeigt jeder Fahrt-Abschnitt "🔄 N Zwischenhalte anzeigen" (natives details-Element, tastaturbedienbar) mit Haltestellenliste inkl. Ankunftszeiten. Die Daten kamen schon immer mit der plan-Antwort - der alte Platzhalter ("N Halte") bekam sie nur nie.
+- **Stationen auf der Live-Map**: Ab Zoom 15 erscheinen die Haltestellen im Ausschnitt als gelbe Punkte (map/stops, 5min-Cache). Tap -> Popup mit den nächsten 3 Abfahrten (1 Request, 15s-Cache) und "Alle Abfahrten →" springt direkt in die Abfahrten-View mit dieser Station. Im Fokus-Modus werden die Punkte automatisch ausgeblendet.
+
## [37.2.0] - 2026-07-07
### Favoriten-Chips in der Abfahrten-View
diff --git a/index.html b/index.html
index 2c3fc35..301ad17 100644
--- a/index.html
+++ b/index.html
@@ -132,13 +132,14 @@
Fahrzeuge in Echtzeit verfolgen
-
+
+
@@ -205,6 +206,10 @@
+
+
+
@@ -300,6 +305,9 @@
🚇 Lade Fahrzeuge...
+
diff --git a/js/api.js b/js/api.js
index acbe57e..d9587c0 100644
--- a/js/api.js
+++ b/js/api.js
@@ -7,7 +7,7 @@
// 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_VERSION = '37.4.0';
const APP_RELEASE_DATE = '07.07.2026';
// Seit v35: Transitous (api.transitous.org) ist die einzige
@@ -25,6 +25,8 @@
// später geladenen Dateien angewiesen ist.
let currentView = 'home';
let departuresCount = 20; // Wie viele Abfahrten laden ("Mehr laden" erhöht)
+ let lastKnownLat = null; // Letzte bekannte GPS-Position
+ let lastKnownLon = null; // (für Geocode-Bias + Karten-Zentrierung)
let currentStationLat = null; // Koordinaten der gewählten Station
let currentStationLon = null; // (für Transitous-Fallback ohne ID-Mapping)
let liveMap = null;
@@ -203,9 +205,12 @@
// ==========================================
const STORAGE_KEYS = {
favorites: 'vbb_favorites',
+ favRoutes: 'vbb_fav_routes',
lastStation: 'vbb_last_station',
lastJourney: 'vbb_last_journey',
- depCachePrefix: 'vbb_dep_cache_'
+ lastPosition: 'vbb_last_position',
+ depCachePrefix: 'vbb_dep_cache_',
+ stopMapPrefix: 'vbb_transitous_stop_'
};
function storageGet(key, fallback = null) {
@@ -335,6 +340,117 @@
btn.title = fav ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen';
}
+ // --- Routen-Favoriten (Pendler: 1-Tap-Suche) ---
+ const MAX_FAV_ROUTES = 5;
+
+ function getFavRoutes() {
+ return storageGet(STORAGE_KEYS.favRoutes, []);
+ }
+
+ function routeKey(from, to) {
+ return `${from?.id}→${to?.id}`;
+ }
+
+ function isFavRoute(from, to) {
+ const key = routeKey(from, to);
+ return getFavRoutes().some(r => routeKey(r.from, r.to) === key);
+ }
+
+ function toggleFavRoute(from, to) {
+ if (!from || !to) return false;
+ let routes = getFavRoutes();
+ const key = routeKey(from, to);
+ if (routes.some(r => routeKey(r.from, r.to) === key)) {
+ routes = routes.filter(r => routeKey(r.from, r.to) !== key);
+ } else {
+ routes.unshift({ from, to });
+ routes = routes.slice(0, MAX_FAV_ROUTES);
+ }
+ storageSet(STORAGE_KEYS.favRoutes, routes);
+ renderRouteFavorites();
+ return isFavRoute(from, to);
+ }
+
+ // Kurzname für Chips: "S+U Alexanderplatz (Berlin)" -> "Alexanderplatz"
+ function shortStationName(name) {
+ return String(name || '')
+ .replace(/\s*\(Berlin\)\s*/g, '')
+ .replace(/^(S\+U|S|U|Bhf\.?)\s+/i, '')
+ .trim();
+ }
+
+ function renderRouteFavorites() {
+ const container = document.getElementById('journeyFavorites');
+ if (!container) return;
+ const routes = getFavRoutes();
+
+ if (routes.length === 0) {
+ container.innerHTML = '';
+ container.style.display = 'none';
+ return;
+ }
+
+ container.style.display = 'block';
+ container.innerHTML = `
+
⭐ Deine Routen
+
+ ${routes.map((r, i) => `
+
+ `).join('')}
+
+ `;
+ }
+
+ // --- Letzte bekannte Position (für Geocode-Bias) ---
+ function rememberPosition(lat, lon) {
+ lastKnownLat = lat;
+ lastKnownLon = lon;
+ storageSet(STORAGE_KEYS.lastPosition, { lat, lon, time: Date.now() });
+ }
+
+ function restorePosition() {
+ const pos = storageGet(STORAGE_KEYS.lastPosition);
+ // Nur nutzen, wenn jünger als 1 Stunde (sonst evtl. ganz woanders)
+ if (pos && Date.now() - pos.time < 60 * 60 * 1000) {
+ lastKnownLat = pos.lat;
+ lastKnownLon = pos.lon;
+ }
+ }
+
+ // --- localStorage-Aufräumung beim App-Start ---
+ // Abfahrten-Caches und Geocode-Mappings wuchsen bisher unbegrenzt.
+ function cleanupStorage() {
+ const now = Date.now();
+ const WEEK = 7 * 24 * 60 * 60 * 1000;
+ const MONTH = 30 * 24 * 60 * 60 * 1000;
+ const toDelete = [];
+
+ try {
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key.startsWith(STORAGE_KEYS.depCachePrefix)) {
+ const entry = storageGet(key);
+ if (!entry || !entry.time || now - entry.time > WEEK) toDelete.push(key);
+ } else if (key.startsWith(STORAGE_KEYS.stopMapPrefix)) {
+ const entry = storageGet(key);
+ // Altes Format (reiner String, ohne Zeitstempel) -> migrieren
+ if (typeof entry === 'string') {
+ storageSet(key, { id: entry, time: now });
+ } else if (!entry || !entry.time || now - entry.time > MONTH) {
+ toDelete.push(key);
+ }
+ }
+ }
+ toDelete.forEach(k => localStorage.removeItem(k));
+ if (toDelete.length > 0) {
+ console.warn(`Storage-Aufräumung: ${toDelete.length} alte Einträge entfernt`);
+ }
+ } catch (e) { /* localStorage nicht verfügbar */ }
+ }
+
// --- Abfahrten-Cache für Offline-Modus ---
function saveDeparturesCache(stationId, departures) {
storageSet(STORAGE_KEYS.depCachePrefix + stationId, {
diff --git a/js/app.js b/js/app.js
index ab8f348..0171dd0 100644
--- a/js/app.js
+++ b/js/app.js
@@ -382,6 +382,7 @@
});
const { latitude, longitude } = position.coords;
+ rememberPosition(latitude, longitude); // für Geocode-Bias merken
stationSearch.placeholder = '📍 Suche Stationen in der Nähe...';
@@ -1229,6 +1230,7 @@
});
const { latitude, longitude } = position.coords;
+ rememberPosition(latitude, longitude); // für Geocode-Bias merken
const locations = await transitousNearby(latitude, longitude);
const stations = locations.filter(loc =>
@@ -1271,6 +1273,13 @@
return;
}
+ runJourneySearch();
+ });
+
+ // Cursor der letzten Suche (für "Früher"/"Später"-Blättern)
+ let journeyPageCursors = { prev: null, next: null };
+
+ async function runJourneySearch(pageCursor = null) {
journeyContainer.innerHTML = '