Add files via upload
This commit is contained in:
1 parent
fa38de2a0a
commit
693e208cfd
12 files changed
+950
-192
No files matched your search
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)}` +
|
||||
|
||||
Reference in new issue
Block a user