// VBB Netz Status - Live-Map (Leaflet, Fahrzeug-Radar) // ========================================== // LIVE-MAP FUNCTIONALITY // ========================================== // (Geteilter Live-Map-Zustand ist zentral in js/api.js deklariert) // Fahrzeug-Icons nach Typ function getVehicleEmoji(product, lineName) { const type = product?.mode || product?.name || ''; const line = lineName || ''; // U-Bahn - verschiedene Icons je nach Linie if (type.includes('subway') || type.includes('U-Bahn') || type.match(/^U\d/)) { return '🚇'; } // S-Bahn else if (type.includes('suburban') || type.includes('S-Bahn') || type.match(/^S\d/)) { return '🚆'; } // Tram/Straßenbahn else if (type.includes('tram') || type.includes('Tram') || line.match(/^M\d/) || line.match(/^\d{2}$/)) { return '🚊'; } // Bus else if (type.includes('bus') || type.includes('Bus')) { // Express-Bus if (line.includes('X') || line.includes('TXL')) { return '🚌'; } // Nacht-Bus else if (line.startsWith('N')) { return '🌙'; } // Normal-Bus return '🚌'; } // Regional-Bahn else if (type.includes('regional') || line.includes('RE') || line.includes('RB')) { return '🚉'; } // Express-Züge else if (type.includes('express') || line.includes('ICE') || line.includes('IC')) { return '🚅'; } // Fähre else if (type.includes('ferry') || type.includes('Fähre')) { return '⛴️'; } return '🚍'; // Default } // BVG-Linienfarben (authentische Farben!) function getBVGLineColor(lineName) { const line = lineName?.toUpperCase() || ''; // U-Bahn Farben (offiziell) const ubahnColors = { 'U1': '#55A823', // Grün 'U2': '#DA421E', // Rot 'U3': '#16683D', // Türkis 'U4': '#F0D722', // Gelb 'U5': '#7E5330', // Braun 'U6': '#8C6DAB', // Lila 'U7': '#528DBA', // Hellblau 'U8': '#224F86', // Dunkelblau 'U9': '#F3791D' // Orange }; // S-Bahn Farben (offiziell) const sbahnColors = { 'S1': '#DE4DA4', // Pink/Magenta 'S2': '#076D3D', // Dunkelgrün 'S25': '#55A823', // Grün 'S26': '#7E5330', // Braun 'S3': '#1E5BA8', // Blau 'S41': '#A65A28', // Braun/Orange (Ring) 'S42': '#A65A28', // Braun/Orange (Ring) 'S45': '#F0D722', // Gelb 'S46': '#DA421E', // Rot 'S47': '#8C6DAB', // Lila 'S5': '#F3791D', // Orange 'S7': '#BA7AB5', // Lila/Magenta 'S75': '#6F7072', // Grau 'S8': '#55A823', // Grün 'S85': '#FFFFFF', // Weiß 'S9': '#DA421E' // Rot }; // Tram Farben (Metro = Rot, andere = Gelb) const tramColors = { 'M1': '#DA421E', 'M2': '#DA421E', 'M4': '#DA421E', 'M5': '#DA421E', 'M6': '#DA421E', 'M8': '#DA421E', 'M10': '#DA421E', 'M13': '#DA421E', 'M17': '#DA421E' }; // Bus Farben (Metro = Gelb, Express = Blau, Normal = Lila) if (line.startsWith('M') && !line.match(/M\d{1,2}$/)) { return '#F0D722'; // Metro-Bus: Gelb } if (line.startsWith('X') || line.includes('TXL')) { return '#1E5BA8'; // Express-Bus: Blau } if (line.startsWith('N')) { return '#000000'; // Nacht-Bus: Schwarz mit weißer Schrift } // Farbe zurückgeben return ubahnColors[line] || sbahnColors[line] || tramColors[line] || '#993399'; // Default: Lila } // Fahrzeugtyp bestimmen function getVehicleType(product, lineName) { const type = product?.mode || product?.name || ''; const line = lineName?.toUpperCase() || ''; // Prüfung nach Liniennamen (zuverlässiger!) if (line.match(/^U\d/)) { return 'subway'; } else if (line.match(/^S\d/) || line.match(/^S\d{2}/)) { return 'suburban'; } else if (line.match(/^M\d/) || line.match(/^\d{2}$/)) { return 'tram'; } else if (line.startsWith('N') || line.startsWith('X') || line.match(/^\d{3}$/)) { return 'bus'; } // Fallback: Prüfung nach product.mode if (type.includes('subway') || type.includes('U-Bahn')) { return 'subway'; } else if (type.includes('suburban') || type.includes('S-Bahn')) { return 'suburban'; } else if (type.includes('tram') || type.includes('Tram')) { return 'tram'; } else if (type.includes('bus') || type.includes('Bus')) { return 'bus'; } return 'other'; } // Live-Map initialisieren function initLiveMap() { if (liveMap) return; // Bereits initialisiert // Zoom-Control nur auf Desktop const isMobile = window.innerWidth < 768; // Map zentriert auf Berlin liveMap = L.map('liveMap', { center: [52.52, 13.405], zoom: 12, zoomControl: !isMobile // Nur auf Desktop! }); // Tile Layer (CartoDB Dark Matter - Dark Mode!) L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors, © CartoDB', maxZoom: 19, minZoom: 10, subdomains: 'abcd' }).addTo(liveMap); // Fahrzeuge initial laden loadVehicles(); // Auto-Update alle 15 Sekunden // Pausiert automatisch, wenn Tab im Hintergrund oder andere View aktiv liveMapUpdateInterval = setInterval(() => { if (document.hidden) return; if (currentView !== 'livemap') return; loadVehicles(); }, 15000); // Event-Listener: Neu laden wenn Karte bewegt/gezoomt wird // Mit Debouncing (1 Sekunde Verzögerung) um API zu schonen liveMap.on('moveend', function() { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { loadVehicles(); }, 1000); // 1 Sekunde warten nach Bewegung }); } // Fahrzeuge von API laden async function loadVehicles() { // Sicherheitsabfrage: Map muss existieren if (!liveMap) { return; } try { // Loading-State anzeigen const counterEl = document.getElementById('vehicleCount'); if (counterEl) { counterEl.innerHTML = '⏳ Lade...'; } // Dynamische Bounding Box vom aktuellen Kartenausschnitt const mapBounds = liveMap.getBounds(); // Puffer hinzufügen (15% größer) damit Fahrzeuge nicht sofort verschwinden const latDiff = mapBounds.getNorth() - mapBounds.getSouth(); const lngDiff = mapBounds.getEast() - mapBounds.getWest(); const latPadding = latDiff * 0.15; const lngPadding = lngDiff * 0.15; const bounds = { north: Math.min(52.70, mapBounds.getNorth() + latPadding), // Max: Berlin Nord south: Math.max(52.35, mapBounds.getSouth() - latPadding), // Min: Berlin Süd west: Math.max(13.08, mapBounds.getWest() - lngPadding), // Min: Berlin West east: Math.min(13.76, mapBounds.getEast() + lngPadding) // Max: Berlin Ost }; // Ergebnisanzahl an Zoom koppeln: nah dran reichen wenige Fahrzeuge, // weit rausgezoomt begrenzen wir auf 200 (kleinere API-Responses) const zoom = liveMap.getZoom ? liveMap.getZoom() : 12; const maxResults = zoom >= 15 ? 80 : zoom >= 13 ? 140 : 200; // Koordinaten runden -> stabile Cache-Keys bei minimalen Kartenbewegungen const url = `${API_BASE}/radar?` + `north=${bounds.north.toFixed(3)}&south=${bounds.south.toFixed(3)}&` + `west=${bounds.west.toFixed(3)}&east=${bounds.east.toFixed(3)}&` + `results=${maxResults}&duration=60&` + `subway=true&bus=true&tram=true&suburban=true®ional=true`; // 10s Cache: schnelles Hin- und Herzoomen kostet keine Extra-Requests const data = await apiFetch(url, { ttl: 10000 }); lastVehicleMovements = data.movements || []; displayVehicles(lastVehicleMovements); } catch (error) { console.error('Load vehicles error:', error); const counterEl = document.getElementById('vehicleCount'); if (counterEl) { counterEl.textContent = error.message === 'RATE_LIMIT' ? '⏳ API-Limit erreicht – kurz warten' : '⚠️ Fehler beim Laden der Fahrzeuge'; } } } // Fahrzeuge auf Map anzeigen // Marker werden per tripId wiederverwendet und nur BEWEGT (setLatLng), // statt bei jedem Update alle zu zerstören und neu zu erzeugen. // -> flüssigere Karte, offene Popups bleiben offen, weniger DOM-Arbeit. // (vehicleMarkerMap ist zentral in js/api.js deklariert) function displayVehicles(movements) { // Filtern nach aktiven Typen const filteredMovements = movements.filter(m => { if (activeVehicleTypes.has('all')) return true; const vehicleType = getVehicleType(m.line?.product, m.line?.name); return activeVehicleTypes.has(vehicleType); }); // Update Counter const countEl = document.getElementById('vehicleCount'); if (countEl) { countEl.innerHTML = `🚇 ${filteredMovements.length} Fahrzeuge aktiv`; } const seenTripIds = new Set(); filteredMovements.forEach(movement => { if (!movement.location || !movement.location.latitude || !movement.location.longitude) { return; } const lat = movement.location.latitude; const lon = movement.location.longitude; const lineName = movement.line?.name || 'Unbekannt'; const tripId = movement.tripId || `${lineName}|${movement.direction || ''}`; seenTripIds.add(tripId); const direction = movement.direction || 'Keine Angabe'; const delay = movement.delay ? `+${Math.round(movement.delay / 60)} min Verspätung` : 'Pünktlich'; const lineColor = getBVGLineColor(lineName); const isLightBg = lineColor === '#F0D722' || lineColor === '#FFFFFF' || lineColor === '#55A823'; const textColor = isLightBg ? '#000' : '#FFF'; const emoji = getVehicleEmoji(movement.line?.product, lineName); const popupContent = `
`; const existing = vehicleMarkerMap.get(tripId); if (existing) { // Marker existiert bereits -> nur bewegen und Popup-Inhalt auffrischen existing.marker.setLatLng([lat, lon]); existing.marker.setPopupContent(popupContent); return; } // Neues Fahrzeug -> Marker erzeugen const icon = L.divIcon({ html: `
${emoji}
${escapeHtml(lineName)}
`, className: 'custom-marker', iconSize: [50, 60], iconAnchor: [25, 30] }); const marker = L.marker([lat, lon], { icon: icon }); marker.bindPopup(popupContent); marker.addTo(liveMap); vehicleMarkerMap.set(tripId, { marker, lineName }); }); // Fahrzeuge entfernen, die nicht mehr in den Daten sind // (außer Sichtbereich, Fahrt beendet, oder vom Filter ausgeblendet) for (const [tripId, entry] of vehicleMarkerMap) { if (!seenTripIds.has(tripId)) { entry.marker.remove(); vehicleMarkerMap.delete(tripId); } } } // Filter Buttons mit Event Delegation document.addEventListener('click', function(e) { // Prüfen ob es ein Filter-Button ist if (e.target.closest('.filter-btn')) { const btn = e.target.closest('.filter-btn'); const type = btn.dataset.type; if (type === 'all') { // Alle Filter deaktivieren, nur "Alle" aktiv document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); activeVehicleTypes.clear(); activeVehicleTypes.add('all'); } else { // "Alle" deaktivieren const allBtn = document.querySelector('.filter-btn[data-type="all"]'); if (allBtn) allBtn.classList.remove('active'); activeVehicleTypes.delete('all'); // Toggle Filter if (activeVehicleTypes.has(type)) { activeVehicleTypes.delete(type); btn.classList.remove('active'); } else { activeVehicleTypes.add(type); btn.classList.add('active'); } // Falls keine Filter aktiv, "Alle" wieder aktivieren if (activeVehicleTypes.size === 0) { activeVehicleTypes.add('all'); const allBtn = document.querySelector('.filter-btn[data-type="all"]'); if (allBtn) allBtn.classList.add('active'); } } // Lokal filtern - Daten sind bereits da, KEIN neuer API-Call nötig! if (lastVehicleMovements.length > 0) { displayVehicles(lastVehicleMovements); } else { loadVehicles(); } if (navigator.vibrate) navigator.vibrate(5); } });