Add files via upload

This commit is contained in:
Serverlele30 authored and GitHub committed 2026-07-07 10:12:43 +02:00
1 parent fa38de2a0a
commit 693e208cfd
12 files changed
+950 -192

No files matched your search

+48 -3
View File
@@ -4,6 +4,12 @@
// 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
// Datenquelle. transport.rest (VBB/BVG) wurde komplett entfernt.
// Die konkreten Endpoints kapselt js/transitous.js.
@@ -18,6 +24,7 @@
// Zentral hier deklariert, damit KEIN Modul auf Variablen aus
// später geladenen Dateien angewiesen ist.
let currentView = 'home';
let departuresCount = 20; // Wie viele Abfahrten laden ("Mehr laden" erhöht)
let currentStationLat = null; // Koordinaten der gewählten Station
let currentStationLon = null; // (für Transitous-Fallback ohne ID-Mapping)
let liveMap = null;
@@ -247,11 +254,40 @@
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() {
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');
if (!container) return;
const favorites = getFavorites();
if (favorites.length === 0) {
container.innerHTML = '';
@@ -259,11 +295,14 @@
return;
}
const top = favorites.slice(0, 3);
const rest = favorites.length - top.length;
container.style.display = '';
container.innerHTML = `
<div class="home-favorites-title">⭐ Deine Stationen</div>
<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)}"
data-lat="${f.lat != null ? f.lat : ''}" data-lon="${f.lon != null ? f.lon : ''}"
aria-label="Abfahrten für ${escapeHtml(f.name)} anzeigen">
@@ -271,6 +310,12 @@
<span class="home-favorite-arrow" aria-hidden="true">→</span>
</button>
`).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>
`;
}
+56 -9
View File
@@ -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) {
departuresCount = 20; // Neue Station -> wieder mit 20 Abfahrten starten
currentStationId = stationId;
currentStationName = stationName;
currentStationLat = lat;
@@ -529,6 +540,7 @@
// Station für den nächsten App-Start merken
storageSet(STORAGE_KEYS.lastStation, { id: stationId, name: stationName, lat, lon });
updateFavoriteButton();
renderFavorites(); // Aktiv-Markierung der Favoriten-Chips auffrischen
// Haptic feedback
if (navigator.vibrate) {
@@ -591,7 +603,7 @@
const data = await transitousDepartures({
id: currentStationId, name: currentStationName,
lat: currentStationLat, lon: currentStationLon
});
}, departuresCount);
displayDepartures(data.departures || []);
saveDeparturesCache(currentStationId, data.departures || []);
noteRefreshSuccess();
@@ -636,13 +648,18 @@
const data = await transitousDepartures({
id: currentStationId, name: currentStationName,
lat: currentStationLat, lon: currentStationLon
});
const departures = data.departures || [];
}, departuresCount);
const fullDepartures = data.departures || [];
allDepartures = fullDepartures;
const departures = (typeof applyDepartureFilter === 'function')
? applyDepartureFilter(fullDepartures) : fullDepartures;
const now = new Date();
// In-Place-Update: nur Zeiten/Verspätungen im bestehenden DOM
// 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 domTripIds = Array.from(items).map(i => i.dataset.tripId);
const newTripIds = departures.map(d => String(d.tripId));
@@ -651,7 +668,7 @@
domTripIds.every((id, i) => id === newTripIds[i]);
if (!sameSet) {
displayDepartures(departures);
displayDepartures(fullDepartures);
} else {
departures.forEach((dep, i) => {
const item = items[i];
@@ -673,7 +690,7 @@
}
}
saveDeparturesCache(currentStationId, departures);
saveDeparturesCache(currentStationId, fullDepartures);
noteRefreshSuccess();
} catch (error) {
@@ -682,8 +699,17 @@
}
}
function displayDepartures(departures) {
if (departures.length === 0) {
function displayDepartures(fullDepartures) {
// 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 = `
<div class="empty-state">
<div class="empty-state-icon">🚉</div>
@@ -693,6 +719,20 @@
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 stationInfoHTML = `
<div class="station-info">
@@ -746,7 +786,14 @@
`;
}).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
departuresContainer.querySelectorAll('.departure-item').forEach(item => {
+56 -113
View File
@@ -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() {
if (!allDepartures || allDepartures.length === 0) return;
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);
}
displayDepartures(allDepartures);
}
// ==========================================
@@ -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.
// Die Funktion existierte nie und verursachte einen ReferenceError,
// der das restliche Script abgebrochen hat.
@@ -440,6 +408,23 @@ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
// APP-START: GESPEICHERTEN ZUSTAND WIEDERHERSTELLEN
// ==========================================
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
renderFavorites();
@@ -466,8 +451,27 @@ document.addEventListener('DOMContentLoaded', () => {
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) => {
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');
if (favItem) {
switchView('departures');
@@ -531,75 +535,14 @@ document.addEventListener('DOMContentLoaded', () => {
});
// ==========================================
// HOVER-EXTRAS (bereinigt)
// Frühere Version zeigte FAKE-Daten (zufällige Gleisnummern,
// erfundene "Genauigkeit ±10m", Alert-Attrappen für Favorit/Teilen/
// Export/Kalender) und hatte einen Scope-Bug im Search-Hint,
// der bei jedem Tastendruck einen ReferenceError warf.
// HOVER-EXTRAS: KOMPLETT ENTFERNT (v37.1)
// - Nav-Quick-Infos: standen als doppelte Beschriftung im Menü
// - Search-Hints ("Tippe mindestens 2 Zeichen"): hatten kein CSS und
// klebten als permanenter Rohtext unter den Suchfeldern
// - 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
// (Event-Delegation statt MutationObserver - robuster und günstiger)
document.addEventListener('click', async (e) => {
+77 -4
View File
@@ -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
// und rendern. Läuft jede Sekunde lokal - null API-Kosten.
function renderVehiclesFromSegments() {
@@ -250,6 +306,8 @@
const movements = [];
for (const seg of liveMapSegments) {
// Fokus-Modus: nur die angetippte Fahrt rendern
if (focusedVehicleTripId && seg.tripId !== focusedVehicleTripId) continue;
const pos = segmentPositionAt(seg, nowMs);
if (!pos) continue; // Fahrzeug (noch) nicht auf diesem Segment unterwegs
movements.push({
@@ -278,12 +336,18 @@
return activeVehicleTypes.has(vehicleType);
});
// Update Counter mit Live-Indikator
// Update Counter mit Live-Indikator (bzw. Fokus-Hinweis)
const countEl = document.getElementById('vehicleCount');
if (countEl) {
countEl.innerHTML =
`<span class="live-dot" aria-hidden="true"></span> ` +
`<span style="color: #fff;">${filteredMovements.length}</span> Fahrzeuge live`;
if (focusedVehicleTripId) {
const focusName = filteredMovements[0]?.line?.name || '';
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();
@@ -342,6 +406,9 @@
const marker = L.marker([lat, lon], { icon: icon });
marker.bindPopup(popupContent);
// Popup auf = Strecke dieser Fahrt einzeichnen (in Linienfarbe)
marker.on('popupopen', () => showVehicleRoute(tripId));
marker.on('popupclose', hideVehicleRoute);
marker.addTo(liveMap);
vehicleMarkerMap.set(tripId, { marker, lineName });
});
@@ -354,6 +421,12 @@
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
+37 -5
View File
@@ -135,10 +135,10 @@ async function transitousStopQuery({ id, name, lat, lon }) {
// ==========================================
// ABFAHRTEN (stoptimes -> HAFAS-departures-Form)
// ==========================================
async function transitousDepartures(station) {
async function transitousDepartures(station, n = 20) {
const stopQuery = await transitousStopQuery(station);
// 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 p = st.place || {};
@@ -294,6 +294,31 @@ async function transitousTrip(tripId) {
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)
//
@@ -303,8 +328,10 @@ async function transitousTrip(tripId) {
// Transitous-Web-Karte selbst auch).
// ==========================================
// Google-Polyline dekodieren (Precision 5 laut MOTIS-Spec für map/trips)
function decodePolyline5(encoded) {
// Google-Polyline dekodieren. Die Precision variiert je Endpoint:
// 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 = [];
let index = 0, lat = 0, lon = 0;
while (index < encoded.length) {
@@ -318,11 +345,16 @@ function decodePolyline5(encoded) {
const delta = (result & 1) ? ~(result >> 1) : (result >> 1);
if (which === 0) lat += delta; else lon += delta;
}
points.push([lat / 1e5, lon / 1e5]);
points.push([lat / factor, lon / factor]);
}
return points;
}
// Kompatibilitäts-Alias (Tests + map/trips)
function decodePolyline5(encoded) {
return decodePolyline(encoded, 5);
}
async function transitousRadarSegments(bounds, zoom) {
const now = new Date();
const path = `/v6/map/trips?zoom=${Math.round(zoom)}` +