// ============================= // Dark Mode // ============================= const darkModeToggle = document.getElementById('darkModeToggle'); const body = document.body; const savedTheme = localStorage.getItem('theme') || 'light'; if (savedTheme === 'dark') { body.classList.add('dark-mode'); darkModeToggle.querySelector('.toggle-icon').textContent = '☀️'; } darkModeToggle.addEventListener('click', () => { body.classList.toggle('dark-mode'); const isDark = body.classList.contains('dark-mode'); darkModeToggle.querySelector('.toggle-icon').textContent = isDark ? '☀️' : '🌙'; localStorage.setItem('theme', isDark ? 'dark' : 'light'); updateMapTiles(); }); // ============================= // Map Setup // ============================= const map = L.map('map').setView([52.52, 13.405], 13); let currentTileLayer; function updateMapTiles() { if (currentTileLayer) { map.removeLayer(currentTileLayer); } const isDark = body.classList.contains('dark-mode'); if (isDark) { currentTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors © CARTO' }).addTo(map); } else { currentTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(map); } } updateMapTiles(); const vehiclesLayer = L.layerGroup().addTo(map); const vehicleMarkers = {}; // ============================= // Colors & Symbols // ============================= const lineColors = { 'S45': '#CD9C53', 'S46': '#CD9C53', 'S47': '#CD9C53', 'S6': '#CD9C53', 'S65': '#CD9C53', 'S66': '#CD9C53', 'S42': '#CB6418', 'S4': '#AD5937', 'S41': '#AD5937', 'U5': '#7E5330', 'U55': '#7E5330', 'S9': '#992746', 'S95': '#992746', 'S96': '#992746', 'U12': ['#7DAD4C', '#DA421E'], 'U2': '#DA421E', 'S3': '#EB7405', 'S5': '#EB7405', 'S55': '#EB7405', 'S56': '#EB7405', 'U9': '#F3791D', 'S3-wannsee': '#F0D722', 'U4': '#F0D722', 'U1': '#7DAD4C', 'U15': '#7DAD4C', 'S8': '#66AA22', 'S85': '#66AA22', 'S86': '#66AA22', 'S2': '#007734', 'S21': '#007734', 'S25': '#007734', 'S26': '#007734', 'U3': '#16683D', 'U35': '#16683D', 'S3-charlottenburg': '#009BD5', 'U7': '#009BD5', 'S10': '#0082C4', 'S19': '#0082C4', 'S3-berlin-schoenefeld': '#0066AD', 'S35': '#0066AD', 'S36': '#0066AD', 'U8': '#224F86', 'S7': '#816DA6', 'S75': '#816DA6', 'S76': '#816DA6', 'U6': '#8C6DAB', 'S1': '#DA6BA2', 'S15': '#DA6BA2', 'S16': '#DA6BA2', 'U0': '#DA6BA2', 'U10': '#808080', 'U11': '#808080', 'F10': '#528DBA', 'F11': '#528DBA', 'F12': '#528DBA', 'F21': '#528DBA', 'F23': '#528DBA', 'F24': '#528DBA' }; const symbols = { bus: '🚌', tram: '🚎', subway: '🚉', suburban: '🚋', regional: '🚆', express: '🚄', longDistance: '🚅', ferry: '🛥️' }; const productNames = { bus: 'Bus', tram: 'Straßenbahn', subway: 'U-Bahn', suburban: 'S-Bahn', regional: 'Regional', express: 'Express', longDistance: 'Fernverkehr', ferry: 'Fähre' }; // ============================= // Helper Functions // ============================= function getLineColor(lineName, product) { if(lineName === 'U12') return null; if(lineName.startsWith('S3 ')) { if(lineName.toLowerCase().includes('wannsee')) return lineColors['S3-wannsee']; if(lineName.toLowerCase().includes('charlottenburg') || lineName.toLowerCase().includes('schönefeld')) return lineColors['S3-berlin-schoenefeld']; return lineColors['S3']; } if(lineColors[lineName]) return lineColors[lineName]; if(product === 'suburban') return '#CD9C53'; if(product === 'subway') return '#7DAD4C'; return '#666'; } function createDivIcon(vehicle) { const product = vehicle.line?.product; const lineName = vehicle.line?.name || '?'; const symbol = symbols[product] || '❓'; if(lineName === 'U12') { const iconHtml = `
${symbol}
U12
`; return L.divIcon({ html: iconHtml, className: 'vehicle-marker', iconSize: [30, 42], iconAnchor: [15, 21] }); } const lineColor = getLineColor(lineName, product); const iconHtml = `
${symbol}
${lineName}
`; return L.divIcon({ html: iconHtml, className: 'vehicle-marker', iconSize: [30, 42], iconAnchor: [15, 21] }); } // ============================= // Update Stats // ============================= function updateStats(vehicleCount) { document.getElementById('vehicleCount').textContent = vehicleCount; const now = new Date(); document.getElementById('updateTime').textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } // ============================= // Load Vehicles // ============================= async function loadVehicles() { const bounds = map.getBounds(); const bbox = `north=${bounds.getNorth()}&west=${bounds.getWest()}&south=${bounds.getSouth()}&east=${bounds.getEast()}`; try { const res = await fetch(`https://v6.vbb.transport.rest/radar?${bbox}&results=100&duration=30`); const result = await res.json(); const data = result.movements || []; const newIds = new Set(); data.forEach(vehicle => { const id = vehicle.tripId; const loc = vehicle.location; if (!loc || !loc.latitude || !loc.longitude) return; newIds.add(id); const icon = createDivIcon(vehicle); const lineName = vehicle.line?.name || 'Unbekannt'; const product = vehicle.line?.product || 'unknown'; const productName = productNames[product] || 'Unbekannt'; const direction = vehicle.direction || 'Unbekannt'; const popupContent = ` `; if (vehicleMarkers[id]) { vehicleMarkers[id].setLatLng([loc.latitude, loc.longitude]); } else { const marker = L.marker([loc.latitude, loc.longitude], { icon }) .bindPopup(popupContent, { maxWidth: 300 }) .addTo(vehiclesLayer); vehicleMarkers[id] = marker; } }); Object.keys(vehicleMarkers).forEach(id => { if (!newIds.has(id)) { vehiclesLayer.removeLayer(vehicleMarkers[id]); delete vehicleMarkers[id]; } }); updateStats(newIds.size); } catch (e) { console.error('Fehler beim Laden der Fahrzeuge:', e); } } // ============================= // Geolocation // ============================= let locationMarker = null; const locationIcon = L.divIcon({ html: `
`, className: '', iconSize: [40, 40], iconAnchor: [20, 20] }); function locateAndLoad() { if (!navigator.geolocation) { loadVehicles(); return; } navigator.geolocation.getCurrentPosition(pos => { map.setView([pos.coords.latitude, pos.coords.longitude], 14); // Standort-Marker hinzufügen oder aktualisieren if (locationMarker) { locationMarker.setLatLng([pos.coords.latitude, pos.coords.longitude]); } else { locationMarker = L.marker([pos.coords.latitude, pos.coords.longitude], { icon: locationIcon }) .addTo(map) .bindPopup('📍 Dein Standort'); } loadVehicles(); }, () => { loadVehicles(); }); } locateAndLoad(); setInterval(loadVehicles, 30000); map.on('moveend', loadVehicles); // ============================= // Stations // ============================= const stations = [ { name: "Berlin Zoologischer Garten", id: "900023201", coords: [52.5069, 13.3326] }, { name: "Berlin Alexanderplatz", id: "900100003", coords: [52.5215, 13.4115] }, { name: "Berlin Gesundbrunnen", id: "900007102", coords: [52.5483, 13.3889] }, { name: "Berlin Spandau", id: "900029101", coords: [52.5341, 13.1977] }, { name: "Berlin Südkreuz", id: "900058101", coords: [52.4752, 13.3653] }, { name: "Berlin Ostbahnhof", id: "900100004", coords: [52.5100, 13.4340] } ]; async function loadStationDepartures(stationId, marker, stationName) { try { const res = await fetch(`https://v6.vbb.transport.rest/stops/${stationId}/departures?duration=30&results=10`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const departures = data.departures || []; let html = ` `; marker.setPopupContent(html).openPopup(); } catch (err) { marker.setPopupContent(` `); console.error(err); } } const stationIcon = L.divIcon({ html: `
📍
`, className: 'station-marker', iconSize: [50, 50], iconAnchor: [25, 45] }); stations.forEach(station => { const marker = L.marker(station.coords, { icon: stationIcon }) .addTo(map) .bindPopup(` `, { maxWidth: 400 }); marker.on('click', () => loadStationDepartures(station.id, marker, station.name)); });