diff --git a/app.js b/app.js index 55221f0..98c7865 100644 --- a/app.js +++ b/app.js @@ -1,19 +1,56 @@ // ============================= -// Karte initialisieren +// 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; -L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: 'ยฉ OpenStreetMap contributors' -}).addTo(map); +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(); -// Layer fรผr Fahrzeuge const vehiclesLayer = L.layerGroup().addTo(map); const vehicleMarkers = {}; // ============================= -// Farben & Symbole +// Colors & Symbols // ============================= const lineColors = { 'S45': '#CD9C53', 'S46': '#CD9C53', 'S47': '#CD9C53', 'S6': '#CD9C53', 'S65': '#CD9C53', 'S66': '#CD9C53', @@ -36,8 +73,13 @@ const symbols = { 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' +}; + // ============================= -// Hilfsfunktionen +// Helper Functions // ============================= function getLineColor(lineName, product) { if(lineName === 'U12') return null; @@ -59,8 +101,8 @@ function createDivIcon(vehicle) { if(lineName === 'U12') { const iconHtml = ` -
- +
+ @@ -69,34 +111,45 @@ function createDivIcon(vehicle) { - + -
+
${symbol}
-
+
U12
`; - return L.divIcon({ html: iconHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 20] }); + return L.divIcon({ html: iconHtml, className: 'vehicle-marker', iconSize: [30, 42], iconAnchor: [15, 21] }); } const lineColor = getLineColor(lineName, product); - const labelStyle = `background: ${lineColor}; color: white; padding: 2px 6px; border-radius: 3px; display: inline-block; font-weight: bold; font-size: 12px; user-select: none; font-family: Arial, sans-serif;`; const iconHtml = ` -
-
${symbol}
-
${lineName}
+
+
${symbol}
+
${lineName}
`; - return L.divIcon({ html: iconHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 20] }); + return L.divIcon({ html: iconHtml, className: 'vehicle-marker', iconSize: [30, 42], iconAnchor: [15, 21] }); } // ============================= -// Fahrzeuge laden +// 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(); @@ -117,11 +170,32 @@ async function loadVehicles() { 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(`Linie: ${vehicle.line?.name || 'unbekannt'}
Typ: ${vehicle.line?.product || 'unbekannt'}
Richtung: ${vehicle.direction || 'unbekannt'}`) + .bindPopup(popupContent, { maxWidth: 300 }) .addTo(vehiclesLayer); vehicleMarkers[id] = marker; } @@ -133,14 +207,38 @@ async function loadVehicles() { delete vehicleMarkers[id]; } }); + + updateStats(newIds.size); } catch (e) { console.error('Fehler beim Laden der Fahrzeuge:', e); } } // ============================= -// Geolocation & Auto-Update +// Geolocation // ============================= +let locationMarker = null; + +const locationIcon = L.divIcon({ + html: ` +
+
+
+ `, + className: '', + iconSize: [40, 40], + iconAnchor: [20, 20] +}); + function locateAndLoad() { if (!navigator.geolocation) { loadVehicles(); @@ -148,6 +246,16 @@ function locateAndLoad() { } 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(); @@ -159,7 +267,7 @@ setInterval(loadVehicles, 30000); map.on('moveend', loadVehicles); // ============================= -// Bahnhรถfe mit Live-Abfahrten +// Stations // ============================= const stations = [ { name: "Berlin Zoologischer Garten", id: "900023201", coords: [52.5069, 13.3326] }, @@ -171,22 +279,29 @@ const stations = [ ]; async function loadStationDepartures(stationId, marker, stationName) { - const symbols = { - bus: '๐ŸšŒ', tram: '๐ŸšŽ', subway: '๐Ÿš‰', suburban: '๐Ÿš‹', - regional: '๐Ÿš†', express: '๐Ÿš„', longDistance: '๐Ÿš…', ferry: '๐Ÿ›ฅ๏ธ' - }; - 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 = `${stationName}
`; - html += ``; - html += ``; + let html = ` + +
LinieRichtungZeit
+ + + + + + + + + `; - departures.forEach((dep, index) => { + departures.forEach(dep => { const line = dep.line?.name || "?"; const product = dep.line?.product; const icon = symbols[product] || 'โ“'; @@ -195,31 +310,65 @@ async function loadStationDepartures(stationId, marker, stationName) { const planned = dep.plannedWhen ? new Date(dep.plannedWhen) : null; const delay = when && planned ? Math.round((when - planned) / 60000) : 0; const timeStr = when ? when.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }) : "n/a"; - const delayStr = delay > 0 ? `+${delay} min` : ""; + const delayStr = delay > 0 ? `+${delay}'` : ""; const color = getLineColor(line, product); - const bg = index % 2 === 0 ? '#f9f9f9' : '#fff'; - html += ` - - - - `; + html += ` + + + + + + `; }); - html += `
LinieRichtungZeit
${icon} ${line}${direction}${timeStr} ${delayStr}
${icon} ${line}${direction}${timeStr}${delayStr}
`; + html += ` + + +
+ `; + marker.setPopupContent(html).openPopup(); } catch (err) { - marker.setPopupContent(`${stationName}
Fehler beim Laden`); + 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) + const marker = L.marker(station.coords, { icon: stationIcon }) .addTo(map) - .bindPopup(`${station.name}
Lade Abfahrten...`); + .bindPopup(` + + + `, { maxWidth: 400 }); marker.on('click', () => loadStationDepartures(station.id, marker, station.name)); -}); +}); \ No newline at end of file diff --git a/index.html b/index.html index cfc3aaf..e3fd9f3 100644 --- a/index.html +++ b/index.html @@ -8,10 +8,33 @@ + +
+
+ +
+
+ 0 + Fahrzeuge +
+
+
+ --:-- + Aktualisiert +
+
+
+ +
+
- - + \ No newline at end of file diff --git a/style.css b/style.css index 030147b..9cbdeeb 100644 --- a/style.css +++ b/style.css @@ -1,43 +1,488 @@ -html, body, #map { - height: 100%; - margin: 0; - padding: 0; +:root { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --text-primary: #1a1a1a; + --text-secondary: #6c757d; + --border-color: #dee2e6; + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --accent: #0066ff; + --accent-hover: #0052cc; } -#controls { - position: absolute; - top: 10px; - right: 10px; - background: rgba(255,255,255,0.9); - padding: 8px 12px; - border-radius: 5px; - z-index: 1000; - font-family: Arial, sans-serif; +body.dark-mode { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --text-primary: #ffffff; + --text-secondary: #a0a0a0; + --border-color: #3d3d3d; + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); + --accent: #4a9eff; + --accent-hover: #6bb0ff; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: var(--bg-secondary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +#map { + position: fixed; + top: 60px; + left: 0; + right: 0; + bottom: 0; + z-index: 0; +} + +/* Top Bar */ +#topbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 60px; + background: var(--bg-primary); + border-bottom: 1px solid var(--border-color); display: flex; - flex-wrap: wrap; /* wenn zu viele, umbrechen */ + align-items: center; + justify-content: space-between; + padding: 0 20px; + z-index: 1001; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +.topbar-left { + display: flex; + align-items: center; + gap: 32px; +} + +/* Logo */ +.logo { + display: flex; + align-items: center; + gap: 10px; + user-select: none; +} + +.logo-icon { + font-size: 28px; + line-height: 1; +} + +.logo-text { + font-size: 20px; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.5px; +} + +/* Stats */ +.stats { + display: flex; + align-items: center; + gap: 20px; +} + +.stat { + display: flex; + flex-direction: column; + gap: 2px; +} + +.stat-number { + font-size: 18px; + font-weight: 700; + color: var(--accent); + line-height: 1; +} + +.stat-label { + font-size: 11px; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; +} + +.stat-divider { + width: 1px; + height: 30px; + background: var(--border-color); +} + +/* Toggle Button */ +.toggle-btn { + width: 40px; + height: 40px; + border-radius: 10px; + border: none; + background: var(--bg-secondary); + color: var(--text-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + font-size: 20px; +} + +.toggle-btn:hover { + background: var(--accent); + transform: scale(1.05); +} + +.toggle-btn:active { + transform: scale(0.95); +} + +.toggle-icon { + transition: transform 0.3s ease; +} + +body.dark-mode .toggle-icon { + transform: rotate(180deg); +} + +/* Leaflet Marker */ +.vehicle-marker { + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); + cursor: pointer; + transition: filter 0.2s ease; +} + +.vehicle-marker:hover { + filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.5)) brightness(1.1); +} + +/* Line Labels */ +.line-label { + padding: 3px 8px; + border-radius: 4px; + font-weight: 700; + font-size: 11px; + display: inline-block; + color: white; + margin-top: 3px; + user-select: none; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +/* Popups */ +.leaflet-popup-content-wrapper { + background: var(--bg-primary); + color: var(--text-primary); + border-radius: 12px; + box-shadow: var(--shadow-md); + border: 1px solid var(--border-color); + padding: 0; + overflow: hidden; + min-width: 280px; +} + +.leaflet-popup-content { + margin: 0; + font-size: 14px; + line-height: 1.5; +} + +.leaflet-popup-tip { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-right: none; + border-bottom: none; +} + +.popup-header { + background: var(--bg-secondary); + padding: 15px; + border-bottom: 1px solid var(--border-color); +} + +.popup-title { + font-size: 16px; + font-weight: 700; + color: var(--accent); + display: flex; + align-items: center; gap: 8px; } -#controls label { +.popup-body { + padding: 15px; +} + +.info-row { display: flex; - align-items: center; - gap: 4px; - font-size: 12px; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid var(--border-color); +} + +.info-row:last-child { + border-bottom: none; +} + +.info-label { + color: var(--text-secondary); + font-size: 13px; +} + +.info-value { + font-weight: 600; + font-size: 13px; +} + +/* Station Table */ +.departure-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.departure-table th { + text-align: left; + padding: 12px; + background: var(--bg-secondary); + color: var(--text-secondary); + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid var(--border-color); +} + +.departure-table td { + padding: 12px; + border-bottom: 1px solid var(--border-color); +} + +.departure-table tr:last-child td { + border-bottom: none; +} + +.departure-table tr:hover { + background: var(--bg-secondary); +} + +.line-cell { + font-weight: 700; + white-space: nowrap; +} + +.direction-cell { + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.time-cell { + font-weight: 600; + white-space: nowrap; +} + +.delay { + color: #dc3545; + font-weight: 700; + margin-left: 4px; +} + +/* Station Markers */ +.station-marker { cursor: pointer; } -.leaflet-marker-icon { - transition: transform 1s linear; +.station-marker-icon { + display: flex; + align-items: center; + justify-content: center; + position: relative; } -.line-label { - padding: 2px 6px; - border-radius: 3px; - font-weight: bold; - font-size: 10px; - display: inline-block; - color: white; - margin-top: 2px; - user-select: none; - font-family: Arial, sans-serif; +.station-marker-icon::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + border: 3px solid var(--accent); + border-radius: 50%; + animation: pulse-ring 2s ease-out infinite; } + +.station-marker-icon::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + border: 2px solid var(--accent); + border-radius: 50%; + animation: pulse-ring 2s ease-out infinite 1s; +} + +@keyframes pulse-ring { + 0% { + width: 30px; + height: 30px; + opacity: 1; + } + 100% { + width: 60px; + height: 60px; + opacity: 0; + } +} + +.station-marker:hover .station-marker-icon { + animation: station-bounce 0.5s ease; +} + +@keyframes station-bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +body.dark-mode .station-marker-icon::before, +body.dark-mode .station-marker-icon::after { + border-color: var(--accent); +} + +/* Location Marker */ +.location-marker-icon { + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.location-marker-icon::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 20px; + border: 3px solid #27E7F5; + border-radius: 50%; + animation: location-pulse 2s ease-out infinite; +} + +.location-marker-icon::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 20px; + border: 2px solid #27E7F5; + border-radius: 50%; + animation: location-pulse 2s ease-out infinite 1s; +} + +@keyframes location-pulse { + 0% { + width: 20px; + height: 20px; + opacity: 1; + } + 100% { + width: 50px; + height: 50px; + opacity: 0; + } +} + +/* Responsive */ +@media (max-width: 768px) { + #topbar { + padding: 0 15px; + } + + .topbar-left { + gap: 16px; + } + + .logo-text { + font-size: 18px; + } + + .stats { + gap: 12px; + } + + .stat-number { + font-size: 16px; + } + + .stat-label { + font-size: 10px; + } + + #map { + top: 60px; + } + + .leaflet-popup-content-wrapper { + min-width: 260px; + } +} + +@media (max-width: 480px) { + .logo-text { + display: none; + } + + .stat-label { + display: none; + } + + .stat-divider { + display: none; + } + + .stats { + gap: 16px; + } +} + +/* Smooth Transitions */ +.leaflet-popup-content-wrapper, +.toggle-btn, +.leaflet-marker-icon { + transition: all 0.3s ease; +} + +/* Loading State */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +.loading { + animation: pulse 1.5s ease-in-out infinite; +} \ No newline at end of file