Files
Live_Navi/js/livemap.js
T
2026-08-27 15:21:56 +02:00

321 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Live-Karte mit mehreren Modi:
// - "radar": zeigt alle Fahrzeuge im aktuellen Kartenausschnitt (Standard),
// gefiltert nach den gewählten Verkehrsmitteln
// - "isolated": ein Fahrzeug wurde angeklickt -> nur dieses wird live weiter-
// verfolgt (serverseitig via tripId gefiltert), sein Streckenverlauf
// wird eingezeichnet, alle anderen Fahrzeuge sind ausgeblendet
// - "trip": verfolgt ein bekanntes Fahrzeug (z.B. aus einer Verbindung heraus)
// Nutzt bewusst reines OpenStreetMap ohne zusätzlichen Kartenstil.
// Zusätzlich: createMiniMap() für kleine, unabhängige Vorschau-Karten in den
// Verbindungsergebnissen (Routenplaner).
const LiveMap = (() => {
let map = null;
let radarLayer = null;
let routeLayer = null;
let userLayer = null;
let tripMarker = null;
let mode = 'radar';
let radarPollHandle = null;
let tripPollHandle = null;
let currentTripId = null;
let isolatedVehicleMeta = null;
let getModesParam = () => null;
function init() {
if (map) return;
map = L.map('live-map', { zoomControl: true }).setView([52.5200, 13.4050], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>-Mitwirkende',
maxZoom: 19,
}).addTo(map);
radarLayer = L.layerGroup().addTo(map);
routeLayer = L.layerGroup().addTo(map);
userLayer = L.layerGroup().addTo(map);
map.on('moveend', () => {
if (mode === 'radar') refreshRadar();
});
}
function setModeFilter(fn) {
getModesParam = fn;
}
function vehicleIcon(label, bg, fg) {
return L.divIcon({
className: 'vehicle-marker',
html: `<span style="background:${bg || '#F5A623'};color:${fg || '#fff'};">${label || '?'}</span>`,
iconSize: [null, null],
});
}
function polylineToLatLngs(geojson) {
if (!geojson || !geojson.features) return [];
return geojson.features.map((f) => [f.geometry.coordinates[1], f.geometry.coordinates[0]]);
}
async function refreshRadar() {
if (!map || mode !== 'radar') return;
const bounds = map.getBounds();
let data;
try {
data = await API.getRadar({
north: bounds.getNorth().toFixed(5),
west: bounds.getWest().toFixed(5),
south: bounds.getSouth().toFixed(5),
east: bounds.getEast().toFixed(5),
modes: getModesParam(),
});
} catch (err) {
console.error('Radar-Fehler:', err.message);
return;
}
radarLayer.clearLayers();
data.vehicles.forEach((v) => {
if (v.latitude == null || v.longitude == null) return;
const marker = L.marker([v.latitude, v.longitude], { icon: vehicleIcon(v.line, v.color, v.textColor) });
marker.bindPopup(
`<strong>${v.line || 'Linie unbekannt'}</strong><br>${v.direction || ''}<br>` +
`<small>Nächster Halt: ${v.nextStopover || '–'}</small>`,
);
marker.on('click', () => isolateVehicle(v));
marker.addTo(radarLayer);
});
}
function startRadar() {
init();
setMode('radar');
refreshRadar();
if (radarPollHandle) clearInterval(radarPollHandle);
radarPollHandle = setInterval(refreshRadar, 15000);
setTimeout(() => map && map.invalidateSize(), 150);
}
function stopRadar() {
if (radarPollHandle) {
clearInterval(radarPollHandle);
radarPollHandle = null;
}
}
async function refreshIsolated() {
if (!map || mode !== 'isolated' || !currentTripId) return;
const bounds = map.getBounds();
let data;
try {
data = await API.getRadar({
north: bounds.getNorth().toFixed(5),
west: bounds.getWest().toFixed(5),
south: bounds.getSouth().toFixed(5),
east: bounds.getEast().toFixed(5),
tripId: currentTripId,
});
} catch (err) {
console.error('Radar-Fehler (isoliert):', err.message);
return;
}
radarLayer.clearLayers();
const v = data.vehicles[0];
if (v && v.latitude != null && v.longitude != null) {
const marker = L.marker([v.latitude, v.longitude], { icon: vehicleIcon(v.line, v.color, v.textColor) });
marker.bindPopup(`<strong>${v.line || ''}</strong><br>${v.direction || ''}<br><small>Nächster Halt: ${v.nextStopover || '–'}</small>`);
marker.addTo(radarLayer);
}
}
async function isolateVehicle(vehicle) {
if (!vehicle || !vehicle.tripId) return;
setMode('isolated');
currentTripId = vehicle.tripId;
isolatedVehicleMeta = { line: vehicle.line, color: vehicle.color, textColor: vehicle.textColor };
updateIsolationBanner();
routeLayer.clearLayers();
try {
const trip = await API.getTrip(vehicle.tripId, { polyline: true });
if (trip.polyline) {
const latlngs = polylineToLatLngs(trip.polyline);
if (latlngs.length > 0) {
L.polyline(latlngs, { color: vehicle.color || '#F5A623', weight: 5, opacity: 0.85 }).addTo(routeLayer);
}
}
} catch (err) {
console.error('Trip-Polyline-Fehler:', err.message);
}
refreshIsolated();
if (radarPollHandle) clearInterval(radarPollHandle);
radarPollHandle = setInterval(refreshIsolated, 10000);
}
function clearIsolation() {
currentTripId = null;
isolatedVehicleMeta = null;
updateIsolationBanner();
startRadar();
}
function updateIsolationBanner() {
const banner = document.getElementById('map-isolation-banner');
if (!banner) return;
if (!isolatedVehicleMeta) {
banner.classList.remove('is-visible');
return;
}
const lineEl = document.getElementById('map-isolation-line');
if (lineEl) {
lineEl.textContent = isolatedVehicleMeta.line || '?';
lineEl.style.background = isolatedVehicleMeta.color || '#F5A623';
lineEl.style.color = isolatedVehicleMeta.textColor || '#fff';
}
banner.classList.add('is-visible');
}
async function refreshTrip() {
if (!currentTripId) return;
let data;
try {
data = await API.getTrip(currentTripId, { polyline: !tripMarker });
} catch (err) {
console.error('Trip-Fehler:', err.message);
return;
}
if (data.polyline && routeLayer) {
routeLayer.clearLayers();
const latlngs = polylineToLatLngs(data.polyline);
if (latlngs.length > 0) {
L.polyline(latlngs, { color: data.color || '#F5A623', weight: 4, opacity: 0.7 }).addTo(routeLayer);
map.fitBounds(L.latLngBounds(latlngs), { padding: [30, 30] });
}
}
if (data.currentLocation) {
const pos = [data.currentLocation.latitude, data.currentLocation.longitude];
if (tripMarker) {
tripMarker.setLatLng(pos);
} else {
tripMarker = L.marker(pos, { icon: vehicleIcon(data.line, data.color, data.textColor) }).addTo(routeLayer);
}
tripMarker.bindPopup(`<strong>${data.line || ''}</strong><br>→ ${data.direction || ''}`);
}
}
function trackTrip(tripId) {
init();
setMode('trip');
currentTripId = tripId;
tripMarker = null;
routeLayer.clearLayers();
refreshTrip();
if (tripPollHandle) clearInterval(tripPollHandle);
tripPollHandle = setInterval(refreshTrip, 10000);
setTimeout(() => map && map.invalidateSize(), 150);
}
function stopTracking() {
if (tripPollHandle) {
clearInterval(tripPollHandle);
tripPollHandle = null;
}
currentTripId = null;
tripMarker = null;
}
function locateUser() {
init();
if (!navigator.geolocation) {
alert('Geolocation wird von diesem Browser nicht unterstützt.');
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
const { latitude, longitude } = pos.coords;
userLayer.clearLayers();
L.marker([latitude, longitude], {
icon: L.divIcon({ className: 'user-location-marker', iconSize: [16, 16] }),
}).addTo(userLayer);
map.setView([latitude, longitude], 16);
},
(err) => alert('Standort konnte nicht ermittelt werden: ' + err.message),
{ enableHighAccuracy: true, timeout: 10000 },
);
}
function setMode(newMode) {
if (mode === newMode) return;
if (mode === 'radar') stopRadar();
if (mode === 'isolated') stopRadar();
if (mode === 'trip') stopTracking();
if (newMode === 'radar') routeLayer.clearLayers();
mode = newMode;
}
function startPolling() {
if (mode === 'radar' || mode === 'idle') startRadar();
else if (map) setTimeout(() => map.invalidateSize(), 150);
}
function stopPolling() {
stopRadar();
stopTracking();
}
function createMiniMap(containerId, journey) {
const miniMap = L.map(containerId, {
zoomControl: false,
attributionControl: false,
dragging: true,
scrollWheelZoom: false,
tap: true,
}).setView([52.5200, 13.4050], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap-Mitwirkende',
maxZoom: 19,
}).addTo(miniMap);
const allPoints = [];
(journey.legs || []).forEach((leg) => {
if (!leg.polyline) return;
const latlngs = polylineToLatLngs(leg.polyline);
if (latlngs.length === 0) return;
L.polyline(latlngs, {
color: leg.walking ? '#8E99A6' : leg.color || '#F5A623',
weight: leg.walking ? 3 : 4,
dashArray: leg.walking ? '5, 7' : null,
opacity: 0.9,
}).addTo(miniMap);
allPoints.push(...latlngs);
});
if (allPoints.length > 0) {
miniMap.fitBounds(L.latLngBounds(allPoints), { padding: [16, 16] });
}
setTimeout(() => miniMap.invalidateSize(), 50);
return miniMap;
}
return {
startPolling,
stopPolling,
trackTrip,
setModeFilter,
clearIsolation,
locateUser,
createMiniMap,
};
})();