// 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 // Map zentriert auf Berlin liveMap = L.map('liveMap', { center: [52.52, 13.405], zoom: 12, zoomControl: false // Keine +/- Tasten (Zoomen per Geste/Scrollrad) }); // 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: 8, subdomains: 'abcd', keepBuffer: 4, // Rand-Kacheln behalten -> flüssigeres Pannen updateWhenIdle: false // Kacheln schon WÄHREND der Bewegung laden }).addTo(liveMap); // HINWEIS: Daten-Polling startet NICHT hier, sondern beim Öffnen // der View (switchView). Dadurch kann die Karte selbst schon im // Hintergrund vorinitialisiert werden (Leaflet-Setup + erste // Kacheln), ohne einen einzigen API-Request zu verursachen. // Lokaler Animations-Loop (1s-Takt, kostet keine API-Requests) if (!liveMapAnimationInterval) { liveMapAnimationInterval = setInterval(() => { if (document.hidden) return; if (currentView !== 'livemap') return; renderVehiclesFromSegments(); }, 1000); } // 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; // Keine Berlin-Klemmung mehr: Transitous deckt ganz // Brandenburg (und darüber hinaus) ab - die alte Klemmung // schnitt z.B. Potsdam ab. const bounds = { north: mapBounds.getNorth() + latPadding, south: mapBounds.getSouth() - latPadding, west: mapBounds.getWest() - lngPadding, east: mapBounds.getEast() + lngPadding }; const zoom = liveMap.getZoom ? liveMap.getZoom() : 12; // Fahrt-Segmente von Transitous holen (map/trips) und für // 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); const counterEl = document.getElementById('vehicleCount'); if (counterEl) { counterEl.textContent = error.message === 'RATE_LIMIT' ? '⏳ API-Limit erreicht – kurz warten' : '⚠️ Fehler beim Laden der Fahrzeuge'; } } } // ========================================== // 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 = '