Add files via upload
This commit is contained in:
1 parent
67a9468142
commit
97a156ce8f
12 files changed
+666
-26
No files matched your search
@@ -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
|
||||
|
||||
+10
-2
@@ -132,13 +132,14 @@
|
||||
<p>Fahrzeuge in Echtzeit verfolgen</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-info-card">
|
||||
<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>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -205,6 +206,10 @@
|
||||
<!-- View: Routenplaner -->
|
||||
<div class="view-container" id="view-journey">
|
||||
<div class="container">
|
||||
<!-- Gespeicherte Routen: 1-Tap-Suche für Pendler -->
|
||||
<div class="departure-favorites" id="journeyFavorites" style="display: none;"
|
||||
role="group" aria-label="Gespeicherte Routen"></div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="journey-inputs">
|
||||
<div class="journey-input-group">
|
||||
@@ -300,6 +305,9 @@
|
||||
<span id="vehicleCount" aria-live="polite">🚇 Lade Fahrzeuge...</span>
|
||||
</div>
|
||||
<div class="livemap-filters">
|
||||
<button class="filter-btn locate-btn" id="locateBtn"
|
||||
aria-label="Karte auf meinen Standort zentrieren"
|
||||
title="Auf meinen Standort zentrieren">📍</button>
|
||||
<button class="filter-btn active" data-type="all">🌐 Alle</button>
|
||||
<button class="filter-btn" data-type="subway">🚇 U-Bahn</button>
|
||||
<button class="filter-btn" data-type="suburban">🚆 S-Bahn</button>
|
||||
|
||||
@@ -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 = `
|
||||
<div class="filter-label">⭐ Deine Routen</div>
|
||||
<div class="filter-chips">
|
||||
${routes.map((r, i) => `
|
||||
<button class="fav-chip route-chip" data-route-index="${i}"
|
||||
aria-label="Route ${escapeHtml(r.from.name)} nach ${escapeHtml(r.to.name)} suchen">
|
||||
${escapeHtml(shortStationName(r.from.name))} → ${escapeHtml(shortStationName(r.to.name))}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// --- 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, {
|
||||
|
||||
@@ -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 = '<div class="loading">⏳ Suche Verbindungen...</div>';
|
||||
|
||||
try {
|
||||
@@ -1280,9 +1289,15 @@
|
||||
const data = await transitousJourneys(journeyFromStation, journeyToStation, {
|
||||
timeISO: (tIn && tIn.value) ? new Date(tIn.value).toISOString() : null,
|
||||
arriveBy: tMode ? tMode.value === 'arrival' : false,
|
||||
products: activeTransportModes
|
||||
products: activeTransportModes,
|
||||
pageCursor
|
||||
});
|
||||
|
||||
journeyPageCursors = {
|
||||
prev: data.previousPageCursor,
|
||||
next: data.nextPageCursor
|
||||
};
|
||||
|
||||
// Route für den nächsten App-Start merken
|
||||
storageSet(STORAGE_KEYS.lastJourney, {
|
||||
from: journeyFromStation,
|
||||
@@ -1302,6 +1317,29 @@
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// "Früher"/"Später"-Blättern (Event-Delegation, Buttons werden
|
||||
// bei jedem Render neu erzeugt)
|
||||
journeyContainer.addEventListener('click', (e) => {
|
||||
// "Route merken"-Toggle
|
||||
const saveBtn = e.target.closest('#saveRouteBtn');
|
||||
if (saveBtn) {
|
||||
const nowFav = toggleFavRoute(journeyFromStation, journeyToStation);
|
||||
saveBtn.textContent = nowFav ? '⭐ Route gemerkt ✓' : '☆ Route merken';
|
||||
saveBtn.classList.toggle('active', nowFav);
|
||||
saveBtn.setAttribute('aria-pressed', String(nowFav));
|
||||
if (navigator.vibrate) navigator.vibrate(10);
|
||||
return;
|
||||
}
|
||||
|
||||
const pagerBtn = e.target.closest('.journey-pager-btn');
|
||||
if (pagerBtn && pagerBtn.dataset.cursor) {
|
||||
if (navigator.vibrate) navigator.vibrate(10);
|
||||
runJourneySearch(pagerBtn.dataset.cursor);
|
||||
// Nach oben scrollen, damit man die neuen Ergebnisse sieht
|
||||
journeyContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
|
||||
function displayJourneys(journeys) {
|
||||
@@ -1317,6 +1355,28 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const favRouteActive = isFavRoute(journeyFromStation, journeyToStation);
|
||||
const saveRouteHTML = `
|
||||
<button class="journey-pager-btn save-route-btn ${favRouteActive ? 'active' : ''}" id="saveRouteBtn"
|
||||
aria-pressed="${favRouteActive}">
|
||||
${favRouteActive ? '⭐ Route gemerkt ✓' : '☆ Route merken'}
|
||||
</button>
|
||||
`;
|
||||
|
||||
const pagerHTML = (pos) => `
|
||||
${pos === 'top' ? saveRouteHTML : ''}
|
||||
<div class="journey-pager journey-pager-${pos}">
|
||||
${journeyPageCursors.prev ? `
|
||||
<button class="journey-pager-btn" data-cursor="${escapeHtml(journeyPageCursors.prev)}">
|
||||
◀ Frühere Verbindungen
|
||||
</button>` : '<span></span>'}
|
||||
${journeyPageCursors.next ? `
|
||||
<button class="journey-pager-btn" data-cursor="${escapeHtml(journeyPageCursors.next)}">
|
||||
Spätere Verbindungen ▶
|
||||
</button>` : '<span></span>'}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const html = journeys.map((journey, journeyIndex) => {
|
||||
const departure = new Date(journey.legs[0].plannedDeparture || journey.legs[0].departure);
|
||||
const arrival = new Date(journey.legs[journey.legs.length - 1].plannedArrival || journey.legs[journey.legs.length - 1].arrival);
|
||||
@@ -1480,7 +1540,7 @@
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
journeyContainer.innerHTML = html;
|
||||
journeyContainer.innerHTML = pagerHTML('top') + html + pagerHTML('bottom');
|
||||
|
||||
// Click-Handler für Journey Results
|
||||
journeyContainer.querySelectorAll('.journey-result').forEach(item => {
|
||||
@@ -1597,9 +1657,16 @@
|
||||
</div>
|
||||
|
||||
${leg.stopovers && leg.stopovers.length > 0 ? `
|
||||
<div class="journey-detail-intermediate">
|
||||
<span style="opacity: 0.6; font-size: 12px;">🔄 ${leg.stopovers.length} Halte</span>
|
||||
</div>
|
||||
<details class="journey-detail-intermediate">
|
||||
<summary>🔄 ${leg.stopovers.length} Zwischenhalte anzeigen</summary>
|
||||
<ul class="intermediate-stops-list">
|
||||
${leg.stopovers.map(s => {
|
||||
const t = s.arrival || s.plannedArrival;
|
||||
const time = t ? new Date(t).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) : '';
|
||||
return `<li><span class="intermediate-time">${time}</span> ${escapeHtml(s.stop?.name || '?')}</li>`;
|
||||
}).join('')}
|
||||
</ul>
|
||||
</details>
|
||||
` : ''}
|
||||
|
||||
<div class="journey-detail-stop">
|
||||
|
||||
+22
-1
@@ -425,8 +425,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
devCard.addEventListener('click', () => switchView('developer'));
|
||||
}
|
||||
|
||||
// Favoriten auf dem Home-Screen anzeigen
|
||||
// Alte Cache-Einträge aufräumen + letzte Position wiederherstellen
|
||||
cleanupStorage();
|
||||
restorePosition();
|
||||
|
||||
// Favoriten (Stationen + Routen) rendern
|
||||
renderFavorites();
|
||||
renderRouteFavorites();
|
||||
|
||||
// Routen-Chip -> Route laden und sofort suchen
|
||||
document.addEventListener('click', (e) => {
|
||||
const chip = e.target.closest('.route-chip');
|
||||
if (!chip) return;
|
||||
const route = getFavRoutes()[parseInt(chip.dataset.routeIndex, 10)];
|
||||
if (!route) return;
|
||||
|
||||
journeyFromStation = route.from;
|
||||
journeyToStation = route.to;
|
||||
journeyFrom.value = route.from.name;
|
||||
journeyTo.value = route.to.name;
|
||||
checkJourneySearchReady();
|
||||
if (navigator.vibrate) navigator.vibrate(10);
|
||||
runJourneySearch();
|
||||
});
|
||||
|
||||
// Letzte Station wiederherstellen (OHNE sofortigen API-Call -
|
||||
// geladen wird erst, wenn die Abfahrten-View geöffnet wird)
|
||||
|
||||
+162
-1
@@ -230,6 +230,7 @@
|
||||
// die lokale Animation zwischenspeichern
|
||||
liveMapSegments = await transitousRadarSegments(bounds, zoom);
|
||||
renderVehiclesFromSegments();
|
||||
updateMapStops(); // Stations-Punkte im selben Takt pflegen
|
||||
|
||||
} catch (error) {
|
||||
console.error('Load vehicles error:', error);
|
||||
@@ -242,6 +243,164 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// EIGENER STANDORT AUF DER KARTE
|
||||
// 📍-Button: blauer Punkt + Karte zentrieren
|
||||
// ==========================================
|
||||
let userLocationMarker = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const locateBtn = document.getElementById('locateBtn');
|
||||
if (!locateBtn) return;
|
||||
|
||||
locateBtn.addEventListener('click', () => {
|
||||
if (!navigator.geolocation || !liveMap) return;
|
||||
locateBtn.textContent = '⏳';
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const { latitude, longitude } = position.coords;
|
||||
rememberPosition(latitude, longitude);
|
||||
|
||||
// Blauer Punkt (wird bei erneutem Tap nur bewegt)
|
||||
if (userLocationMarker) {
|
||||
userLocationMarker.setLatLng([latitude, longitude]);
|
||||
} else {
|
||||
userLocationMarker = L.circleMarker([latitude, longitude], {
|
||||
radius: 8,
|
||||
color: '#FFFFFF',
|
||||
weight: 3,
|
||||
fillColor: '#2A93EE',
|
||||
fillOpacity: 1
|
||||
}).addTo(liveMap);
|
||||
}
|
||||
|
||||
liveMap.setView([latitude, longitude], Math.max(liveMap.getZoom(), 15));
|
||||
locateBtn.textContent = '📍';
|
||||
if (navigator.vibrate) navigator.vibrate(10);
|
||||
},
|
||||
(error) => {
|
||||
console.warn('Standort nicht verfügbar:', error.message);
|
||||
locateBtn.textContent = '📍';
|
||||
const countEl = document.getElementById('vehicleCount');
|
||||
if (countEl) countEl.textContent = '⚠️ Standort nicht verfügbar';
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// STATIONS-PUNKTE AUF DER KARTE (ab Zoom 15)
|
||||
// Tap auf einen Punkt -> Popup mit den nächsten 3 Abfahrten
|
||||
// (1 stoptimes-Request, 15s-Cache) + Sprung zur Abfahrten-View.
|
||||
// ==========================================
|
||||
const STOPS_MIN_ZOOM = 15;
|
||||
let stopMarkerMap = new Map(); // stopId -> marker
|
||||
|
||||
async function updateMapStops() {
|
||||
if (!liveMap) return;
|
||||
|
||||
// Bei niedrigem Zoom oder im Fokus-Modus: Punkte ausblenden
|
||||
const zoom = liveMap.getZoom ? liveMap.getZoom() : 12;
|
||||
if (zoom < STOPS_MIN_ZOOM || focusedVehicleTripId) {
|
||||
clearMapStops();
|
||||
return;
|
||||
}
|
||||
|
||||
const b = liveMap.getBounds();
|
||||
let stops;
|
||||
try {
|
||||
stops = await transitousMapStops({
|
||||
north: b.getNorth(), south: b.getSouth(),
|
||||
west: b.getWest(), east: b.getEast()
|
||||
});
|
||||
} catch (e) {
|
||||
return; // Punkte sind nice-to-have - Fehler still schlucken
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
for (const stop of stops) {
|
||||
seen.add(stop.id);
|
||||
if (stopMarkerMap.has(stop.id)) continue;
|
||||
|
||||
const marker = L.circleMarker([stop.lat, stop.lon], {
|
||||
radius: 6,
|
||||
color: '#FFED00',
|
||||
weight: 2,
|
||||
fillColor: '#111',
|
||||
fillOpacity: 0.9
|
||||
});
|
||||
marker.bindPopup(stopPopupHTML(stop, null), { minWidth: 220 });
|
||||
marker.on('popupopen', () => loadStopPopupDepartures(stop, marker));
|
||||
marker.addTo(liveMap);
|
||||
stopMarkerMap.set(stop.id, marker);
|
||||
}
|
||||
|
||||
// Punkte außerhalb des Ausschnitts entfernen
|
||||
for (const [id, marker] of stopMarkerMap) {
|
||||
if (!seen.has(id)) {
|
||||
marker.remove();
|
||||
stopMarkerMap.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearMapStops() {
|
||||
for (const [, marker] of stopMarkerMap) marker.remove();
|
||||
stopMarkerMap.clear();
|
||||
}
|
||||
|
||||
function stopPopupHTML(stop, departures) {
|
||||
let depsHTML = '<div class="stop-popup-loading">⏳ Lade Abfahrten…</div>';
|
||||
if (departures) {
|
||||
depsHTML = departures.length === 0
|
||||
? '<div class="stop-popup-loading">Keine Abfahrten in Kürze</div>'
|
||||
: departures.map(dep => {
|
||||
const mins = Math.max(0, Math.round((new Date(dep.when) - Date.now()) / 60000));
|
||||
const color = dep.line?.color || getBVGLineColor(dep.line?.name || '');
|
||||
return `
|
||||
<div class="stop-popup-dep">
|
||||
<span class="stop-popup-line" style="background:${color};color:${bestTextColor(color)}">${escapeHtml(dep.line?.name || '?')}</span>
|
||||
<span class="stop-popup-dir">${escapeHtml(dep.direction || '')}</span>
|
||||
<span class="stop-popup-min">${mins === 0 ? 'Jetzt' : mins + ' min'}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
return `
|
||||
<div class="stop-popup">
|
||||
<div class="stop-popup-name">🚏 ${escapeHtml(stop.name)}</div>
|
||||
${depsHTML}
|
||||
<button class="map-stop-open" data-id="${escapeHtml(stop.id)}" data-name="${escapeHtml(stop.name)}"
|
||||
data-lat="${stop.lat}" data-lon="${stop.lon}">
|
||||
Alle Abfahrten →
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function loadStopPopupDepartures(stop, marker) {
|
||||
try {
|
||||
const data = await transitousDepartures(
|
||||
{ id: stop.id, name: stop.name, lat: stop.lat, lon: stop.lon }, 3
|
||||
);
|
||||
marker.setPopupContent(stopPopupHTML(stop, (data.departures || []).slice(0, 3)));
|
||||
} catch (e) {
|
||||
marker.setPopupContent(stopPopupHTML(stop, []));
|
||||
}
|
||||
}
|
||||
|
||||
// "Alle Abfahrten" im Stations-Popup -> Abfahrten-View
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.map-stop-open');
|
||||
if (!btn) return;
|
||||
switchView('departures');
|
||||
selectStation(
|
||||
btn.dataset.id, btn.dataset.name,
|
||||
btn.dataset.lat ? parseFloat(btn.dataset.lat) : null,
|
||||
btn.dataset.lon ? parseFloat(btn.dataset.lon) : null
|
||||
);
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// STRECKEN-ANZEIGE BEIM FAHRZEUG-KLICK
|
||||
// Die Polylines liegen bereits dekodiert in liveMapSegments -
|
||||
@@ -265,8 +424,9 @@
|
||||
}).addTo(liveMap);
|
||||
}
|
||||
|
||||
// Fokus-Modus aktivieren: andere Fahrzeuge ausblenden
|
||||
// Fokus-Modus aktivieren: andere Fahrzeuge + Stations-Punkte ausblenden
|
||||
renderVehiclesFromSegments();
|
||||
clearMapStops();
|
||||
|
||||
// 2) ...dann die KOMPLETTE Route nachladen (map/trips liefert nur
|
||||
// das Segment im Ausschnitt; die ganze Strecke steckt in /trip).
|
||||
@@ -295,6 +455,7 @@
|
||||
if (focusedVehicleTripId) {
|
||||
focusedVehicleTripId = null;
|
||||
renderVehiclesFromSegments(); // alle Fahrzeuge wieder zeigen
|
||||
updateMapStops(); // Stations-Punkte wiederherstellen
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-9
@@ -66,6 +66,15 @@ function motisDelaySeconds(actual, scheduled) {
|
||||
return Number.isFinite(diff) ? Math.round(diff) : null;
|
||||
}
|
||||
|
||||
// Geocode-Bias: letzte bekannte GPS-Position bevorzugen (bessere
|
||||
// Trefferreihenfolge in Brandenburg), sonst Berlin-Mitte als Default
|
||||
function geocodeBias() {
|
||||
if (lastKnownLat != null && lastKnownLon != null) {
|
||||
return `${lastKnownLat.toFixed(3)},${lastKnownLon.toFixed(3)}`;
|
||||
}
|
||||
return '52.52,13.41';
|
||||
}
|
||||
|
||||
// GTFS-Farbe aus der API normalisieren ('DA421E' -> '#DA421E')
|
||||
// Das sind die OFFIZIELLEN VBB-Linienfarben aus dem GTFS-Feed -
|
||||
// kein Extra-Request nötig, sie stecken in stoptimes/plan/map-trips.
|
||||
@@ -95,7 +104,7 @@ async function transitousLocations(query) {
|
||||
// place=Berlin-Mitte als Bias, damit VBB-Gebiet zuerst kommt
|
||||
// 6h Cache: Stationsnamen ändern sich praktisch nie
|
||||
const matches = await transitousFetch(
|
||||
`/v1/geocode?text=${encodeURIComponent(query)}&type=STOP&place=52.52,13.41&numResults=10`,
|
||||
`/v1/geocode?text=${encodeURIComponent(query)}&type=STOP&place=${geocodeBias()}&numResults=10`,
|
||||
6 * 60 * 60 * 1000
|
||||
);
|
||||
|
||||
@@ -119,16 +128,19 @@ async function transitousStopQuery({ id, name, lat, lon }) {
|
||||
return `center=${lat},${lon}&radius=250`;
|
||||
}
|
||||
// Nur Name bekannt (alte Favoriten): einmalig geocoden, Mapping cachen
|
||||
const cacheKey = 'vbb_transitous_stop_' + name;
|
||||
const cacheKey = STORAGE_KEYS.stopMapPrefix + name;
|
||||
const cached = storageGet(cacheKey);
|
||||
if (cached) return 'stopId=' + encodeURIComponent(cached);
|
||||
// Neues Format {id, time}; altes Format (String) wird von
|
||||
// cleanupStorage beim Start migriert, hier zur Sicherheit beides lesen
|
||||
const cachedId = cached && (typeof cached === 'string' ? cached : cached.id);
|
||||
if (cachedId) return 'stopId=' + encodeURIComponent(cachedId);
|
||||
|
||||
const matches = await transitousFetch(
|
||||
`/v1/geocode?text=${encodeURIComponent(name)}&type=STOP&place=52.52,13.41&numResults=1`,
|
||||
`/v1/geocode?text=${encodeURIComponent(name)}&type=STOP&place=${geocodeBias()}&numResults=1`,
|
||||
6 * 60 * 60 * 1000
|
||||
);
|
||||
if (!matches || matches.length === 0) throw new Error('Station bei Transitous nicht gefunden');
|
||||
storageSet(cacheKey, matches[0].id);
|
||||
storageSet(cacheKey, { id: matches[0].id, time: Date.now() });
|
||||
return 'stopId=' + encodeURIComponent(matches[0].id);
|
||||
}
|
||||
|
||||
@@ -181,14 +193,14 @@ async function transitousPlace(station) {
|
||||
return `${station.lat},${station.lon}`;
|
||||
}
|
||||
const matches = await transitousFetch(
|
||||
`/v1/geocode?text=${encodeURIComponent(station.name)}&type=STOP&place=52.52,13.41&numResults=1`,
|
||||
`/v1/geocode?text=${encodeURIComponent(station.name)}&type=STOP&place=${geocodeBias()}&numResults=1`,
|
||||
6 * 60 * 60 * 1000
|
||||
);
|
||||
if (!matches || matches.length === 0) throw new Error('Station bei Transitous nicht gefunden');
|
||||
return matches[0].id;
|
||||
}
|
||||
|
||||
async function transitousJourneys(fromStation, toStation, { timeISO = null, arriveBy = false, products = null } = {}) {
|
||||
async function transitousJourneys(fromStation, toStation, { timeISO = null, arriveBy = false, products = null, pageCursor = null } = {}) {
|
||||
const fromPlace = await transitousPlace(fromStation);
|
||||
const toPlace = await transitousPlace(toStation);
|
||||
|
||||
@@ -197,6 +209,8 @@ async function transitousJourneys(fromStation, toStation, { timeISO = null, arri
|
||||
`&maxTransfers=3&minTransferTime=5`;
|
||||
if (timeISO) url += `&time=${encodeURIComponent(timeISO)}&arriveBy=${arriveBy}`;
|
||||
if (products) url += `&transitModes=${productsToMotisModes(products)}`;
|
||||
// Blättern: Cursor aus der vorherigen Antwort ("Früher"/"Später")
|
||||
if (pageCursor) url += `&pageCursor=${encodeURIComponent(pageCursor)}`;
|
||||
|
||||
// 30s Cache: identische Suche direkt hintereinander = 1 Request
|
||||
const data = await transitousFetch(url, 30000);
|
||||
@@ -220,12 +234,22 @@ async function transitousJourneys(fromStation, toStation, { timeISO = null, arri
|
||||
arrival: leg.endTime,
|
||||
plannedArrival: leg.scheduledEndTime,
|
||||
departurePlatform: leg.from?.track || null,
|
||||
arrivalPlatform: leg.to?.track || null
|
||||
arrivalPlatform: leg.to?.track || null,
|
||||
// Zwischenhalte (kamen bisher mit und wurden weggeworfen)
|
||||
stopovers: (leg.intermediateStops || []).map(s => ({
|
||||
stop: { name: s.name },
|
||||
arrival: s.arrival || s.departure || null,
|
||||
plannedArrival: s.scheduledArrival || s.scheduledDeparture || null
|
||||
}))
|
||||
};
|
||||
})
|
||||
}));
|
||||
|
||||
return { journeys };
|
||||
return {
|
||||
journeys,
|
||||
previousPageCursor: data.previousPageCursor || null,
|
||||
nextPageCursor: data.nextPageCursor || null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -294,6 +318,24 @@ async function transitousTrip(tripId) {
|
||||
return { trip: { stopovers } };
|
||||
}
|
||||
|
||||
// Stationen im Kartenausschnitt (für die Stations-Punkte auf der Live-Map).
|
||||
// Koordinaten gerundet -> stabile Cache-Keys, 5min TTL.
|
||||
async function transitousMapStops(bounds) {
|
||||
const stops = await transitousFetch(
|
||||
`/v6/map/stops?min=${bounds.south.toFixed(3)},${bounds.west.toFixed(3)}` +
|
||||
`&max=${bounds.north.toFixed(3)},${bounds.east.toFixed(3)}`,
|
||||
5 * 60 * 1000
|
||||
);
|
||||
return (stops || [])
|
||||
.filter(s => s.stopId && s.lat != null && s.lon != null)
|
||||
.map(s => ({
|
||||
id: TRANSITOUS_ID_PREFIX + s.stopId,
|
||||
name: s.name,
|
||||
lat: s.lat,
|
||||
lon: s.lon
|
||||
}));
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vbb-netz-status",
|
||||
"version": "37.2.0",
|
||||
"version": "37.4.0",
|
||||
"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",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = 'vbb-status-v37.2.0';
|
||||
const CACHE_NAME = 'vbb-status-v37.4.0';
|
||||
|
||||
// App-Shell: nur Dateien, die wirklich existieren!
|
||||
// (Der alte SW listete eine nicht existierende Font-Datei – dadurch
|
||||
|
||||
+143
@@ -4067,3 +4067,146 @@ select:focus-visible {
|
||||
border-style: dashed;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
v37.3: Blätter-Buttons, Zwischenhalte, Stations-Popups
|
||||
========================================== */
|
||||
|
||||
/* "Früher"/"Später"-Blättern in der Routensuche */
|
||||
.journey-pager {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.journey-pager-btn {
|
||||
background: rgba(255, 237, 0, 0.08);
|
||||
border: 1px dashed rgba(255, 237, 0, 0.4);
|
||||
color: #FFED00;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-family: var(--font-stack);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.journey-pager-btn:hover,
|
||||
.journey-pager-btn:focus-visible {
|
||||
background: rgba(255, 237, 0, 0.15);
|
||||
border-style: solid;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Ausklappbare Zwischenhalte (natives <details>) */
|
||||
.journey-detail-intermediate {
|
||||
margin: 8px 0 8px 12px;
|
||||
}
|
||||
|
||||
.journey-detail-intermediate summary {
|
||||
cursor: pointer;
|
||||
opacity: 0.65;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.journey-detail-intermediate summary:hover,
|
||||
.journey-detail-intermediate summary:focus-visible {
|
||||
opacity: 1;
|
||||
color: #FFED00;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.intermediate-stops-list {
|
||||
list-style: none;
|
||||
margin: 6px 0 4px 6px;
|
||||
padding: 0;
|
||||
border-left: 2px dotted rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.intermediate-stops-list li {
|
||||
padding: 4px 0 4px 12px;
|
||||
font-size: 13px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.intermediate-time {
|
||||
display: inline-block;
|
||||
min-width: 44px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Stations-Popups auf der Live-Map */
|
||||
.stop-popup-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stop-popup-dep {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 3px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stop-popup-line {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stop-popup-dir {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stop-popup-min {
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stop-popup-loading {
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.map-stop-open {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 8px;
|
||||
background: #FFED00;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-stack);
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* v37.4: Route-merken-Button + Standort-Button */
|
||||
.save-route-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.save-route-btn.active {
|
||||
background: rgba(255, 237, 0, 0.2);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.locate-btn {
|
||||
font-size: 15px;
|
||||
}
|
||||
+45
-4
@@ -18,7 +18,7 @@ const fixtures = {
|
||||
mode: 'SUBWAY', realTime: true, headsign: 'U Ruhleben', routeShortName: 'U2', tripId: 'trip123',
|
||||
agencyName: 'BVG', cancelled: false, tripCancelled: false
|
||||
}], place: {}, previousPageCursor: '', nextPageCursor: '' },
|
||||
plan: { itineraries: [{
|
||||
plan: { previousPageCursor: 'CURSOR_PREV', nextPageCursor: 'CURSOR_NEXT', 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 },
|
||||
@@ -28,7 +28,11 @@ const fixtures = {
|
||||
{ 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: {} }
|
||||
realTime: true, scheduled: false, duration: 840, routeShortName: 'S2', headsign: 'S Bernau', legGeometry: {},
|
||||
intermediateStops: [
|
||||
{ name: 'S Yorckstr.', stopId: 'Y1', arrival: '2026-07-06T10:09:00+02:00', scheduledArrival: '2026-07-06T10:08:00+02:00' },
|
||||
{ name: 'S Anhalter Bf', stopId: 'A1', arrival: '2026-07-06T10:12:00+02:00', scheduledArrival: '2026-07-06T10:11:00+02:00' }
|
||||
] }
|
||||
]
|
||||
}] }
|
||||
};
|
||||
@@ -89,12 +93,14 @@ global.fetch = async (url) => {
|
||||
|
||||
// api.js-Ausschnitte + Adapter in EINEM Scope evaluieren
|
||||
const combined =
|
||||
api.slice(api.indexOf('const API_SOFT_LIMIT'), api.indexOf('/**'))
|
||||
'let lastKnownLat = null, lastKnownLon = null;'
|
||||
+ api.slice(api.indexOf('const STORAGE_KEYS'), api.indexOf('function storageGet'))
|
||||
+ 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 };';
|
||||
+ ';global.__a = { transitousLocations, transitousDepartures, transitousJourneys, motisModeToProduct, productsToMotisModes, transitousNearby, transitousTrip, transitousRadarSegments, segmentPositionAt, decodePolyline5, apiCache, transitousMapStops };global.__t = (c) => eval(c);';
|
||||
eval(combined);
|
||||
const A = global.__a;
|
||||
|
||||
@@ -171,6 +177,41 @@ const A = global.__a;
|
||||
t('Interpolation: außerhalb des Zeitfensters -> null',
|
||||
A.segmentPositionAt(segs[0], Date.now() + 120000) === null);
|
||||
|
||||
// 10) Blätter-Cursor: rein in die URL, raus aus der Antwort
|
||||
A.apiCache.clear();
|
||||
requestedUrls.length = 0;
|
||||
const jPage = await A.transitousJourneys(
|
||||
{ id: 'x', name: 'Start', lat: 52.47, lon: 13.36 },
|
||||
{ id: 'y', name: 'Ziel', lat: 52.52, lon: 13.41 },
|
||||
{ pageCursor: 'CURSOR_NEXT' }
|
||||
);
|
||||
const pagedUrl = requestedUrls.find(u => u.includes('/plan'));
|
||||
t('pageCursor in plan-URL', pagedUrl.includes('pageCursor=CURSOR_NEXT'));
|
||||
t('Cursor aus Antwort normalisiert', jPage.previousPageCursor === 'CURSOR_PREV' && jPage.nextPageCursor === 'CURSOR_NEXT');
|
||||
|
||||
// 11) Zwischenhalte im Leg
|
||||
const legWithStops = jPage.journeys[0].legs[1];
|
||||
t('Zwischenhalte gemappt (2 Stück, Name + Zeit)',
|
||||
legWithStops.stopovers.length === 2 &&
|
||||
legWithStops.stopovers[0].stop.name === 'S Yorckstr.' &&
|
||||
legWithStops.stopovers[0].arrival.includes('10:09'));
|
||||
|
||||
// 12) map/stops normalisiert (transitous:-Präfix + Koordinaten)
|
||||
const mapStops = await A.transitousMapStops({ north: 52.55, south: 52.53, west: 13.33, east: 13.36 });
|
||||
t('map/stops -> id-Präfix + Koordinaten',
|
||||
mapStops.length === 2 && mapStops[0].id.startsWith('transitous:') && mapStops[0].lat > 52);
|
||||
|
||||
// 13) Geocode-Bias: ohne Position Berlin-Mitte, mit Position deren Koordinaten
|
||||
A.apiCache.clear();
|
||||
requestedUrls.length = 0;
|
||||
await A.transitousLocations('teststr');
|
||||
t('Bias-Default Berlin-Mitte', requestedUrls[0].includes('place=52.52,13.41'));
|
||||
global.__t('lastKnownLat = 52.401; lastKnownLon = 13.052;'); // Potsdam
|
||||
A.apiCache.clear();
|
||||
requestedUrls.length = 0;
|
||||
await A.transitousLocations('teststr2');
|
||||
t('Bias nutzt letzte GPS-Position', requestedUrls[0].includes('place=52.401,13.052'));
|
||||
|
||||
console.log(fails === 0 ? '\n🎉 TRANSITOUS-ADAPTER-TESTS BESTANDEN' : `\n❌ ${fails} fehlgeschlagen`);
|
||||
process.exit(fails ? 1 : 0);
|
||||
})();
|
||||
@@ -251,6 +251,26 @@ setTimeout(() => {
|
||||
if (homeItems.length !== 3 || !moreBtn) errors.push('Startseite nicht kompakt (Top 3 + Alle anzeigen)');
|
||||
else console.log('✅ Startseite kompakt: Top 3 + "Alle anzeigen"-Button');
|
||||
|
||||
// 14) Routen-Favoriten: merken -> Chip erscheint -> Chip füllt Felder + sucht
|
||||
lastJourneyUrl = null;
|
||||
window.__t(`
|
||||
journeyFromStation = { id: 'rA', name: 'S Pendlerheim (Berlin)', lat: 52.4, lon: 13.2 };
|
||||
journeyToStation = { id: 'rB', name: 'U Arbeitsplatz (Berlin)', lat: 52.5, lon: 13.4 };
|
||||
toggleFavRoute(journeyFromStation, journeyToStation);
|
||||
`);
|
||||
const routeChip = doc.querySelector('#journeyFavorites .route-chip');
|
||||
if (!routeChip) errors.push('Routen-Chip erscheint nicht');
|
||||
else if (!routeChip.textContent.includes('Pendlerheim → Arbeitsplatz')) errors.push('Routen-Chip-Kurzname falsch: ' + routeChip.textContent.trim());
|
||||
else console.log('✅ Routen-Favorit gespeichert, Chip mit Kurznamen gerendert');
|
||||
|
||||
window.__t(`journeyFromStation = null; journeyToStation = null; journeyFrom.value = ''; journeyTo.value = '';`);
|
||||
routeChip?.click();
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
const filled = doc.getElementById('journeyFrom').value.includes('Pendlerheim');
|
||||
if (!filled) errors.push('Routen-Chip füllt Felder nicht');
|
||||
else if (!lastJourneyUrl) errors.push('Routen-Chip startet keine Suche');
|
||||
else console.log('✅ Routen-Chip: 1 Tap füllt Felder und sucht sofort');
|
||||
|
||||
console.log('✅ Struktur-Checks abgeschlossen');
|
||||
|
||||
if (errors.length === 0) {
|
||||
|
||||
Reference in new issue
Block a user