// ===== 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();