181 lines
4.1 KiB
JavaScript
181 lines
4.1 KiB
JavaScript
// ===== 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 = `<strong>${name}</strong> Usikkerhed dækker hele kortet`;
|
|
} else {
|
|
div.innerHTML = `<strong>${name}</strong> 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();
|