Add files via upload
This commit is contained in:
1 parent
710c47f481
commit
69f1742486
3 files changed
+687
-70
No files matched your search
@@ -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);
|
const map = L.map('map').setView([52.52, 13.405], 13);
|
||||||
|
let currentTileLayer;
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
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,
|
maxZoom: 19,
|
||||||
attribution: '© OpenStreetMap contributors'
|
attribution: '© OpenStreetMap contributors'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMapTiles();
|
||||||
|
|
||||||
// Layer für Fahrzeuge
|
|
||||||
const vehiclesLayer = L.layerGroup().addTo(map);
|
const vehiclesLayer = L.layerGroup().addTo(map);
|
||||||
const vehicleMarkers = {};
|
const vehicleMarkers = {};
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// Farben & Symbole
|
// Colors & Symbols
|
||||||
// =============================
|
// =============================
|
||||||
const lineColors = {
|
const lineColors = {
|
||||||
'S45': '#CD9C53', 'S46': '#CD9C53', 'S47': '#CD9C53', 'S6': '#CD9C53', 'S65': '#CD9C53', 'S66': '#CD9C53',
|
'S45': '#CD9C53', 'S46': '#CD9C53', 'S47': '#CD9C53', 'S6': '#CD9C53', 'S65': '#CD9C53', 'S66': '#CD9C53',
|
||||||
@@ -36,8 +73,13 @@ const symbols = {
|
|||||||
regional: '🚆', express: '🚄', longDistance: '🚅', ferry: '🛥️'
|
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) {
|
function getLineColor(lineName, product) {
|
||||||
if(lineName === 'U12') return null;
|
if(lineName === 'U12') return null;
|
||||||
@@ -59,8 +101,8 @@ function createDivIcon(vehicle) {
|
|||||||
|
|
||||||
if(lineName === 'U12') {
|
if(lineName === 'U12') {
|
||||||
const iconHtml = `
|
const iconHtml = `
|
||||||
<div style="text-align:center; font-size:24px; position: relative; width: 30px; height: 40px;">
|
<div style="text-align:center; font-size:26px; position: relative; width: 30px; height: 42px;">
|
||||||
<svg width="30" height="40" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg" style="position:absolute; top:0; left:0;">
|
<svg width="30" height="42" viewBox="0 0 30 42" xmlns="http://www.w3.org/2000/svg" style="position:absolute; top:0; left:0;">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="u12grad" x1="0" y1="0" x2="1" y2="1">
|
<linearGradient id="u12grad" x1="0" y1="0" x2="1" y2="1">
|
||||||
<stop offset="0%" stop-color="#7DAD4C" />
|
<stop offset="0%" stop-color="#7DAD4C" />
|
||||||
@@ -69,34 +111,45 @@ function createDivIcon(vehicle) {
|
|||||||
<stop offset="100%" stop-color="#DA421E" />
|
<stop offset="100%" stop-color="#DA421E" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="30" height="40" fill="url(#u12grad)" />
|
<rect width="30" height="42" rx="4" fill="url(#u12grad)" />
|
||||||
</svg>
|
</svg>
|
||||||
<div style="position:absolute; top:0; left:0; width:30px; height:40px; line-height:40px; font-size:24px; font-weight:bold; color:#fff; mix-blend-mode: difference;">
|
<div style="position:absolute; top:0; left:0; width:30px; height:42px; line-height:42px; font-size:26px; font-weight:bold; color:#fff; mix-blend-mode: difference;">
|
||||||
${symbol}
|
${symbol}
|
||||||
</div>
|
</div>
|
||||||
<div class="line-label" style="position:absolute; bottom:0; left:50%; transform: translateX(-50%); font-size:12px; background:none; color:#000; font-weight:bold; mix-blend-mode:none;">
|
<div class="line-label" style="position:absolute; bottom:2px; left:50%; transform: translateX(-50%); font-size:11px; background:none; color:#000; font-weight:bold; mix-blend-mode:none;">
|
||||||
U12
|
U12
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
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 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 = `
|
const iconHtml = `
|
||||||
<div style="text-align: center; font-size: 26px; line-height: 1;">
|
<div style="text-align: center; font-size: 28px; line-height: 1;">
|
||||||
<div style="color: ${lineColor}; filter: drop-shadow(0 0 1px rgba(0,0,0,0.5));">${symbol}</div>
|
<div style="color: ${lineColor}; filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));">${symbol}</div>
|
||||||
<div class="line-label" style="${labelStyle}">${lineName}</div>
|
<div class="line-label" style="background: ${lineColor};">${lineName}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
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() {
|
async function loadVehicles() {
|
||||||
const bounds = map.getBounds();
|
const bounds = map.getBounds();
|
||||||
@@ -117,11 +170,32 @@ async function loadVehicles() {
|
|||||||
newIds.add(id);
|
newIds.add(id);
|
||||||
const icon = createDivIcon(vehicle);
|
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 = `
|
||||||
|
<div class="popup-header">
|
||||||
|
<div class="popup-title">${symbols[product] || '❓'} ${lineName}</div>
|
||||||
|
</div>
|
||||||
|
<div class="popup-body">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Typ</span>
|
||||||
|
<span class="info-value">${productName}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Richtung</span>
|
||||||
|
<span class="info-value">${direction}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
if (vehicleMarkers[id]) {
|
if (vehicleMarkers[id]) {
|
||||||
vehicleMarkers[id].setLatLng([loc.latitude, loc.longitude]);
|
vehicleMarkers[id].setLatLng([loc.latitude, loc.longitude]);
|
||||||
} else {
|
} else {
|
||||||
const marker = L.marker([loc.latitude, loc.longitude], { icon })
|
const marker = L.marker([loc.latitude, loc.longitude], { icon })
|
||||||
.bindPopup(`<b>Linie:</b> ${vehicle.line?.name || 'unbekannt'}<br><b>Typ:</b> ${vehicle.line?.product || 'unbekannt'}<br><b>Richtung:</b> ${vehicle.direction || 'unbekannt'}`)
|
.bindPopup(popupContent, { maxWidth: 300 })
|
||||||
.addTo(vehiclesLayer);
|
.addTo(vehiclesLayer);
|
||||||
vehicleMarkers[id] = marker;
|
vehicleMarkers[id] = marker;
|
||||||
}
|
}
|
||||||
@@ -133,14 +207,38 @@ async function loadVehicles() {
|
|||||||
delete vehicleMarkers[id];
|
delete vehicleMarkers[id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateStats(newIds.size);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Laden der Fahrzeuge:', e);
|
console.error('Fehler beim Laden der Fahrzeuge:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// Geolocation & Auto-Update
|
// Geolocation
|
||||||
// =============================
|
// =============================
|
||||||
|
let locationMarker = null;
|
||||||
|
|
||||||
|
const locationIcon = L.divIcon({
|
||||||
|
html: `
|
||||||
|
<div class="location-marker-icon" style="width: 40px; height: 40px;">
|
||||||
|
<div style="
|
||||||
|
background: #27E7F5;
|
||||||
|
border: 4px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(39, 231, 245, 0.5);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
className: '',
|
||||||
|
iconSize: [40, 40],
|
||||||
|
iconAnchor: [20, 20]
|
||||||
|
});
|
||||||
|
|
||||||
function locateAndLoad() {
|
function locateAndLoad() {
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
loadVehicles();
|
loadVehicles();
|
||||||
@@ -148,6 +246,16 @@ function locateAndLoad() {
|
|||||||
}
|
}
|
||||||
navigator.geolocation.getCurrentPosition(pos => {
|
navigator.geolocation.getCurrentPosition(pos => {
|
||||||
map.setView([pos.coords.latitude, pos.coords.longitude], 14);
|
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('<b>📍 Dein Standort</b>');
|
||||||
|
}
|
||||||
|
|
||||||
loadVehicles();
|
loadVehicles();
|
||||||
}, () => {
|
}, () => {
|
||||||
loadVehicles();
|
loadVehicles();
|
||||||
@@ -159,7 +267,7 @@ setInterval(loadVehicles, 30000);
|
|||||||
map.on('moveend', loadVehicles);
|
map.on('moveend', loadVehicles);
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// Bahnhöfe mit Live-Abfahrten
|
// Stations
|
||||||
// =============================
|
// =============================
|
||||||
const stations = [
|
const stations = [
|
||||||
{ name: "Berlin Zoologischer Garten", id: "900023201", coords: [52.5069, 13.3326] },
|
{ name: "Berlin Zoologischer Garten", id: "900023201", coords: [52.5069, 13.3326] },
|
||||||
@@ -171,22 +279,29 @@ const stations = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
async function loadStationDepartures(stationId, marker, stationName) {
|
async function loadStationDepartures(stationId, marker, stationName) {
|
||||||
const symbols = {
|
|
||||||
bus: '🚌', tram: '🚎', subway: '🚉', suburban: '🚋',
|
|
||||||
regional: '🚆', express: '🚄', longDistance: '🚅', ferry: '🛥️'
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://v6.vbb.transport.rest/stops/${stationId}/departures?duration=30&results=10`);
|
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}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const departures = data.departures || [];
|
const departures = data.departures || [];
|
||||||
|
|
||||||
let html = `<b style="font-size:14px;">${stationName}</b><br>`;
|
let html = `
|
||||||
html += `<table style="border-collapse:collapse; width:100%; font-size:12px;">`;
|
<div class="popup-header">
|
||||||
html += `<tr style="border-bottom:1px solid #ccc;"><th style="text-align:left; padding:2px 4px;">Linie</th><th style="text-align:left; padding:2px 4px;">Richtung</th><th style="text-align:left; padding:2px 4px;">Zeit</th></tr>`;
|
<div class="popup-title">🚉 ${stationName}</div>
|
||||||
|
</div>
|
||||||
|
<div class="popup-body" style="padding: 0;">
|
||||||
|
<table class="departure-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Linie</th>
|
||||||
|
<th>Richtung</th>
|
||||||
|
<th>Zeit</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
departures.forEach((dep, index) => {
|
departures.forEach(dep => {
|
||||||
const line = dep.line?.name || "?";
|
const line = dep.line?.name || "?";
|
||||||
const product = dep.line?.product;
|
const product = dep.line?.product;
|
||||||
const icon = symbols[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 planned = dep.plannedWhen ? new Date(dep.plannedWhen) : null;
|
||||||
const delay = when && planned ? Math.round((when - planned) / 60000) : 0;
|
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 timeStr = when ? when.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }) : "n/a";
|
||||||
const delayStr = delay > 0 ? `<span style="color:red;">+${delay} min</span>` : "";
|
const delayStr = delay > 0 ? `<span class="delay">+${delay}'</span>` : "";
|
||||||
|
|
||||||
const color = getLineColor(line, product);
|
const color = getLineColor(line, product);
|
||||||
const bg = index % 2 === 0 ? '#f9f9f9' : '#fff';
|
|
||||||
|
|
||||||
html += `<tr style="background:${bg};">
|
html += `
|
||||||
<td style="padding:4px 6px; font-weight:bold; color:${color}">${icon} ${line}</td>
|
<tr>
|
||||||
<td style="padding:4px 6px;">${direction}</td>
|
<td class="line-cell" style="color: ${color}">${icon} ${line}</td>
|
||||||
<td style="padding:4px 6px;">${timeStr} ${delayStr}</td>
|
<td class="direction-cell">${direction}</td>
|
||||||
</tr>`;
|
<td class="time-cell">${timeStr}${delayStr}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `</table>`;
|
html += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
marker.setPopupContent(html).openPopup();
|
marker.setPopupContent(html).openPopup();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
marker.setPopupContent(`<b>${stationName}</b><br>Fehler beim Laden`);
|
marker.setPopupContent(`
|
||||||
|
<div class="popup-header">
|
||||||
|
<div class="popup-title">🚉 ${stationName}</div>
|
||||||
|
</div>
|
||||||
|
<div class="popup-body">
|
||||||
|
<p style="color: var(--text-secondary); text-align: center; padding: 20px 0;">Fehler beim Laden der Abfahrten</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stationIcon = L.divIcon({
|
||||||
|
html: `
|
||||||
|
<div class="station-marker-icon" style="width: 50px; height: 50px;">
|
||||||
|
<div style="
|
||||||
|
font-size: 42px;
|
||||||
|
line-height: 1;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
|
||||||
|
">📍</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
className: 'station-marker',
|
||||||
|
iconSize: [50, 50],
|
||||||
|
iconAnchor: [25, 45]
|
||||||
|
});
|
||||||
|
|
||||||
stations.forEach(station => {
|
stations.forEach(station => {
|
||||||
const marker = L.marker(station.coords)
|
const marker = L.marker(station.coords, { icon: stationIcon })
|
||||||
.addTo(map)
|
.addTo(map)
|
||||||
.bindPopup(`<b>${station.name}</b><br>Lade Abfahrten...`);
|
.bindPopup(`
|
||||||
|
<div class="popup-header">
|
||||||
|
<div class="popup-title">🚉 ${station.name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="popup-body">
|
||||||
|
<p style="color: var(--text-secondary); text-align: center; padding: 20px 0;">Lade Abfahrten...</p>
|
||||||
|
</div>
|
||||||
|
`, { maxWidth: 400 });
|
||||||
|
|
||||||
marker.on('click', () => loadStationDepartures(station.id, marker, station.name));
|
marker.on('click', () => loadStationDepartures(station.id, marker, station.name));
|
||||||
});
|
});
|
||||||
+24
-1
@@ -8,10 +8,33 @@
|
|||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<div id="topbar">
|
||||||
|
<div class="topbar-left">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icon">🚇</span>
|
||||||
|
<span class="logo-text">VBB Live</span>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-number" id="vehicleCount">0</span>
|
||||||
|
<span class="stat-label">Fahrzeuge</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-number" id="updateTime">--:--</span>
|
||||||
|
<span class="stat-label">Aktualisiert</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="darkModeToggle" class="toggle-btn" title="Dark Mode">
|
||||||
|
<span class="toggle-icon">🌙</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
@@ -1,43 +1,488 @@
|
|||||||
html, body, #map {
|
:root {
|
||||||
height: 100%;
|
--bg-primary: #ffffff;
|
||||||
margin: 0;
|
--bg-secondary: #f8f9fa;
|
||||||
padding: 0;
|
--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 {
|
body.dark-mode {
|
||||||
position: absolute;
|
--bg-primary: #1a1a1a;
|
||||||
top: 10px;
|
--bg-secondary: #2d2d2d;
|
||||||
right: 10px;
|
--text-primary: #ffffff;
|
||||||
background: rgba(255,255,255,0.9);
|
--text-secondary: #a0a0a0;
|
||||||
padding: 8px 12px;
|
--border-color: #3d3d3d;
|
||||||
border-radius: 5px;
|
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
z-index: 1000;
|
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||||
font-family: Arial, sans-serif;
|
--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;
|
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;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#controls label {
|
.popup-body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
gap: 4px;
|
padding: 8px 0;
|
||||||
font-size: 12px;
|
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;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-marker-icon {
|
.station-marker-icon {
|
||||||
transition: transform 1s linear;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-label {
|
.station-marker-icon::before {
|
||||||
padding: 2px 6px;
|
content: '';
|
||||||
border-radius: 3px;
|
position: absolute;
|
||||||
font-weight: bold;
|
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;
|
font-size: 10px;
|
||||||
display: inline-block;
|
}
|
||||||
color: white;
|
|
||||||
margin-top: 2px;
|
#map {
|
||||||
user-select: none;
|
top: 60px;
|
||||||
font-family: Arial, sans-serif;
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user