// ===== Map setup ===== const map = L.map("map").setView([55.6761, 12.5683], 13); L.tileLayer( "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap contributors", maxZoom: 19 } ).addTo(map); // ===== UI ===== const statusEl = document.getElementById("status"); const alertsEl = document.getElementById("presenceAlerts"); // ===== State ===== let simTime = 0; let running = false; let timer = null; // Group events per vehicle const vehicles = {}; for (const ev of VEHICLE_EVENTS) { if (!vehicles[ev.vehicle]) vehicles[ev.vehicle] = []; vehicles[ev.vehicle].push(ev); } // Sort each vehicle timeline for (const v in vehicles) { vehicles[v].sort((a, b) => a.ts - b.ts); } // Runtime objects const runtime = {}; function metersBetween(lat1, lon1, lat2, lon2) { const R = 6371000; const toRad = x => x * Math.PI / 180; const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function distanceToBoundsMeters(lat, lon, bounds) { const clampedLat = Math.max(bounds.getSouth(), Math.min(bounds.getNorth(), lat)); const clampedLon = Math.max(bounds.getWest(), Math.min(bounds.getEast(), lon)); return metersBetween(lat, lon, clampedLat, clampedLon); } function farthestCornerDistanceMeters(lat, lon, bounds) { const corners = [ bounds.getSouthWest(), bounds.getSouthEast(), bounds.getNorthWest(), bounds.getNorthEast() ]; let max = 0; for (const c of corners) { const d = metersBetween(lat, lon, c.lat, c.lng); if (d > max) max = d; } return max; } // ===== Core classification ===== function getPresenceState(lat, lon, uncertaintyM) { const bounds = map.getBounds(); const inside = bounds.contains([lat, lon]); if (inside) return "inside"; if (uncertaintyM > 0) { const dist = distanceToBoundsMeters(lat, lon, bounds); if (dist <= uncertaintyM) { const far = farthestCornerDistanceMeters(lat, lon, bounds); if (far <= uncertaintyM) return "cover_all"; return "overlap"; } } return "outside"; } // ===== Render ===== function updateVehicles() { alertsEl.innerHTML = ""; for (const name in vehicles) { const events = vehicles[name]; // Find latest event <= simTime let ev = null; for (const e of events) { if (e.ts <= simTime) ev = e; } if (!ev) continue; // not yet appeared const state = getPresenceState(ev.lat, ev.lon, ev.uncertaintyM); // Ensure runtime object exists if (!runtime[name]) { runtime[name] = { marker: L.marker([ev.lat, ev.lon]) }; } const r = runtime[name]; if (state === "inside") { r.marker.setLatLng([ev.lat, ev.lon]); if (!map.hasLayer(r.marker)) { r.marker.addTo(map); } } else { if (map.hasLayer(r.marker)) { map.removeLayer(r.marker); } } // ===== Corner panel ===== if (state === "overlap" || state === "cover_all") { const div = document.createElement("div"); div.className = "alert-item"; if (state === "cover_all") { div.innerHTML = `${name} Usikkerhed dækker hele kortet`; } else { div.innerHTML = `${name} Kan være i kortudsnittet`; } alertsEl.appendChild(div); } } statusEl.textContent = `Tid: ${simTime}s`; } // ===== Simulation ===== function tick() { simTime += 1; updateVehicles(); } document.getElementById("playBtn").onclick = () => { if (!running) { running = true; timer = setInterval(tick, 1000); } }; document.getElementById("pauseBtn").onclick = () => { running = false; clearInterval(timer); }; document.getElementById("resetBtn").onclick = () => { simTime = 0; running = false; clearInterval(timer); // remove markers for (const name in runtime) { if (map.hasLayer(runtime[name].marker)) { map.removeLayer(runtime[name].marker); } } updateVehicles(); }; // Initial draw updateVehicles();