Add files via upload
This commit is contained in:
1 file changed
+681
+681
@@ -0,0 +1,681 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Berlin Live Transit</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #0a0e27;
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
.clock-container {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: 40px;
|
||||
z-index: 1000;
|
||||
background: rgba(20, 20, 30, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 35px 45px;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||||
min-width: 350px;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clock-container:hover {
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.7);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 84px;
|
||||
font-weight: 200;
|
||||
color: rgba(255, 255, 255, 0.98);
|
||||
letter-spacing: -2px;
|
||||
line-height: 1;
|
||||
text-shadow: 0 3px 20px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
margin-top: 8px;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
background: rgba(20, 20, 30, 0.9);
|
||||
backdrop-filter: blur(15px);
|
||||
padding: 15px 30px;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loading-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #00ffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vehicle-marker-container {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.vehicle-marker {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
animation: vehicle-pulse 2s ease-in-out infinite;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
text-shadow:
|
||||
0 0 8px currentColor,
|
||||
0 0 15px currentColor,
|
||||
0 2px 4px rgba(0, 0, 0, 0.8);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.6));
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.vehicle-marker:hover {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
|
||||
@keyframes vehicle-pulse {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vehicle-ripple {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-marker-icon {
|
||||
transition: transform 10s linear !important;
|
||||
}
|
||||
|
||||
.vehicle-label {
|
||||
position: absolute;
|
||||
top: -26px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.6);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
letter-spacing: 0.5px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vehicle-subway {
|
||||
background: #0066cc;
|
||||
color: #0066cc;
|
||||
}
|
||||
.vehicle-suburban {
|
||||
background: #009933;
|
||||
color: #009933;
|
||||
}
|
||||
.vehicle-tram {
|
||||
background: #cc0000;
|
||||
color: #cc0000;
|
||||
}
|
||||
.vehicle-bus {
|
||||
background: #993399;
|
||||
color: #993399;
|
||||
}
|
||||
.vehicle-ferry {
|
||||
background: #0099cc;
|
||||
color: #0099cc;
|
||||
}
|
||||
.vehicle-regional {
|
||||
background: #cc6600;
|
||||
color: #cc6600;
|
||||
}
|
||||
|
||||
.user-location-marker {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.user-location-dot {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #00ffff;
|
||||
border: 4px solid white;
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow:
|
||||
0 0 20px rgba(0, 255, 255, 0.8),
|
||||
0 0 40px rgba(0, 255, 255, 0.6);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.user-location-pulse {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #00ffff;
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: location-pulse 2s ease-out infinite;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes location-pulse {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: rgba(20, 20, 30, 0.98);
|
||||
backdrop-filter: blur(15px);
|
||||
color: white;
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: rgba(20, 20, 30, 0.98);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.clock-container {
|
||||
padding: 25px 30px;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Verstecke Cursor überall */
|
||||
* {
|
||||
cursor: none !important;
|
||||
}
|
||||
</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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
|
||||
<div class="clock-container">
|
||||
<div class="time" id="time"></div>
|
||||
<div class="date" id="date"></div>
|
||||
</div>
|
||||
|
||||
<div class="loading-indicator" id="loadingIndicator">
|
||||
<div class="spinner"></div>
|
||||
<span class="loading-text">Lade Fahrzeuge...</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const map = L.map('map', {
|
||||
zoomControl: false,
|
||||
attributionControl: true,
|
||||
zoomAnimation: true,
|
||||
fadeAnimation: true,
|
||||
markerZoomAnimation: true
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CartoDB'
|
||||
}).addTo(map);
|
||||
|
||||
const berlinCenter = [52.52, 13.405];
|
||||
map.setView(berlinCenter, 14);
|
||||
|
||||
const vehicleMarkers = {};
|
||||
let userLocationMarker = null;
|
||||
let isLoading = false;
|
||||
let userPosition = null;
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('loadingIndicator').classList.add('show');
|
||||
isLoading = true;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loadingIndicator').classList.remove('show');
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function getProductType(product) {
|
||||
if (!product) return 'bus';
|
||||
const p = product.toLowerCase();
|
||||
if (p.includes('subway') || p === 'subway') return 'subway';
|
||||
if (p.includes('suburban') || p === 'suburban') return 'suburban';
|
||||
if (p.includes('tram') || p === 'tram') return 'tram';
|
||||
if (p.includes('bus') || p === 'bus') return 'bus';
|
||||
if (p.includes('ferry')) return 'ferry';
|
||||
if (p.includes('regional') || p.includes('express')) return 'regional';
|
||||
return 'bus';
|
||||
}
|
||||
|
||||
function getProductColor(type) {
|
||||
const colors = {
|
||||
subway: '#0066cc',
|
||||
suburban: '#00cc44',
|
||||
tram: '#ff3333',
|
||||
bus: '#cc44cc',
|
||||
ferry: '#00ccff',
|
||||
regional: '#ff9933'
|
||||
};
|
||||
return colors[type] || '#cc44cc';
|
||||
}
|
||||
|
||||
function getProductIcon(type) {
|
||||
const icons = {
|
||||
subway: 'U',
|
||||
suburban: 'S',
|
||||
tram: 'T',
|
||||
bus: 'B',
|
||||
ferry: 'F',
|
||||
regional: 'R'
|
||||
};
|
||||
return icons[type] || 'B';
|
||||
}
|
||||
|
||||
function getProductName(type) {
|
||||
const names = {
|
||||
subway: 'U-Bahn',
|
||||
suburban: 'S-Bahn',
|
||||
tram: 'Straßenbahn',
|
||||
bus: 'Bus',
|
||||
ferry: 'Fähre',
|
||||
regional: 'Regionalverkehr'
|
||||
};
|
||||
return names[type] || 'Bus';
|
||||
}
|
||||
|
||||
function createVehicleIcon(type, lineName) {
|
||||
const icon = getProductIcon(type);
|
||||
return L.divIcon({
|
||||
className: 'custom-vehicle-icon',
|
||||
html: `
|
||||
<div class="vehicle-marker-container">
|
||||
<div class="vehicle-label">${lineName}</div>
|
||||
<div class="vehicle-marker vehicle-${type}">${icon}</div>
|
||||
</div>
|
||||
`,
|
||||
iconSize: [32, 56],
|
||||
iconAnchor: [16, 28]
|
||||
});
|
||||
}
|
||||
|
||||
function createUserLocationIcon() {
|
||||
return L.divIcon({
|
||||
className: 'user-location-icon',
|
||||
html: `
|
||||
<div class="user-location-marker">
|
||||
<div class="user-location-pulse"></div>
|
||||
<div class="user-location-dot"></div>
|
||||
</div>
|
||||
`,
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20]
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRadarData() {
|
||||
if (isLoading) return;
|
||||
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const bounds = map.getBounds();
|
||||
const north = bounds.getNorth();
|
||||
const south = bounds.getSouth();
|
||||
const west = bounds.getWest();
|
||||
const east = bounds.getEast();
|
||||
|
||||
const radarUrl = `https://v6.vbb.transport.rest/radar?north=${north}&south=${south}&west=${west}&east=${east}&results=150`;
|
||||
|
||||
const response = await fetch(radarUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const currentVehicleIds = new Set();
|
||||
|
||||
if (data.movements) {
|
||||
data.movements.forEach(movement => {
|
||||
if (movement.location && movement.line) {
|
||||
const type = getProductType(movement.line.product);
|
||||
const lineName = movement.line.name || '?';
|
||||
const icon = getProductIcon(type);
|
||||
|
||||
const vehicleId = movement.tripId || `${lineName}-${movement.direction || 'unknown'}`;
|
||||
currentVehicleIds.add(vehicleId);
|
||||
|
||||
const newLatLng = L.latLng(movement.location.latitude, movement.location.longitude);
|
||||
|
||||
if (vehicleMarkers[vehicleId]) {
|
||||
const marker = vehicleMarkers[vehicleId].marker;
|
||||
const oldLatLng = marker.getLatLng();
|
||||
|
||||
if (oldLatLng.lat !== newLatLng.lat || oldLatLng.lng !== newLatLng.lng) {
|
||||
animateMarker(marker, oldLatLng, newLatLng, 3000);
|
||||
}
|
||||
} else {
|
||||
const marker = L.marker(newLatLng, {
|
||||
icon: createVehicleIcon(type, lineName),
|
||||
riseOnHover: true
|
||||
}).addTo(map);
|
||||
|
||||
const direction = movement.direction || 'Unbekannt';
|
||||
const productName = getProductName(type);
|
||||
|
||||
let nextStopInfo = '';
|
||||
if (movement.nextStopovers && movement.nextStopovers.length > 0) {
|
||||
const nextStop = movement.nextStopovers[0];
|
||||
if (nextStop.stop) {
|
||||
const arrivalTime = nextStop.arrival ?
|
||||
new Date(nextStop.arrival).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
seconds: '2-digit'
|
||||
}) : '—';
|
||||
nextStopInfo = `
|
||||
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.2);">
|
||||
<div style="font-size: 11px; color: rgba(255,255,255,0.6); margin-bottom: 4px;">
|
||||
NÄCHSTER HALT
|
||||
</div>
|
||||
<div style="font-size: 13px; font-weight: 500;">
|
||||
${nextStop.stop.name}
|
||||
</div>
|
||||
<div style="font-size: 12px; color: rgba(255,255,255,0.7); margin-top: 2px;">
|
||||
Ankunft: ${arrivalTime}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const popupContent = `
|
||||
<div style="text-align: center; min-width: 200px;">
|
||||
<div style="
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 15px;
|
||||
background: ${getProductColor(type)};
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.4);
|
||||
">${icon}</div>
|
||||
<div style="font-size: 11px; color: rgba(255,255,255,0.6); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px;">
|
||||
${productName}
|
||||
</div>
|
||||
<div style="font-weight: 700; font-size: 22px; margin-bottom: 8px; color: ${getProductColor(type)};">
|
||||
${lineName}
|
||||
</div>
|
||||
<div style="font-size: 14px; color: rgba(255,255,255,0.85); line-height: 1.4;">
|
||||
→ ${direction}
|
||||
</div>
|
||||
${nextStopInfo}
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent, {
|
||||
maxWidth: 300,
|
||||
className: 'custom-popup'
|
||||
});
|
||||
|
||||
vehicleMarkers[vehicleId] = {
|
||||
marker: marker,
|
||||
type: type
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(vehicleMarkers).forEach(vehicleId => {
|
||||
if (!currentVehicleIds.has(vehicleId)) {
|
||||
map.removeLayer(vehicleMarkers[vehicleId].marker);
|
||||
delete vehicleMarkers[vehicleId];
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`${Object.keys(vehicleMarkers).length} Fahrzeuge aktiv`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden:', error);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function animateMarker(marker, startLatLng, endLatLng, duration) {
|
||||
const startTime = Date.now();
|
||||
const latDiff = endLatLng.lat - startLatLng.lat;
|
||||
const lngDiff = endLatLng.lng - startLatLng.lng;
|
||||
|
||||
function updatePosition() {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
const easeProgress = progress < 0.5
|
||||
? 2 * progress * progress
|
||||
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
||||
|
||||
const currentLat = startLatLng.lat + (latDiff * easeProgress);
|
||||
const currentLng = startLatLng.lng + (lngDiff * easeProgress);
|
||||
|
||||
marker.setLatLng([currentLat, currentLng]);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updatePosition);
|
||||
}
|
||||
}
|
||||
|
||||
updatePosition();
|
||||
}
|
||||
|
||||
function getUserLocation() {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
userPosition = [lat, lon];
|
||||
|
||||
map.setView([lat, lon], 16, {
|
||||
animate: true,
|
||||
duration: 1
|
||||
});
|
||||
|
||||
if (userLocationMarker) {
|
||||
map.removeLayer(userLocationMarker);
|
||||
}
|
||||
|
||||
userLocationMarker = L.marker([lat, lon], {
|
||||
icon: createUserLocationIcon(),
|
||||
zIndexOffset: 1000
|
||||
}).addTo(map);
|
||||
|
||||
userLocationMarker.bindPopup(`
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 32px; margin-bottom: 10px;">📍</div>
|
||||
<div style="font-weight: 700; font-size: 16px; margin-bottom: 4px;">
|
||||
Dein Standort
|
||||
</div>
|
||||
<div style="font-size: 12px; color: rgba(255,255,255,0.7);">
|
||||
${lat.toFixed(4)}°, ${lon.toFixed(4)}°
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
console.log('Standort gefunden:', lat, lon);
|
||||
loadRadarData();
|
||||
},
|
||||
(error) => {
|
||||
console.log('Standort nicht verfügbar:', error);
|
||||
loadRadarData();
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 5000,
|
||||
maximumAge: 0
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Geolocation nicht unterstützt');
|
||||
loadRadarData();
|
||||
}
|
||||
}
|
||||
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
document.getElementById('time').textContent = `${hours}:${minutes}:${seconds}`;
|
||||
|
||||
const options = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
};
|
||||
const dateStr = now.toLocaleDateString('de-DE', options);
|
||||
document.getElementById('date').textContent = dateStr;
|
||||
}
|
||||
|
||||
let moveEndTimeout;
|
||||
map.on('moveend', () => {
|
||||
clearTimeout(moveEndTimeout);
|
||||
moveEndTimeout = setTimeout(() => {
|
||||
loadRadarData();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
updateClock();
|
||||
getUserLocation();
|
||||
|
||||
setInterval(updateClock, 1000);
|
||||
setInterval(() => {
|
||||
if (!isLoading) {
|
||||
loadRadarData();
|
||||
}
|
||||
}, 15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user