kort/app.js

290 lines
7.2 KiB
JavaScript

const COLORS = {
"Bil 1": "#ff0000",
"Bil 2": "#00c853",
"Bil 3": "#2979ff",
"Bil 4": "#2979ff",
"Bil 5": "#2979ff"
};
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);
const statusEl = document.getElementById("status");
const playBtn = document.getElementById("playBtn");
const pauseBtn = document.getElementById("pauseBtn");
const resetBtn = document.getElementById("resetBtn");
function carIcon(color) {
return L.divIcon({
className: "",
iconSize: [24, 24],
iconAnchor: [12, 12],
html: `
<div class="car" data-role="car">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 2 L20 22 L12 18 L4 22 Z"
fill="${color}"
stroke="#ffffff"
stroke-width="1.5"
/>
</svg>
</div>
`
});
}
function setHeading(marker, deg) {
const el = marker.getElement();
if (!el) return;
const car = el.querySelector("[data-role=car]");
if (!car) return;
car.style.transform = `rotate(${deg}deg)`;
}
function fmtTs(ts) {
return `${ts}s`;
}
function popupHtml(name, state) {
return `
<div style="font:13px/1.4 system-ui,sans-serif;min-width:170px;">
<div style="font-weight:600;margin-bottom:6px;">${name}</div>
<div><strong>Tid:</strong> ${fmtTs(state.current.ts)}</div>
<div><strong>Lat:</strong> ${state.displayLat.toFixed(5)}</div>
<div><strong>Lon:</strong> ${state.displayLon.toFixed(5)}</div>
<div><strong>Hastighed:</strong> ${state.current.speedMps} m/s</div>
<div><strong>Retning:</strong> ${state.displayHeading.toFixed(0)}°</div>
<div><strong>Usikkerhed:</strong> ${state.current.uncertaintyM} m</div>
</div>
`;
}
function smoothstep(t) {
t = Math.max(0, Math.min(1, t));
return t * t * (3 - 2 * t);
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function lerpAngleDeg(a, b, t) {
let d = ((b - a + 540) % 360) - 180;
return (a + d * t + 360) % 360;
}
function buildVehicleBuckets(events) {
const out = {};
for (const e of events) {
if (!out[e.vehicle]) out[e.vehicle] = [];
out[e.vehicle].push({ ...e });
}
for (const k of Object.keys(out)) {
out[k].sort((a, b) => a.ts - b.ts);
}
return out;
}
const vehicleEvents = buildVehicleBuckets(VEHICLE_EVENTS);
const vehicleNames = Object.keys(vehicleEvents);
const vehicles = {};
for (const name of vehicleNames) {
const first = vehicleEvents[name][0];
const color = COLORS[name] || "#2979ff";
const marker = L.marker([first.lat, first.lon], {
icon: carIcon(color)
}).addTo(map);
const trail = L.polyline([[first.lat, first.lon]], {
color,
weight: 3,
opacity: 0.85
}).addTo(map);
let uncertaintyCircle = null;
if (name === "Bil 2") {
uncertaintyCircle = L.circle([first.lat, first.lon], {
radius: first.uncertaintyM,
color: color,
weight: 2,
dashArray: "6,6",
opacity: 0.7,
fillColor: color,
fillOpacity: 0.08
}).addTo(map);
}
vehicles[name] = {
name,
color,
events: vehicleEvents[name],
marker,
trail,
uncertaintyCircle,
currentIndex: 0,
current: first,
next: vehicleEvents[name][1] || null,
displayLat: first.lat,
displayLon: first.lon,
displayHeading: first.headingDeg,
lastTrailLat: first.lat,
lastTrailLon: first.lon
};
marker.bindPopup(popupHtml(name, vehicles[name]));
setHeading(marker, first.headingDeg);
}
let simTime = 0;
let lastFrameTime = null;
let running = true;
const TIME_SCALE = 0.175; // 2x hurtigere end før (tidligere 0.35)
function resetDemo() {
simTime = 0;
lastFrameTime = null;
for (const name of vehicleNames) {
const v = vehicles[name];
const first = v.events[0];
v.currentIndex = 0;
v.current = first;
v.next = v.events[1] || null;
v.displayLat = first.lat;
v.displayLon = first.lon;
v.displayHeading = first.headingDeg;
v.lastTrailLat = first.lat;
v.lastTrailLon = first.lon;
v.marker.setLatLng([first.lat, first.lon]);
setHeading(v.marker, first.headingDeg);
v.marker.setPopupContent(popupHtml(name, v));
v.trail.setLatLngs([[first.lat, first.lon]]);
if (v.uncertaintyCircle) {
v.uncertaintyCircle.setLatLng([first.lat, first.lon]);
v.uncertaintyCircle.setRadius(first.uncertaintyM);
}
}
updateStatus();
}
function advanceVehiclePointers(v) {
while (v.next && simTime >= v.next.ts) {
v.currentIndex += 1;
v.current = v.events[v.currentIndex];
v.next = v.events[v.currentIndex + 1] || null;
v.trail.addLatLng([v.current.lat, v.current.lon]);
v.lastTrailLat = v.current.lat;
v.lastTrailLon = v.current.lon;
}
}
function updateVehicleDisplay(v) {
advanceVehiclePointers(v);
let lat = v.current.lat;
let lon = v.current.lon;
let heading = v.current.headingDeg;
if (v.next) {
const dt = v.next.ts - v.current.ts;
const raw = dt > 0 ? (simTime - v.current.ts) / dt : 1;
const t = smoothstep(raw);
lat = lerp(v.current.lat, v.next.lat, t);
lon = lerp(v.current.lon, v.next.lon, t);
heading = lerpAngleDeg(v.current.headingDeg, v.next.headingDeg, t);
} else {
// ingen nyere observation: fortsæt kort frem med speed + heading
const age = simTime - v.current.ts;
const predictFor = Math.min(age, 8); // max 8 sekunders prediction
const meters = v.current.speedMps * predictFor;
// grov lokal projektion til demo-formål
const rad = v.current.headingDeg * Math.PI / 180;
const northM = Math.cos(rad) * meters;
const eastM = Math.sin(rad) * meters;
const metersPerDegLat = 111320;
const metersPerDegLon = 111320 * Math.cos(v.current.lat * Math.PI / 180);
lat = v.current.lat + northM / metersPerDegLat;
lon = v.current.lon + eastM / metersPerDegLon;
heading = v.current.headingDeg;
}
v.displayLat = lat;
v.displayLon = lon;
v.displayHeading = heading;
v.marker.setLatLng([lat, lon]);
setHeading(v.marker, heading);
v.marker.setPopupContent(popupHtml(v.name, v));
if (v.uncertaintyCircle) {
v.uncertaintyCircle.setLatLng([lat, lon]);
v.uncertaintyCircle.setRadius(v.current.uncertaintyM); // vis modelens værdi uændret
}
}
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\n` +
`Events passeret: ${passed} / ${total}`;
}
function tick(now) {
if (!running) return;
if (lastFrameTime === null) {
lastFrameTime = now;
}
const dtReal = (now - lastFrameTime) / 1000;
lastFrameTime = now;
simTime += dtReal / TIME_SCALE;
for (const name of vehicleNames) {
updateVehicleDisplay(vehicles[name]);
}
updateStatus();
requestAnimationFrame(tick);
}
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(tick);