${v.name}: ${v.presenceState === "covers" ? "usikkerhed dækker hele kortet" : "mulig tilstedeværelse"}
${v.presenceState === "covers" ? "Estimatet er udenfor kortet, men usikkerheden spænder over hele viewporten." : "Estimatet er udenfor kortet, men usikkerheden overlapper det viste område delvist."}
`).join("");
const bounds = view.map.getBounds();
view.indicatorsEl.innerHTML = alertVehicles.map(v => {
const direction = getDirectionInfo(v.displayLat, v.displayLon, bounds);
const subtitle = v.presenceState === "covers" ? "Usikkerheden dækker hele viewporten" : `Mulig tilstedeværelse fra ${direction.label}`;
return `
`;
}).join("");
}
function resetDemo() {
simTime = 0;
lastFrameTime = null;
renderSelectedVehicleInfo(null);
for (const view of views) {
for (const name of vehicleNames) {
const v = view.vehicles[name];
const first = v.events[0];
const firstPos = getEventLatLon(first, view.positionKey);
v.currentIndex = 0;
v.current = first;
v.next = v.events[1] || null;
v.displayLat = firstPos.lat;
v.displayLon = firstPos.lon;
v.displayHeading = first.headingDeg;
v.presenceState = "inside";
v.presenceStateLabel = "ja";
v.marker.setLatLng([firstPos.lat, firstPos.lon]);
setHeading(v.marker, first.headingDeg);
v.trail.setLatLngs([[firstPos.lat, firstPos.lon]]);
if (!view.map.hasLayer(v.marker)) v.marker.addTo(view.map);
syncUncertaintyCircle(view, v);
}
updateViewAlerts(view);
}
updateStatus();
}
function updateDebugPanel() {
const lines = views.map((view) => {
const el = document.getElementById(view.id);
const rect = el.getBoundingClientRect();
const center = view.map.getCenter();
const zoom = view.map.getZoom();
const stats = view.getTileStats();
return [
`${view.label}`,
` size: ${Math.round(rect.width)}x${Math.round(rect.height)}`,
` center: ${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}`,
` zoom: ${zoom}`,
` tiles loaded/errors: ${stats.tileLoads}/${stats.tileErrors}`
].join("\n");
});
debugPanelEl.textContent = lines.join("\n\n");
}
function updateStatus() {
const total = VEHICLE_EVENTS.length;
const passed = VEHICLE_EVENTS.filter(e => e.ts <= simTime).length;
statusEl.textContent = `Simuleret tid: ${simTime.toFixed(1)} s\nEvents passeret: ${passed} / ${total}\nAfspilning: 2x`;
updateDebugPanel();
}
function tick(now) {
if (!running) return;
if (lastFrameTime === null) lastFrameTime = now;
const dtReal = (now - lastFrameTime) / 1000;
lastFrameTime = now;
simTime += dtReal / TIME_SCALE;
for (const view of views) {
for (const name of vehicleNames) {
updateVehicleDisplay(view, view.vehicles[name]);
}
updateViewAlerts(view);
}
updateStatus();
requestAnimationFrame(tick);
}
function invalidateAllMaps() {
for (const view of views) {
view.map.invalidateSize();
}
}
for (const view of views) {
view.indicatorsEl.addEventListener("click", (event) => {
const button = event.target.closest(".offmap-indicator");
if (!button) return;
const v = view.vehicles[button.dataset.vehicle];
renderSelectedVehicleInfo({
name: v.name,
viewLabel: view.label,
source: "hjørneindikator",
state: v
});
});
view.map.on("moveend zoomend", () => {
for (const name of vehicleNames) {
updateVehicleDisplay(view, view.vehicles[name]);
}
updateViewAlerts(view);
});
}
playBtn.addEventListener("click", () => {
if (running) return;
running = true;
lastFrameTime = null;
requestAnimationFrame(tick);
});
pauseBtn.addEventListener("click", () => {
running = false;
});
resetBtn.addEventListener("click", () => {
running = false;
resetDemo();
running = true;
requestAnimationFrame(tick);
});
resetDemo();
requestAnimationFrame(() => {
invalidateAllMaps();
requestAnimationFrame(invalidateAllMaps);
});
window.addEventListener("resize", invalidateAllMaps);
requestAnimationFrame(tick);