Update all
This commit is contained in:
1 parent
03a0e8f024
commit
a7e6ae575a
11 files changed
+76
-846
No files matched your search
@@ -36,31 +36,6 @@ const symbols = {
|
|||||||
regional: '🚆', express: '🚄', longDistance: '🚅', ferry: '🛥️'
|
regional: '🚆', express: '🚄', longDistance: '🚅', ferry: '🛥️'
|
||||||
};
|
};
|
||||||
|
|
||||||
// =============================
|
|
||||||
// Aktive Filter
|
|
||||||
// =============================
|
|
||||||
let activeProducts = Array.from(document.querySelectorAll('#controls input[type="checkbox"]'))
|
|
||||||
.filter(c => c.checked)
|
|
||||||
.map(c => c.value);
|
|
||||||
|
|
||||||
// Checkbox Event
|
|
||||||
document.querySelectorAll('#controls input[type="checkbox"]').forEach(cb => {
|
|
||||||
cb.addEventListener('change', () => {
|
|
||||||
activeProducts = Array.from(document.querySelectorAll('#controls input[type="checkbox"]'))
|
|
||||||
.filter(c => c.checked)
|
|
||||||
.map(c => c.value);
|
|
||||||
|
|
||||||
Object.values(vehicleMarkers).forEach(marker => {
|
|
||||||
const prod = marker.options.vehicleProduct || '';
|
|
||||||
if (activeProducts.includes(prod)) {
|
|
||||||
if (!map.hasLayer(marker)) map.addLayer(marker);
|
|
||||||
} else {
|
|
||||||
if (map.hasLayer(marker)) map.removeLayer(marker);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// Hilfsfunktionen
|
// Hilfsfunktionen
|
||||||
// =============================
|
// =============================
|
||||||
@@ -78,7 +53,7 @@ function getLineColor(lineName, product) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDivIcon(vehicle) {
|
function createDivIcon(vehicle) {
|
||||||
const product = vehicle.line?.product || '';
|
const product = vehicle.line?.product;
|
||||||
const lineName = vehicle.line?.name || '?';
|
const lineName = vehicle.line?.name || '?';
|
||||||
const symbol = symbols[product] || '❓';
|
const symbol = symbols[product] || '❓';
|
||||||
|
|
||||||
@@ -104,9 +79,7 @@ function createDivIcon(vehicle) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const icon = L.divIcon({ html: iconHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 20] });
|
return L.divIcon({ html: iconHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 20] });
|
||||||
icon.product = product;
|
|
||||||
return icon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineColor = getLineColor(lineName, product);
|
const lineColor = getLineColor(lineName, product);
|
||||||
@@ -119,9 +92,7 @@ function createDivIcon(vehicle) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const icon = L.divIcon({ html: iconHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 20] });
|
return L.divIcon({ html: iconHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 20] });
|
||||||
icon.product = product;
|
|
||||||
return icon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
@@ -149,18 +120,16 @@ async function loadVehicles() {
|
|||||||
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 })
|
||||||
marker.options.vehicleProduct = vehicle.line?.product || '';
|
.bindPopup(`<b>Linie:</b> ${vehicle.line?.name || 'unbekannt'}<br><b>Typ:</b> ${vehicle.line?.product || 'unbekannt'}<br><b>Richtung:</b> ${vehicle.direction || 'unbekannt'}`)
|
||||||
if (activeProducts.includes(marker.options.vehicleProduct)) {
|
.addTo(vehiclesLayer);
|
||||||
marker.addTo(map);
|
|
||||||
}
|
|
||||||
vehicleMarkers[id] = marker;
|
vehicleMarkers[id] = marker;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(vehicleMarkers).forEach(id => {
|
Object.keys(vehicleMarkers).forEach(id => {
|
||||||
if (!newIds.has(id)) {
|
if (!newIds.has(id)) {
|
||||||
if (map.hasLayer(vehicleMarkers[id])) map.removeLayer(vehicleMarkers[id]);
|
vehiclesLayer.removeLayer(vehicleMarkers[id]);
|
||||||
delete vehicleMarkers[id];
|
delete vehicleMarkers[id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -188,3 +157,69 @@ function locateAndLoad() {
|
|||||||
locateAndLoad();
|
locateAndLoad();
|
||||||
setInterval(loadVehicles, 30000);
|
setInterval(loadVehicles, 30000);
|
||||||
map.on('moveend', loadVehicles);
|
map.on('moveend', loadVehicles);
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// Bahnhöfe mit Live-Abfahrten
|
||||||
|
// =============================
|
||||||
|
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) {
|
||||||
|
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 = `<b style="font-size:14px;">${stationName}</b><br>`;
|
||||||
|
html += `<table style="border-collapse:collapse; width:100%; font-size:12px;">`;
|
||||||
|
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>`;
|
||||||
|
|
||||||
|
departures.forEach((dep, index) => {
|
||||||
|
const line = dep.line?.name || "?";
|
||||||
|
const product = dep.line?.product;
|
||||||
|
const icon = symbols[product] || '❓';
|
||||||
|
const direction = dep.direction || "?";
|
||||||
|
const when = dep.when ? new Date(dep.when) : null;
|
||||||
|
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 ? `<span style="color:red;">+${delay} min</span>` : "";
|
||||||
|
|
||||||
|
const color = getLineColor(line, product);
|
||||||
|
const bg = index % 2 === 0 ? '#f9f9f9' : '#fff';
|
||||||
|
|
||||||
|
html += `<tr style="background:${bg};">
|
||||||
|
<td style="padding:4px 6px; font-weight:bold; color:${color}">${icon} ${line}</td>
|
||||||
|
<td style="padding:4px 6px;">${direction}</td>
|
||||||
|
<td style="padding:4px 6px;">${timeStr} ${delayStr}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `</table>`;
|
||||||
|
marker.setPopupContent(html).openPopup();
|
||||||
|
} catch (err) {
|
||||||
|
marker.setPopupContent(`<b>${stationName}</b><br>Fehler beim Laden`);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stations.forEach(station => {
|
||||||
|
const marker = L.marker(station.coords)
|
||||||
|
.addTo(map)
|
||||||
|
.bindPopup(`<b>${station.name}</b><br>Lade Abfahrten...`);
|
||||||
|
|
||||||
|
marker.on('click', () => loadStationDepartures(station.id, marker, station.name));
|
||||||
|
});
|
||||||
+2
-2
@@ -8,8 +8,6 @@
|
|||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="map"></div>
|
|
||||||
|
|
||||||
<div id="controls">
|
<div id="controls">
|
||||||
<label><input type="checkbox" value="subway" checked> U-Bahn</label>
|
<label><input type="checkbox" value="subway" checked> U-Bahn</label>
|
||||||
<label><input type="checkbox" value="suburban" checked> S-Bahn</label>
|
<label><input type="checkbox" value="suburban" checked> S-Bahn</label>
|
||||||
@@ -17,7 +15,9 @@
|
|||||||
<label><input type="checkbox" value="express" checked> Fernzüge</label>
|
<label><input type="checkbox" value="express" checked> Fernzüge</label>
|
||||||
<label><input type="checkbox" value="tram" checked> Tram</label>
|
<label><input type="checkbox" value="tram" checked> Tram</label>
|
||||||
<label><input type="checkbox" value="bus" checked> Bus</label>
|
<label><input type="checkbox" value="bus" checked> Bus</label>
|
||||||
|
<label><input type="checkbox" value="ferry" checked> Fähre</label>
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# VBB-Live-Maps
|
|
||||||
VBB Live Maps ist eine interaktive Live-Karte, die Fahrzeuge des Berliner Nahverkehrs (z. B. Busse, U-Bahnen, S-Bahnen etc.) in Echtzeit anzeigt.
|
|
||||||
|
|
||||||
https://vbb-live-maps.netlify.app/
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
// =============================
|
|
||||||
// Karte initialisieren
|
|
||||||
// =============================
|
|
||||||
const map = L.map('map', { zoomControl: false }).setView([52.52, 13.405], 13);
|
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
maxZoom: 19,
|
|
||||||
attribution: '© OpenStreetMap contributors'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
|
|
||||||
const vehiclesLayer = L.layerGroup().addTo(map);
|
|
||||||
const vehicleMarkers = {};
|
|
||||||
|
|
||||||
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: '🛥️'
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div style="text-align:center; font-size:24px; position: relative; width: 30px; height: 40px;">
|
|
||||||
<svg width="30" height="40" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg" style="position:absolute; top:0; left:0;">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="u12grad" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#7DAD4C" />
|
|
||||||
<stop offset="50%" stop-color="#7DAD4C" />
|
|
||||||
<stop offset="50%" stop-color="#DA421E" />
|
|
||||||
<stop offset="100%" stop-color="#DA421E" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="30" height="40" fill="url(#u12grad)" />
|
|
||||||
</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;">
|
|
||||||
${symbol}
|
|
||||||
</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;">
|
|
||||||
U12
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return L.divIcon({
|
|
||||||
html: iconHtml,
|
|
||||||
className: '',
|
|
||||||
iconSize: [30, 40],
|
|
||||||
iconAnchor: [15, 20]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div style="text-align: center; font-size: 26px; line-height: 1;">
|
|
||||||
<div style="color: ${lineColor}; filter: drop-shadow(0 0 1px rgba(0,0,0,0.5));">${symbol}</div>
|
|
||||||
<div class="line-label" style="${labelStyle}">${lineName}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return L.divIcon({
|
|
||||||
html: iconHtml,
|
|
||||||
className: '',
|
|
||||||
iconSize: [30, 40],
|
|
||||||
iconAnchor: [15, 20]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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=200&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);
|
|
||||||
|
|
||||||
if (vehicleMarkers[id]) {
|
|
||||||
vehicleMarkers[id].setLatLng([loc.latitude, loc.longitude]);
|
|
||||||
} else {
|
|
||||||
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'}
|
|
||||||
`)
|
|
||||||
.addTo(vehiclesLayer);
|
|
||||||
vehicleMarkers[id] = marker;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(vehicleMarkers).forEach(id => {
|
|
||||||
if (!newIds.has(id)) {
|
|
||||||
vehiclesLayer.removeLayer(vehicleMarkers[id]);
|
|
||||||
delete vehicleMarkers[id];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Laden der Fahrzeuge:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function locateAndLoad() {
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
alert('Geolocation nicht unterstützt');
|
|
||||||
loadVehicles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigator.geolocation.getCurrentPosition(pos => {
|
|
||||||
const { latitude, longitude } = pos.coords;
|
|
||||||
map.setView([latitude, longitude], 14);
|
|
||||||
|
|
||||||
const locationIcon = L.divIcon({
|
|
||||||
className: 'user-location-icon',
|
|
||||||
iconSize: [12, 12],
|
|
||||||
iconAnchor: [6, 6]
|
|
||||||
});
|
|
||||||
|
|
||||||
L.marker([latitude, longitude], { icon: locationIcon }).addTo(map);
|
|
||||||
loadVehicles();
|
|
||||||
}, () => {
|
|
||||||
map.setView([52.52, 13.405], 13);
|
|
||||||
loadVehicles();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
locateAndLoad();
|
|
||||||
setInterval(loadVehicles, 610);
|
|
||||||
map.on('moveend', loadVehicles);
|
|
||||||
|
|
||||||
// ====================
|
|
||||||
// Uhr unten rechts
|
|
||||||
// ====================
|
|
||||||
const ssTimeEl = document.getElementById('ss-time');
|
|
||||||
const ssDateEl = document.getElementById('ss-date');
|
|
||||||
|
|
||||||
function updateClock() {
|
|
||||||
const now = new Date();
|
|
||||||
ssTimeEl.textContent = now.toLocaleTimeString('de-DE', { hour12: false });
|
|
||||||
ssDateEl.textContent = now.toLocaleDateString('de-DE', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: '2-digit'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateClock();
|
|
||||||
setInterval(updateClock, 1000);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>VBB Live Tracker</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
||||||
<link rel="stylesheet" href="style.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Karte im Hintergrund -->
|
|
||||||
<div id="map"></div>
|
|
||||||
|
|
||||||
<!-- Uhr-Overlay -->
|
|
||||||
<div id="clock">
|
|
||||||
<div class="time" id="ss-time">--:--:--</div>
|
|
||||||
<div class="date" id="ss-date">----</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
/* ==================== */
|
|
||||||
/* Grundlayout */
|
|
||||||
/* ==================== */
|
|
||||||
html, body, #map {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-marker-icon {
|
|
||||||
transition: transform 1s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-location-icon {
|
|
||||||
background-color: #007bff;
|
|
||||||
border: 2px solid white;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
box-shadow: 0 0 6px rgba(0, 123, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== */
|
|
||||||
/* Uhr unten rechts */
|
|
||||||
/* ==================== */
|
|
||||||
#clock {
|
|
||||||
position: fixed;
|
|
||||||
right: 40px;
|
|
||||||
bottom: 40px;
|
|
||||||
z-index: 9999;
|
|
||||||
text-align: right;
|
|
||||||
font-family: "Comic Sans MS", Comic Sans, cursive; /* Comic Sans einfügen */
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 0 0 8px rgba(0,0,0,0.7);
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
/* Hintergrund-Box */
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#clock .time {
|
|
||||||
font-size: clamp(42px, 6vw, 72px);
|
|
||||||
line-height: 1.1;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
#clock .date {
|
|
||||||
font-size: clamp(18px, 3vw, 28px);
|
|
||||||
line-height: 1.2;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
-225
@@ -1,225 +0,0 @@
|
|||||||
// =============================
|
|
||||||
// Karte initialisieren
|
|
||||||
// =============================
|
|
||||||
const map = L.map('map').setView([52.52, 13.405], 13);
|
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
maxZoom: 19,
|
|
||||||
attribution: '© OpenStreetMap contributors'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
// Layer für Fahrzeuge
|
|
||||||
const vehiclesLayer = L.layerGroup().addTo(map);
|
|
||||||
const vehicleMarkers = {};
|
|
||||||
|
|
||||||
// =============================
|
|
||||||
// Farben & Symbole
|
|
||||||
// =============================
|
|
||||||
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: '🛥️'
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================
|
|
||||||
// Hilfsfunktionen
|
|
||||||
// =============================
|
|
||||||
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 = `
|
|
||||||
<div style="text-align:center; font-size:24px; position: relative; width: 30px; height: 40px;">
|
|
||||||
<svg width="30" height="40" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg" style="position:absolute; top:0; left:0;">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="u12grad" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#7DAD4C" />
|
|
||||||
<stop offset="50%" stop-color="#7DAD4C" />
|
|
||||||
<stop offset="50%" stop-color="#DA421E" />
|
|
||||||
<stop offset="100%" stop-color="#DA421E" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="30" height="40" fill="url(#u12grad)" />
|
|
||||||
</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;">
|
|
||||||
${symbol}
|
|
||||||
</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;">
|
|
||||||
U12
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return L.divIcon({ html: iconHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 20] });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div style="text-align: center; font-size: 26px; line-height: 1;">
|
|
||||||
<div style="color: ${lineColor}; filter: drop-shadow(0 0 1px rgba(0,0,0,0.5));">${symbol}</div>
|
|
||||||
<div class="line-label" style="${labelStyle}">${lineName}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return L.divIcon({ html: iconHtml, className: '', iconSize: [30, 40], iconAnchor: [15, 20] });
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================
|
|
||||||
// Fahrzeuge laden
|
|
||||||
// =============================
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (vehicleMarkers[id]) {
|
|
||||||
vehicleMarkers[id].setLatLng([loc.latitude, loc.longitude]);
|
|
||||||
} else {
|
|
||||||
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'}`)
|
|
||||||
.addTo(vehiclesLayer);
|
|
||||||
vehicleMarkers[id] = marker;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(vehicleMarkers).forEach(id => {
|
|
||||||
if (!newIds.has(id)) {
|
|
||||||
vehiclesLayer.removeLayer(vehicleMarkers[id]);
|
|
||||||
delete vehicleMarkers[id];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Laden der Fahrzeuge:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================
|
|
||||||
// Geolocation & Auto-Update
|
|
||||||
// =============================
|
|
||||||
function locateAndLoad() {
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
loadVehicles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigator.geolocation.getCurrentPosition(pos => {
|
|
||||||
map.setView([pos.coords.latitude, pos.coords.longitude], 14);
|
|
||||||
loadVehicles();
|
|
||||||
}, () => {
|
|
||||||
loadVehicles();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
locateAndLoad();
|
|
||||||
setInterval(loadVehicles, 30000);
|
|
||||||
map.on('moveend', loadVehicles);
|
|
||||||
|
|
||||||
// =============================
|
|
||||||
// Bahnhöfe mit Live-Abfahrten
|
|
||||||
// =============================
|
|
||||||
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) {
|
|
||||||
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 = `<b style="font-size:14px;">${stationName}</b><br>`;
|
|
||||||
html += `<table style="border-collapse:collapse; width:100%; font-size:12px;">`;
|
|
||||||
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>`;
|
|
||||||
|
|
||||||
departures.forEach((dep, index) => {
|
|
||||||
const line = dep.line?.name || "?";
|
|
||||||
const product = dep.line?.product;
|
|
||||||
const icon = symbols[product] || '❓';
|
|
||||||
const direction = dep.direction || "?";
|
|
||||||
const when = dep.when ? new Date(dep.when) : null;
|
|
||||||
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 ? `<span style="color:red;">+${delay} min</span>` : "";
|
|
||||||
|
|
||||||
const color = getLineColor(line, product);
|
|
||||||
const bg = index % 2 === 0 ? '#f9f9f9' : '#fff';
|
|
||||||
|
|
||||||
html += `<tr style="background:${bg};">
|
|
||||||
<td style="padding:4px 6px; font-weight:bold; color:${color}">${icon} ${line}</td>
|
|
||||||
<td style="padding:4px 6px;">${direction}</td>
|
|
||||||
<td style="padding:4px 6px;">${timeStr} ${delayStr}</td>
|
|
||||||
</tr>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
html += `</table>`;
|
|
||||||
marker.setPopupContent(html).openPopup();
|
|
||||||
} catch (err) {
|
|
||||||
marker.setPopupContent(`<b>${stationName}</b><br>Fehler beim Laden`);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
stations.forEach(station => {
|
|
||||||
const marker = L.marker(station.coords)
|
|
||||||
.addTo(map)
|
|
||||||
.bindPopup(`<b>${station.name}</b><br>Lade Abfahrten...`);
|
|
||||||
|
|
||||||
marker.on('click', () => loadStationDepartures(station.id, marker, station.name));
|
|
||||||
});
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
const map = L.map('map').setView([52.52, 13.405], 13);
|
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
maxZoom: 19,
|
|
||||||
attribution: '© OpenStreetMap contributors'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const vehiclesLayer = L.layerGroup().addTo(map);
|
|
||||||
const vehicleMarkers = {};
|
|
||||||
|
|
||||||
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: '🛥️'
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div style="text-align:center; font-size:24px; position: relative; width: 30px; height: 40px;">
|
|
||||||
<svg width="30" height="40" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg" style="position:absolute; top:0; left:0;">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="u12grad" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#7DAD4C" />
|
|
||||||
<stop offset="50%" stop-color="#7DAD4C" />
|
|
||||||
<stop offset="50%" stop-color="#DA421E" />
|
|
||||||
<stop offset="100%" stop-color="#DA421E" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="30" height="40" fill="url(#u12grad)" />
|
|
||||||
</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;">
|
|
||||||
${symbol}
|
|
||||||
</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;">
|
|
||||||
U12
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return L.divIcon({
|
|
||||||
html: iconHtml,
|
|
||||||
className: '',
|
|
||||||
iconSize: [30, 40],
|
|
||||||
iconAnchor: [15, 20]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div style="text-align: center; font-size: 26px; line-height: 1;">
|
|
||||||
<div style="color: ${lineColor}; filter: drop-shadow(0 0 1px rgba(0,0,0,0.5));">${symbol}</div>
|
|
||||||
<div class="line-label" style="${labelStyle}">${lineName}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return L.divIcon({
|
|
||||||
html: iconHtml,
|
|
||||||
className: '',
|
|
||||||
iconSize: [30, 40],
|
|
||||||
iconAnchor: [15, 20]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (vehicleMarkers[id]) {
|
|
||||||
vehicleMarkers[id].setLatLng([loc.latitude, loc.longitude]);
|
|
||||||
} else {
|
|
||||||
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'}
|
|
||||||
`)
|
|
||||||
.addTo(vehiclesLayer);
|
|
||||||
vehicleMarkers[id] = marker;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(vehicleMarkers).forEach(id => {
|
|
||||||
if (!newIds.has(id)) {
|
|
||||||
vehiclesLayer.removeLayer(vehicleMarkers[id]);
|
|
||||||
delete vehicleMarkers[id];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Laden der Fahrzeuge:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function locateAndLoad() {
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
alert('Geolocation nicht unterstützt');
|
|
||||||
loadVehicles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigator.geolocation.getCurrentPosition(pos => {
|
|
||||||
const { latitude, longitude } = pos.coords;
|
|
||||||
map.setView([latitude, longitude], 14);
|
|
||||||
|
|
||||||
const locationIcon = L.divIcon({
|
|
||||||
className: 'user-location-icon',
|
|
||||||
iconSize: [12, 12],
|
|
||||||
iconAnchor: [6, 6]
|
|
||||||
});
|
|
||||||
|
|
||||||
L.marker([latitude, longitude], { icon: locationIcon }).addTo(map);
|
|
||||||
loadVehicles();
|
|
||||||
}, () => {
|
|
||||||
map.setView([52.52, 13.405], 13);
|
|
||||||
loadVehicles();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
locateAndLoad();
|
|
||||||
setInterval(loadVehicles, 610);
|
|
||||||
map.on('moveend', loadVehicles);
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>VBB Live Tracker</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
||||||
<link rel="stylesheet" href="style.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="controls">
|
|
||||||
<label><input type="checkbox" value="subway" checked> U-Bahn</label>
|
|
||||||
<label><input type="checkbox" value="suburban" checked> S-Bahn</label>
|
|
||||||
<label><input type="checkbox" value="regional" checked> Regio</label>
|
|
||||||
<label><input type="checkbox" value="express" checked> Fernzüge</label>
|
|
||||||
<label><input type="checkbox" value="tram" checked> Tram</label>
|
|
||||||
<label><input type="checkbox" value="bus" checked> Bus</label>
|
|
||||||
<label><input type="checkbox" value="ferry" checked> Fähre</label>
|
|
||||||
</div>
|
|
||||||
<div id="map"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
html, body, #map {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap; /* wenn zu viele, umbrechen */
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controls label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-marker-icon {
|
|
||||||
transition: transform 1s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user