Verbessere HTML-Code: Semantik, Accessibility und Performance
Umfassende Verbesserungen am HTML/CSS/JS-Code: HTML & Meta-Tags: - Erweiterte Meta-Tags für SEO (description, keywords, author) - Open Graph Tags für Social Media Sharing - Twitter Card Meta-Tags - Theme-color für mobile Geräte - Preconnect und DNS-Prefetch für externe Ressourcen - SRI-Hashes für Leaflet-Ressourcen Semantik & Accessibility: - Semantische HTML5-Elemente (<main>, <aside>, <time>) - ARIA-Labels und Rollen (role="application", role="complementary") - ARIA-Live-Regionen für dynamische Inhalte (aria-live="polite") - Verbesserte Strukturierung für Screenreader Performance: - defer für externe Scripts - Preconnect für kritische externe Domains - DNS-Prefetch für Tile-Server Code-Qualität: - Umfassende JSDoc-Kommentare für alle Funktionen - CSS-Abschnitte mit Kommentaren strukturiert - 'use strict' Modus aktiviert - Konstanten für magische Werte (BERLIN_CENTER, DEFAULT_ZOOM) Fehlerbehandlung: - Request Timeout (10 Sekunden) mit AbortController - Differenzierte Fehlerbehandlung (Timeout, Network, Generic) - Null-Checks für DOM-Elemente - Array.isArray() Validierung für API-Daten - Benutzerfreundliche Console-Logs Code-Organisation: - Logische Gruppierung mit Kommentar-Headern - Konsistente Formatierung - Verbesserte Lesbarkeit durch Inline-Kommentare - Bessere Variablennamen 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 file changed
+246
-32
+246
-32
@@ -3,8 +3,33 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Berlin Live Transit</title>
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
|
<title>Berlin Live Transit - Echtzeit-Nahverkehrskarte</title>
|
||||||
|
<meta name="title" content="Berlin Live Transit - Echtzeit-Nahverkehrskarte">
|
||||||
|
<meta name="description" content="Verfolgen Sie den Berliner Nahverkehr in Echtzeit. U-Bahn, S-Bahn, Trams, Busse und Fähren live auf der Karte.">
|
||||||
|
<meta name="keywords" content="Berlin, Transit, Nahverkehr, Live, Karte, U-Bahn, S-Bahn, Tram, Bus, BVG, VBB">
|
||||||
|
<meta name="author" content="Berlin Live Transit">
|
||||||
|
<meta name="theme-color" content="#0a0e27">
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://example.com/">
|
||||||
|
<meta property="og:title" content="Berlin Live Transit - Echtzeit-Nahverkehrskarte">
|
||||||
|
<meta property="og:description" content="Verfolgen Sie den Berliner Nahverkehr in Echtzeit. U-Bahn, S-Bahn, Trams, Busse und Fähren live auf der Karte.">
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image">
|
||||||
|
<meta property="twitter:url" content="https://example.com/">
|
||||||
|
<meta property="twitter:title" content="Berlin Live Transit - Echtzeit-Nahverkehrskarte">
|
||||||
|
<meta property="twitter:description" content="Verfolgen Sie den Berliner Nahverkehr in Echtzeit. U-Bahn, S-Bahn, Trams, Busse und Fähren live auf der Karte.">
|
||||||
|
|
||||||
|
<!-- Preconnect to external resources for better performance -->
|
||||||
|
<link rel="preconnect" href="https://unpkg.com" crossorigin>
|
||||||
|
<link rel="preconnect" href="https://v6.vbb.transport.rest" crossorigin>
|
||||||
|
<link rel="dns-prefetch" href="https://basemaps.cartocdn.com">
|
||||||
<style>
|
<style>
|
||||||
|
/* ===== Base Styles ===== */
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -20,6 +45,7 @@
|
|||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Map Container ===== */
|
||||||
#map {
|
#map {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -31,6 +57,7 @@
|
|||||||
cursor: none !important;
|
cursor: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Clock Display ===== */
|
||||||
.clock-container {
|
.clock-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 40px;
|
bottom: 40px;
|
||||||
@@ -73,6 +100,7 @@
|
|||||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Loading Indicator ===== */
|
||||||
.loading-indicator {
|
.loading-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 40px;
|
bottom: 40px;
|
||||||
@@ -115,6 +143,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Vehicle Markers ===== */
|
||||||
.vehicle-marker-container {
|
.vehicle-marker-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
@@ -191,6 +220,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Vehicle Type Colors ===== */
|
||||||
.vehicle-subway {
|
.vehicle-subway {
|
||||||
background: #0066cc;
|
background: #0066cc;
|
||||||
color: #0066cc;
|
color: #0066cc;
|
||||||
@@ -216,6 +246,7 @@
|
|||||||
color: #cc6600;
|
color: #cc6600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== User Location Marker ===== */
|
||||||
.user-location-marker {
|
.user-location-marker {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@@ -262,6 +293,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Leaflet Popup Customization ===== */
|
||||||
.leaflet-popup-content-wrapper {
|
.leaflet-popup-content-wrapper {
|
||||||
background: rgba(20, 20, 30, 0.98);
|
background: rgba(20, 20, 30, 0.98);
|
||||||
backdrop-filter: blur(15px);
|
backdrop-filter: blur(15px);
|
||||||
@@ -280,6 +312,7 @@
|
|||||||
background: rgba(20, 20, 30, 0.98);
|
background: rgba(20, 20, 30, 0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Responsive Design ===== */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.clock-container {
|
.clock-container {
|
||||||
padding: 25px 30px;
|
padding: 25px 30px;
|
||||||
@@ -296,28 +329,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verstecke Cursor überall */
|
/* ===== Cursor Styles ===== */
|
||||||
* {
|
* {
|
||||||
cursor: none !important;
|
cursor: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<!-- External Resources -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||||
|
crossorigin="" />
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
|
crossorigin=""
|
||||||
|
defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="map"></div>
|
<!-- Main Application Container -->
|
||||||
|
<main id="app" role="main">
|
||||||
|
<!-- Interactive Map -->
|
||||||
|
<div id="map" role="application" aria-label="Berlin Transit Live Map"></div>
|
||||||
|
|
||||||
<div class="clock-container">
|
<!-- Clock Display -->
|
||||||
<div class="time" id="time"></div>
|
<aside class="clock-container" role="complementary" aria-label="Aktuelle Uhrzeit und Datum">
|
||||||
<div class="date" id="date"></div>
|
<time class="time" id="time" aria-live="polite" aria-atomic="true"></time>
|
||||||
</div>
|
<div class="date" id="date" aria-live="polite" aria-atomic="true"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<div class="loading-indicator" id="loadingIndicator">
|
<!-- Loading Indicator -->
|
||||||
<div class="spinner"></div>
|
<div class="loading-indicator"
|
||||||
<span class="loading-text">Lade Fahrzeuge...</span>
|
id="loadingIndicator"
|
||||||
</div>
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="Ladezustand">
|
||||||
|
<div class="spinner" aria-hidden="true"></div>
|
||||||
|
<span class="loading-text">Lade Fahrzeuge...</span>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// ===== Application Initialization =====
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Leaflet map with custom settings
|
||||||
|
* Disables zoom controls for cleaner interface
|
||||||
|
*/
|
||||||
const map = L.map('map', {
|
const map = L.map('map', {
|
||||||
zoomControl: false,
|
zoomControl: false,
|
||||||
attributionControl: true,
|
attributionControl: true,
|
||||||
@@ -326,29 +383,55 @@
|
|||||||
markerZoomAnimation: true
|
markerZoomAnimation: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add dark theme tile layer from CartoDB
|
||||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
attribution: '© OpenStreetMap © CartoDB'
|
attribution: '© OpenStreetMap © CartoDB'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
const berlinCenter = [52.52, 13.405];
|
// Set initial view to Berlin center
|
||||||
map.setView(berlinCenter, 14);
|
const BERLIN_CENTER = [52.52, 13.405];
|
||||||
|
const DEFAULT_ZOOM = 14;
|
||||||
|
map.setView(BERLIN_CENTER, DEFAULT_ZOOM);
|
||||||
|
|
||||||
|
// ===== Application State =====
|
||||||
const vehicleMarkers = {};
|
const vehicleMarkers = {};
|
||||||
let userLocationMarker = null;
|
let userLocationMarker = null;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let userPosition = null;
|
let userPosition = null;
|
||||||
|
let updateInterval = null;
|
||||||
|
|
||||||
|
// ===== UI Helper Functions =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the loading indicator
|
||||||
|
*/
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
document.getElementById('loadingIndicator').classList.add('show');
|
const indicator = document.getElementById('loadingIndicator');
|
||||||
isLoading = true;
|
if (indicator) {
|
||||||
|
indicator.classList.add('show');
|
||||||
|
isLoading = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the loading indicator
|
||||||
|
*/
|
||||||
function hideLoading() {
|
function hideLoading() {
|
||||||
document.getElementById('loadingIndicator').classList.remove('show');
|
const indicator = document.getElementById('loadingIndicator');
|
||||||
isLoading = false;
|
if (indicator) {
|
||||||
|
indicator.classList.remove('show');
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Transit Product Type Helpers =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the vehicle type from product string
|
||||||
|
* @param {string} product - Product type from API
|
||||||
|
* @returns {string} Normalized product type
|
||||||
|
*/
|
||||||
function getProductType(product) {
|
function getProductType(product) {
|
||||||
if (!product) return 'bus';
|
if (!product) return 'bus';
|
||||||
const p = product.toLowerCase();
|
const p = product.toLowerCase();
|
||||||
@@ -361,6 +444,11 @@
|
|||||||
return 'bus';
|
return 'bus';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color associated with vehicle type
|
||||||
|
* @param {string} type - Vehicle type
|
||||||
|
* @returns {string} Hex color code
|
||||||
|
*/
|
||||||
function getProductColor(type) {
|
function getProductColor(type) {
|
||||||
const colors = {
|
const colors = {
|
||||||
subway: '#0066cc',
|
subway: '#0066cc',
|
||||||
@@ -373,6 +461,11 @@
|
|||||||
return colors[type] || '#cc44cc';
|
return colors[type] || '#cc44cc';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get icon letter for vehicle type
|
||||||
|
* @param {string} type - Vehicle type
|
||||||
|
* @returns {string} Single letter icon
|
||||||
|
*/
|
||||||
function getProductIcon(type) {
|
function getProductIcon(type) {
|
||||||
const icons = {
|
const icons = {
|
||||||
subway: 'U',
|
subway: 'U',
|
||||||
@@ -385,6 +478,11 @@
|
|||||||
return icons[type] || 'B';
|
return icons[type] || 'B';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get German name for vehicle type
|
||||||
|
* @param {string} type - Vehicle type
|
||||||
|
* @returns {string} Localized name
|
||||||
|
*/
|
||||||
function getProductName(type) {
|
function getProductName(type) {
|
||||||
const names = {
|
const names = {
|
||||||
subway: 'U-Bahn',
|
subway: 'U-Bahn',
|
||||||
@@ -397,6 +495,14 @@
|
|||||||
return names[type] || 'Bus';
|
return names[type] || 'Bus';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Marker Creation Functions =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom icon for vehicle markers
|
||||||
|
* @param {string} type - Vehicle type
|
||||||
|
* @param {string} lineName - Line name to display
|
||||||
|
* @returns {L.DivIcon} Leaflet div icon
|
||||||
|
*/
|
||||||
function createVehicleIcon(type, lineName) {
|
function createVehicleIcon(type, lineName) {
|
||||||
const icon = getProductIcon(type);
|
const icon = getProductIcon(type);
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
@@ -412,6 +518,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom icon for user location marker
|
||||||
|
* @returns {L.DivIcon} Leaflet div icon with pulse effect
|
||||||
|
*/
|
||||||
function createUserLocationIcon() {
|
function createUserLocationIcon() {
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
className: 'user-location-icon',
|
className: 'user-location-icon',
|
||||||
@@ -426,49 +536,85 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Data Loading Functions =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load vehicle data from VBB Radar API
|
||||||
|
* Fetches vehicles within current map bounds
|
||||||
|
* Updates existing markers or creates new ones
|
||||||
|
*/
|
||||||
async function loadRadarData() {
|
async function loadRadarData() {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get current map bounds for API request
|
||||||
const bounds = map.getBounds();
|
const bounds = map.getBounds();
|
||||||
const north = bounds.getNorth();
|
const north = bounds.getNorth().toFixed(6);
|
||||||
const south = bounds.getSouth();
|
const south = bounds.getSouth().toFixed(6);
|
||||||
const west = bounds.getWest();
|
const west = bounds.getWest().toFixed(6);
|
||||||
const east = bounds.getEast();
|
const east = bounds.getEast().toFixed(6);
|
||||||
|
|
||||||
|
// Build API URL with bounds
|
||||||
const radarUrl = `https://v6.vbb.transport.rest/radar?north=${north}&south=${south}&west=${west}&east=${east}&results=150`;
|
const radarUrl = `https://v6.vbb.transport.rest/radar?north=${north}&south=${south}&west=${west}&east=${east}&results=150`;
|
||||||
|
|
||||||
const response = await fetch(radarUrl);
|
// Fetch vehicle data with timeout
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||||
|
|
||||||
|
const response = await fetch(radarUrl, {
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP Error ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Validate response data
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
throw new Error('Invalid response data format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track current vehicle IDs to remove stale markers
|
||||||
const currentVehicleIds = new Set();
|
const currentVehicleIds = new Set();
|
||||||
|
|
||||||
if (data.movements) {
|
// Process vehicle movements from API
|
||||||
|
if (data.movements && Array.isArray(data.movements)) {
|
||||||
data.movements.forEach(movement => {
|
data.movements.forEach(movement => {
|
||||||
|
// Validate movement data
|
||||||
|
if (!movement || !movement.location || !movement.line) {
|
||||||
|
return; // Skip invalid movement
|
||||||
|
}
|
||||||
|
|
||||||
if (movement.location && movement.line) {
|
if (movement.location && movement.line) {
|
||||||
const type = getProductType(movement.line.product);
|
const type = getProductType(movement.line.product);
|
||||||
const lineName = movement.line.name || '?';
|
const lineName = movement.line.name || '?';
|
||||||
const icon = getProductIcon(type);
|
const icon = getProductIcon(type);
|
||||||
|
|
||||||
|
// Create unique vehicle ID
|
||||||
const vehicleId = movement.tripId || `${lineName}-${movement.direction || 'unknown'}`;
|
const vehicleId = movement.tripId || `${lineName}-${movement.direction || 'unknown'}`;
|
||||||
currentVehicleIds.add(vehicleId);
|
currentVehicleIds.add(vehicleId);
|
||||||
|
|
||||||
|
// Create new position
|
||||||
const newLatLng = L.latLng(movement.location.latitude, movement.location.longitude);
|
const newLatLng = L.latLng(movement.location.latitude, movement.location.longitude);
|
||||||
|
|
||||||
|
// Update existing marker or create new one
|
||||||
if (vehicleMarkers[vehicleId]) {
|
if (vehicleMarkers[vehicleId]) {
|
||||||
|
// Vehicle exists - update position with animation
|
||||||
const marker = vehicleMarkers[vehicleId].marker;
|
const marker = vehicleMarkers[vehicleId].marker;
|
||||||
const oldLatLng = marker.getLatLng();
|
const oldLatLng = marker.getLatLng();
|
||||||
|
|
||||||
|
// Only animate if position changed
|
||||||
if (oldLatLng.lat !== newLatLng.lat || oldLatLng.lng !== newLatLng.lng) {
|
if (oldLatLng.lat !== newLatLng.lat || oldLatLng.lng !== newLatLng.lng) {
|
||||||
animateMarker(marker, oldLatLng, newLatLng, 3000);
|
animateMarker(marker, oldLatLng, newLatLng, 3000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Create new marker for vehicle
|
||||||
const marker = L.marker(newLatLng, {
|
const marker = L.marker(newLatLng, {
|
||||||
icon: createVehicleIcon(type, lineName),
|
icon: createVehicleIcon(type, lineName),
|
||||||
riseOnHover: true
|
riseOnHover: true
|
||||||
@@ -477,8 +623,9 @@
|
|||||||
const direction = movement.direction || 'Unbekannt';
|
const direction = movement.direction || 'Unbekannt';
|
||||||
const productName = getProductName(type);
|
const productName = getProductName(type);
|
||||||
|
|
||||||
|
// Build next stop information if available
|
||||||
let nextStopInfo = '';
|
let nextStopInfo = '';
|
||||||
if (movement.nextStopovers && movement.nextStopovers.length > 0) {
|
if (movement.nextStopovers && Array.isArray(movement.nextStopovers) && movement.nextStopovers.length > 0) {
|
||||||
const nextStop = movement.nextStopovers[0];
|
const nextStop = movement.nextStopovers[0];
|
||||||
if (nextStop.stop) {
|
if (nextStop.stop) {
|
||||||
const arrivalTime = nextStop.arrival ?
|
const arrivalTime = nextStop.arrival ?
|
||||||
@@ -545,6 +692,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove vehicles that are no longer in the response
|
||||||
Object.keys(vehicleMarkers).forEach(vehicleId => {
|
Object.keys(vehicleMarkers).forEach(vehicleId => {
|
||||||
if (!currentVehicleIds.has(vehicleId)) {
|
if (!currentVehicleIds.has(vehicleId)) {
|
||||||
map.removeLayer(vehicleMarkers[vehicleId].marker);
|
map.removeLayer(vehicleMarkers[vehicleId].marker);
|
||||||
@@ -555,12 +703,31 @@
|
|||||||
console.log(`${Object.keys(vehicleMarkers).length} Fahrzeuge aktiv`);
|
console.log(`${Object.keys(vehicleMarkers).length} Fahrzeuge aktiv`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden:', error);
|
// Handle different error types
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.warn('Request timeout - Fahrzeugdaten konnten nicht geladen werden');
|
||||||
|
} else if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||||
|
console.error('Netzwerkfehler - Bitte Internetverbindung prüfen:', error);
|
||||||
|
} else {
|
||||||
|
console.error('Fehler beim Laden der Fahrzeugdaten:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show user-friendly error in UI (optional)
|
||||||
|
// You could add an error notification here
|
||||||
} finally {
|
} finally {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Animation Functions =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smoothly animate marker from one position to another
|
||||||
|
* @param {L.Marker} marker - Leaflet marker to animate
|
||||||
|
* @param {L.LatLng} startLatLng - Starting position
|
||||||
|
* @param {L.LatLng} endLatLng - Ending position
|
||||||
|
* @param {number} duration - Animation duration in milliseconds
|
||||||
|
*/
|
||||||
function animateMarker(marker, startLatLng, endLatLng, duration) {
|
function animateMarker(marker, startLatLng, endLatLng, duration) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const latDiff = endLatLng.lat - startLatLng.lat;
|
const latDiff = endLatLng.lat - startLatLng.lat;
|
||||||
@@ -570,6 +737,7 @@
|
|||||||
const elapsed = Date.now() - startTime;
|
const elapsed = Date.now() - startTime;
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
|
||||||
|
// Ease in-out cubic interpolation for smooth movement
|
||||||
const easeProgress = progress < 0.5
|
const easeProgress = progress < 0.5
|
||||||
? 2 * progress * progress
|
? 2 * progress * progress
|
||||||
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
||||||
@@ -579,6 +747,7 @@
|
|||||||
|
|
||||||
marker.setLatLng([currentLat, currentLng]);
|
marker.setLatLng([currentLat, currentLng]);
|
||||||
|
|
||||||
|
// Continue animation until complete
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
requestAnimationFrame(updatePosition);
|
requestAnimationFrame(updatePosition);
|
||||||
}
|
}
|
||||||
@@ -587,28 +756,39 @@
|
|||||||
updatePosition();
|
updatePosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Geolocation Functions =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's current location and center map on it
|
||||||
|
* Falls back to Berlin center if geolocation is unavailable
|
||||||
|
*/
|
||||||
function getUserLocation() {
|
function getUserLocation() {
|
||||||
if (navigator.geolocation) {
|
if (navigator.geolocation) {
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
// Success callback
|
||||||
(position) => {
|
(position) => {
|
||||||
const lat = position.coords.latitude;
|
const lat = position.coords.latitude;
|
||||||
const lon = position.coords.longitude;
|
const lon = position.coords.longitude;
|
||||||
userPosition = [lat, lon];
|
userPosition = [lat, lon];
|
||||||
|
|
||||||
|
// Center map on user location
|
||||||
map.setView([lat, lon], 16, {
|
map.setView([lat, lon], 16, {
|
||||||
animate: true,
|
animate: true,
|
||||||
duration: 1
|
duration: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove old location marker if exists
|
||||||
if (userLocationMarker) {
|
if (userLocationMarker) {
|
||||||
map.removeLayer(userLocationMarker);
|
map.removeLayer(userLocationMarker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new location marker
|
||||||
userLocationMarker = L.marker([lat, lon], {
|
userLocationMarker = L.marker([lat, lon], {
|
||||||
icon: createUserLocationIcon(),
|
icon: createUserLocationIcon(),
|
||||||
zIndexOffset: 1000
|
zIndexOffset: 1000
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add popup with coordinates
|
||||||
userLocationMarker.bindPopup(`
|
userLocationMarker.bindPopup(`
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<div style="font-size: 32px; margin-bottom: 10px;">📍</div>
|
<div style="font-size: 32px; margin-bottom: 10px;">📍</div>
|
||||||
@@ -624,10 +804,13 @@
|
|||||||
console.log('Standort gefunden:', lat, lon);
|
console.log('Standort gefunden:', lat, lon);
|
||||||
loadRadarData();
|
loadRadarData();
|
||||||
},
|
},
|
||||||
|
// Error callback
|
||||||
(error) => {
|
(error) => {
|
||||||
console.log('Standort nicht verfügbar:', error);
|
console.warn('Standort nicht verfügbar:', error.message);
|
||||||
|
// Load data with default Berlin view
|
||||||
loadRadarData();
|
loadRadarData();
|
||||||
},
|
},
|
||||||
|
// Options
|
||||||
{
|
{
|
||||||
enableHighAccuracy: true,
|
enableHighAccuracy: true,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
@@ -635,20 +818,31 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('Geolocation nicht unterstützt');
|
console.warn('Geolocation wird von diesem Browser nicht unterstützt');
|
||||||
loadRadarData();
|
loadRadarData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Clock Functions =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update clock display with current time and date
|
||||||
|
* Formats time as HH:MM:SS and date in German format
|
||||||
|
*/
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
// Format time as HH:MM:SS
|
||||||
const hours = String(now.getHours()).padStart(2, '0');
|
const hours = String(now.getHours()).padStart(2, '0');
|
||||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
document.getElementById('time').textContent = `${hours}:${minutes}:${seconds}`;
|
const timeElement = document.getElementById('time');
|
||||||
|
if (timeElement) {
|
||||||
|
timeElement.textContent = `${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date in German locale
|
||||||
const options = {
|
const options = {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -656,9 +850,18 @@
|
|||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
};
|
};
|
||||||
const dateStr = now.toLocaleDateString('de-DE', options);
|
const dateStr = now.toLocaleDateString('de-DE', options);
|
||||||
document.getElementById('date').textContent = dateStr;
|
const dateElement = document.getElementById('date');
|
||||||
|
if (dateElement) {
|
||||||
|
dateElement.textContent = dateStr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Event Listeners =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounced map movement handler
|
||||||
|
* Reloads vehicle data when map stops moving
|
||||||
|
*/
|
||||||
let moveEndTimeout;
|
let moveEndTimeout;
|
||||||
map.on('moveend', () => {
|
map.on('moveend', () => {
|
||||||
clearTimeout(moveEndTimeout);
|
clearTimeout(moveEndTimeout);
|
||||||
@@ -667,15 +870,26 @@
|
|||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== Application Startup =====
|
||||||
|
|
||||||
|
// Initialize clock display
|
||||||
updateClock();
|
updateClock();
|
||||||
|
|
||||||
|
// Get user location and load initial data
|
||||||
getUserLocation();
|
getUserLocation();
|
||||||
|
|
||||||
|
// Update clock every second
|
||||||
setInterval(updateClock, 1000);
|
setInterval(updateClock, 1000);
|
||||||
|
|
||||||
|
// Refresh vehicle data every 15 seconds
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
loadRadarData();
|
loadRadarData();
|
||||||
}
|
}
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
|
// Log application start
|
||||||
|
console.log('Berlin Live Transit gestartet');
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user