// Live-Karte mit mehreren Modi: // - "radar": zeigt alle Fahrzeuge im aktuellen Kartenausschnitt (Standard), // gefiltert nach den gewählten Verkehrsmitteln // - "isolated": ein Fahrzeug wurde angeklickt -> nur dieses wird live weiter- // verfolgt (serverseitig via tripId gefiltert), sein Streckenverlauf // wird eingezeichnet, alle anderen Fahrzeuge sind ausgeblendet // - "trip": verfolgt ein bekanntes Fahrzeug (z.B. aus einer Verbindung heraus) // Nutzt bewusst reines OpenStreetMap ohne zusätzlichen Kartenstil. // Zusätzlich: createMiniMap() für kleine, unabhängige Vorschau-Karten in den // Verbindungsergebnissen (Routenplaner). const LiveMap = (() => { let map = null; let radarLayer = null; let routeLayer = null; let userLayer = null; let tripMarker = null; let mode = 'radar'; let radarPollHandle = null; let tripPollHandle = null; let currentTripId = null; let isolatedVehicleMeta = null; let getModesParam = () => null; function init() { if (map) return; map = L.map('live-map', { zoomControl: true }).setView([52.5200, 13.4050], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap-Mitwirkende', maxZoom: 19, }).addTo(map); radarLayer = L.layerGroup().addTo(map); routeLayer = L.layerGroup().addTo(map); userLayer = L.layerGroup().addTo(map); map.on('moveend', () => { if (mode === 'radar') refreshRadar(); }); } function setModeFilter(fn) { getModesParam = fn; } function vehicleIcon(label, bg, fg) { return L.divIcon({ className: 'vehicle-marker', html: `${label || '?'}`, iconSize: [null, null], }); } function polylineToLatLngs(geojson) { if (!geojson || !geojson.features) return []; return geojson.features.map((f) => [f.geometry.coordinates[1], f.geometry.coordinates[0]]); } async function refreshRadar() { if (!map || mode !== 'radar') return; const bounds = map.getBounds(); let data; try { data = await API.getRadar({ north: bounds.getNorth().toFixed(5), west: bounds.getWest().toFixed(5), south: bounds.getSouth().toFixed(5), east: bounds.getEast().toFixed(5), modes: getModesParam(), }); } catch (err) { console.error('Radar-Fehler:', err.message); return; } radarLayer.clearLayers(); data.vehicles.forEach((v) => { if (v.latitude == null || v.longitude == null) return; const marker = L.marker([v.latitude, v.longitude], { icon: vehicleIcon(v.line, v.color, v.textColor) }); marker.bindPopup( `${v.line || 'Linie unbekannt'}
${v.direction || ''}
` + `Nächster Halt: ${v.nextStopover || '–'}`, ); marker.on('click', () => isolateVehicle(v)); marker.addTo(radarLayer); }); } function startRadar() { init(); setMode('radar'); refreshRadar(); if (radarPollHandle) clearInterval(radarPollHandle); radarPollHandle = setInterval(refreshRadar, 15000); setTimeout(() => map && map.invalidateSize(), 150); } function stopRadar() { if (radarPollHandle) { clearInterval(radarPollHandle); radarPollHandle = null; } } async function refreshIsolated() { if (!map || mode !== 'isolated' || !currentTripId) return; const bounds = map.getBounds(); let data; try { data = await API.getRadar({ north: bounds.getNorth().toFixed(5), west: bounds.getWest().toFixed(5), south: bounds.getSouth().toFixed(5), east: bounds.getEast().toFixed(5), tripId: currentTripId, }); } catch (err) { console.error('Radar-Fehler (isoliert):', err.message); return; } radarLayer.clearLayers(); const v = data.vehicles[0]; if (v && v.latitude != null && v.longitude != null) { const marker = L.marker([v.latitude, v.longitude], { icon: vehicleIcon(v.line, v.color, v.textColor) }); marker.bindPopup(`${v.line || ''}
${v.direction || ''}
Nächster Halt: ${v.nextStopover || '–'}`); marker.addTo(radarLayer); } } async function isolateVehicle(vehicle) { if (!vehicle || !vehicle.tripId) return; setMode('isolated'); currentTripId = vehicle.tripId; isolatedVehicleMeta = { line: vehicle.line, color: vehicle.color, textColor: vehicle.textColor }; updateIsolationBanner(); routeLayer.clearLayers(); try { const trip = await API.getTrip(vehicle.tripId, { polyline: true }); if (trip.polyline) { const latlngs = polylineToLatLngs(trip.polyline); if (latlngs.length > 0) { L.polyline(latlngs, { color: vehicle.color || '#F5A623', weight: 5, opacity: 0.85 }).addTo(routeLayer); } } } catch (err) { console.error('Trip-Polyline-Fehler:', err.message); } refreshIsolated(); if (radarPollHandle) clearInterval(radarPollHandle); radarPollHandle = setInterval(refreshIsolated, 10000); } function clearIsolation() { currentTripId = null; isolatedVehicleMeta = null; updateIsolationBanner(); startRadar(); } function updateIsolationBanner() { const banner = document.getElementById('map-isolation-banner'); if (!banner) return; if (!isolatedVehicleMeta) { banner.classList.remove('is-visible'); return; } const lineEl = document.getElementById('map-isolation-line'); if (lineEl) { lineEl.textContent = isolatedVehicleMeta.line || '?'; lineEl.style.background = isolatedVehicleMeta.color || '#F5A623'; lineEl.style.color = isolatedVehicleMeta.textColor || '#fff'; } banner.classList.add('is-visible'); } async function refreshTrip() { if (!currentTripId) return; let data; try { data = await API.getTrip(currentTripId, { polyline: !tripMarker }); } catch (err) { console.error('Trip-Fehler:', err.message); return; } if (data.polyline && routeLayer) { routeLayer.clearLayers(); const latlngs = polylineToLatLngs(data.polyline); if (latlngs.length > 0) { L.polyline(latlngs, { color: data.color || '#F5A623', weight: 4, opacity: 0.7 }).addTo(routeLayer); map.fitBounds(L.latLngBounds(latlngs), { padding: [30, 30] }); } } if (data.currentLocation) { const pos = [data.currentLocation.latitude, data.currentLocation.longitude]; if (tripMarker) { tripMarker.setLatLng(pos); } else { tripMarker = L.marker(pos, { icon: vehicleIcon(data.line, data.color, data.textColor) }).addTo(routeLayer); } tripMarker.bindPopup(`${data.line || ''}
→ ${data.direction || ''}`); } } function trackTrip(tripId) { init(); setMode('trip'); currentTripId = tripId; tripMarker = null; routeLayer.clearLayers(); refreshTrip(); if (tripPollHandle) clearInterval(tripPollHandle); tripPollHandle = setInterval(refreshTrip, 10000); setTimeout(() => map && map.invalidateSize(), 150); } function stopTracking() { if (tripPollHandle) { clearInterval(tripPollHandle); tripPollHandle = null; } currentTripId = null; tripMarker = null; } function locateUser() { init(); if (!navigator.geolocation) { alert('Geolocation wird von diesem Browser nicht unterstützt.'); return; } navigator.geolocation.getCurrentPosition( (pos) => { const { latitude, longitude } = pos.coords; userLayer.clearLayers(); L.marker([latitude, longitude], { icon: L.divIcon({ className: 'user-location-marker', iconSize: [16, 16] }), }).addTo(userLayer); map.setView([latitude, longitude], 16); }, (err) => alert('Standort konnte nicht ermittelt werden: ' + err.message), { enableHighAccuracy: true, timeout: 10000 }, ); } function setMode(newMode) { if (mode === newMode) return; if (mode === 'radar') stopRadar(); if (mode === 'isolated') stopRadar(); if (mode === 'trip') stopTracking(); if (newMode === 'radar') routeLayer.clearLayers(); mode = newMode; } function startPolling() { if (mode === 'radar' || mode === 'idle') startRadar(); else if (map) setTimeout(() => map.invalidateSize(), 150); } function stopPolling() { stopRadar(); stopTracking(); } function createMiniMap(containerId, journey) { const miniMap = L.map(containerId, { zoomControl: false, attributionControl: false, dragging: true, scrollWheelZoom: false, tap: true, }).setView([52.5200, 13.4050], 12); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap-Mitwirkende', maxZoom: 19, }).addTo(miniMap); const allPoints = []; (journey.legs || []).forEach((leg) => { if (!leg.polyline) return; const latlngs = polylineToLatLngs(leg.polyline); if (latlngs.length === 0) return; L.polyline(latlngs, { color: leg.walking ? '#8E99A6' : leg.color || '#F5A623', weight: leg.walking ? 3 : 4, dashArray: leg.walking ? '5, 7' : null, opacity: 0.9, }).addTo(miniMap); allPoints.push(...latlngs); }); if (allPoints.length > 0) { miniMap.fitBounds(L.latLngBounds(allPoints), { padding: [16, 16] }); } setTimeout(() => miniMap.invalidateSize(), 50); return miniMap; } return { startPolling, stopPolling, trackTrip, setModeFilter, clearIsolation, locateUser, createMiniMap, }; })();