From 7133b57e248ec34ed6e35270a8112ad72dee5cf5 Mon Sep 17 00:00:00 2001 From: "Morten V. Christiansen" Date: Sun, 5 Apr 2026 12:06:27 +0200 Subject: [PATCH] init version --- app.js | 289 ++++++++++++++++++++++++++ data.js | 61 ++++++ kort 8/app_fixed_v2.js | 180 ++++++++++++++++ kort 8/data.js | 19 ++ kort 8/kort7_fixed_v2.html | 138 +++++++++++++ kort1.html | 259 +++++++++++++++++++++++ kort2.html | 324 +++++++++++++++++++++++++++++ kort2.zip | Bin 0 -> 4546 bytes kort3.html | 262 ++++++++++++++++++++++++ kort4.html | 382 ++++++++++++++++++++++++++++++++++ kort5.html | 240 ++++++++++++++++++++++ kort6.html | 75 +++++++ kort7/ARBEJDSPLAN.md | 88 ++++++++ kort7/app.js | 406 +++++++++++++++++++++++++++++++++++++ kort7/app.js.orig | 406 +++++++++++++++++++++++++++++++++++++ kort7/data.js | 74 +++++++ kort7/data.js.orig | 61 ++++++ kort7/data.js.rej | 19 ++ kort7/kort6.html.orig | 75 +++++++ kort7/kort7.html | 94 +++++++++ kort7/patch | 37 ++++ kort7/patch2 | 20 ++ kort7/patch3 | 11 + 23 files changed, 3520 insertions(+) create mode 100644 app.js create mode 100644 data.js create mode 100644 kort 8/app_fixed_v2.js create mode 100644 kort 8/data.js create mode 100644 kort 8/kort7_fixed_v2.html create mode 100644 kort1.html create mode 100644 kort2.html create mode 100644 kort2.zip create mode 100644 kort3.html create mode 100644 kort4.html create mode 100644 kort5.html create mode 100644 kort6.html create mode 100644 kort7/ARBEJDSPLAN.md create mode 100644 kort7/app.js create mode 100644 kort7/app.js.orig create mode 100644 kort7/data.js create mode 100644 kort7/data.js.orig create mode 100644 kort7/data.js.rej create mode 100644 kort7/kort6.html.orig create mode 100644 kort7/kort7.html create mode 100644 kort7/patch create mode 100644 kort7/patch2 create mode 100644 kort7/patch3 diff --git a/app.js b/app.js new file mode 100644 index 0000000..1c4d4d3 --- /dev/null +++ b/app.js @@ -0,0 +1,289 @@ +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: ` +
+ +
+ ` + }); +} + +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 ` +
+
${name}
+
Tid: ${fmtTs(state.current.ts)}
+
Lat: ${state.displayLat.toFixed(5)}
+
Lon: ${state.displayLon.toFixed(5)}
+
Hastighed: ${state.current.speedMps} m/s
+
Retning: ${state.displayHeading.toFixed(0)}°
+
Usikkerhed: ${state.current.uncertaintyM} m
+
+ `; +} + +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); diff --git a/data.js b/data.js new file mode 100644 index 0000000..345d701 --- /dev/null +++ b/data.js @@ -0,0 +1,61 @@ +const VEHICLE_EVENTS = [ + { vehicle: "Bil 3", ts: 0, lat: 55.6780, lon: 12.5600, speedMps: 8, headingDeg: 190, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 2, lat: 55.6748, lon: 12.5650, speedMps: 9, headingDeg: 45, uncertaintyM: 1200 }, + { vehicle: "Bil 1", ts: 4, lat: 55.6761, lon: 12.5683, speedMps: 12, headingDeg: 90, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 5, lat: 55.6728, lon: 12.5710, speedMps: 7, headingDeg: 330, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 6, lat: 55.6795, lon: 12.5845, speedMps: 11, headingDeg: 260, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 12, lat: 55.6772, lon: 12.5603, speedMps: 8, headingDeg: 195, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 13, lat: 55.6760, lon: 12.5670, speedMps: 10, headingDeg: 50, uncertaintyM: 1000 }, + { vehicle: "Bil 1", ts: 15, lat: 55.6762, lon: 12.5720, speedMps: 13, headingDeg: 95, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 16, lat: 55.6736, lon: 12.5698, speedMps: 8, headingDeg: 340, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 18, lat: 55.6793, lon: 12.5815, speedMps: 12, headingDeg: 255, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 24, lat: 55.6765, lon: 12.5610, speedMps: 9, headingDeg: 200, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 26, lat: 55.6772, lon: 12.5692, speedMps: 11, headingDeg: 55, uncertaintyM: 900 }, + { vehicle: "Bil 1", ts: 27, lat: 55.6763, lon: 12.5758, speedMps: 14, headingDeg: 100, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 28, lat: 55.6746, lon: 12.5688, speedMps: 8, headingDeg: 350, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 29, lat: 55.6790, lon: 12.5784, speedMps: 12, headingDeg: 250, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 38, lat: 55.6758, lon: 12.5620, speedMps: 9, headingDeg: 210, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 40, lat: 55.6785, lon: 12.5716, speedMps: 12, headingDeg: 60, uncertaintyM: 850 }, + { vehicle: "Bil 1", ts: 41, lat: 55.6764, lon: 12.5798, speedMps: 14, headingDeg: 105, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 42, lat: 55.6757, lon: 12.5682, speedMps: 9, headingDeg: 0, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 43, lat: 55.6786, lon: 12.5753, speedMps: 11, headingDeg: 245, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 50, lat: 55.6752, lon: 12.5634, speedMps: 10, headingDeg: 220, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 54, lat: 55.6796, lon: 12.5744, speedMps: 12, headingDeg: 70, uncertaintyM: 800 }, + { vehicle: "Bil 1", ts: 55, lat: 55.6766, lon: 12.5837, speedMps: 15, headingDeg: 110, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 56, lat: 55.6769, lon: 12.5682, speedMps: 9, headingDeg: 10, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 57, lat: 55.6781, lon: 12.5722, speedMps: 10, headingDeg: 240, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 66, lat: 55.6747, lon: 12.5652, speedMps: 10, headingDeg: 230, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 68, lat: 55.6804, lon: 12.5776, speedMps: 11, headingDeg: 85, uncertaintyM: 950 }, + { vehicle: "Bil 1", ts: 69, lat: 55.6768, lon: 12.5875, speedMps: 14, headingDeg: 115, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 70, lat: 55.6781, lon: 12.5688, speedMps: 10, headingDeg: 20, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 72, lat: 55.6776, lon: 12.5692, speedMps: 10, headingDeg: 235, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 82, lat: 55.6744, lon: 12.5673, speedMps: 9, headingDeg: 240, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 84, lat: 55.6810, lon: 12.5808, speedMps: 10, headingDeg: 100, uncertaintyM: 1100 }, + { vehicle: "Bil 1", ts: 85, lat: 55.6770, lon: 12.5912, speedMps: 13, headingDeg: 120, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 87, lat: 55.6792, lon: 12.5700, speedMps: 10, headingDeg: 30, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 88, lat: 55.6771, lon: 12.5662, speedMps: 9, headingDeg: 230, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 96, lat: 55.6742, lon: 12.5696, speedMps: 8, headingDeg: 250, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 98, lat: 55.6812, lon: 12.5842, speedMps: 10, headingDeg: 115, uncertaintyM: 1200 }, + { vehicle: "Bil 1", ts: 99, lat: 55.6772, lon: 12.5948, speedMps: 12, headingDeg: 125, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 101,lat: 55.6800, lon: 12.5716, speedMps: 9, headingDeg: 40, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 102,lat: 55.6766, lon: 12.5634, speedMps: 9, headingDeg: 225, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 112,lat: 55.6742, lon: 12.5720, speedMps: 8, headingDeg: 260, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 114,lat: 55.6811, lon: 12.5877, speedMps: 9, headingDeg: 125, uncertaintyM: 1000 }, + { vehicle: "Bil 1", ts: 115,lat: 55.6775, lon: 12.5982, speedMps: 11, headingDeg: 130, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 117,lat: 55.6806, lon: 12.5736, speedMps: 8, headingDeg: 50, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 118,lat: 55.6761, lon: 12.5608, speedMps: 8, headingDeg: 220, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 }, + { vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 } +]; diff --git a/kort 8/app_fixed_v2.js b/kort 8/app_fixed_v2.js new file mode 100644 index 0000000..c2cbceb --- /dev/null +++ b/kort 8/app_fixed_v2.js @@ -0,0 +1,180 @@ +// ===== 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(); diff --git a/kort 8/data.js b/kort 8/data.js new file mode 100644 index 0000000..11c447b --- /dev/null +++ b/kort 8/data.js @@ -0,0 +1,19 @@ +const VEHICLE_EVENTS = [ + + { vehicle: "Bil 3", ts: 0, lat: 55.6780, lon: 12.5600, speedMps: 8, headingDeg: 190, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 2, lat: 55.6748, lon: 12.5650, speedMps: 9, headingDeg: 45, uncertaintyM: 1200 }, + { vehicle: "Bil 1", ts: 4, lat: 55.6761, lon: 12.5683, speedMps: 12, headingDeg: 90, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 5, lat: 55.6728, lon: 12.5710, speedMps: 7, headingDeg: 330, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 6, lat: 55.6795, lon: 12.5845, speedMps: 11, headingDeg: 260, uncertaintyM: 0 }, + + { vehicle: "Bil 6", ts: 8, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, + + { vehicle: "Bil 7", ts: 10, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, + + { vehicle: "Bil 3", ts: 12, lat: 55.6772, lon: 12.5603, speedMps: 8, headingDeg: 195, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 13, lat: 55.6760, lon: 12.5670, speedMps: 10, headingDeg: 50, uncertaintyM: 1000 }, + { vehicle: "Bil 1", ts: 15, lat: 55.6762, lon: 12.5720, speedMps: 13, headingDeg: 95, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 16, lat: 55.6736, lon: 12.5698, speedMps: 8, headingDeg: 340, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 18, lat: 55.6793, lon: 12.5815, speedMps: 12, headingDeg: 255, uncertaintyM: 0 } + +]; diff --git a/kort 8/kort7_fixed_v2.html b/kort 8/kort7_fixed_v2.html new file mode 100644 index 0000000..25fac64 --- /dev/null +++ b/kort 8/kort7_fixed_v2.html @@ -0,0 +1,138 @@ + + + + + + Vehicle stream demo + + + + + + + +
+ +
+
+ + + +
+
+
+
+ + + + + + + diff --git a/kort1.html b/kort1.html new file mode 100644 index 0000000..eb6f9ac --- /dev/null +++ b/kort1.html @@ -0,0 +1,259 @@ + + + + + + GPS demo (frames a→b→c) + + + + + + + +
+
+
+ + + +
+
Data: 10 frames (a→b→c…) med 5 biler pr. frame
+ Justér STEP_MS i koden. +
+ + + + diff --git a/kort2.html b/kort2.html new file mode 100644 index 0000000..accd5cc --- /dev/null +++ b/kort2.html @@ -0,0 +1,324 @@ + + + + + + GPS demo med 5 biler og 10 position-sæt + + + + + + + + +
+ +
+
+ + + +
+
+ 10 sæt positioner (a → b → c ...) med 5 biler. +
+
+ + + + diff --git a/kort2.zip b/kort2.zip new file mode 100644 index 0000000000000000000000000000000000000000..950bf0e0c1ffaab1b5fd0033ffceb4224161c1fa GIT binary patch literal 4546 zcmaKwWmFXEzQ$*O8NfkOrAtaSjesx?DWK#4Lk%&{7&Ho&^-V^jurp_184w7 zj*i^cE*k3i0BnivGAqTq{n`Tp0OD+70|5U#(sXvzW?ntI89N3K#H?VFuSExVJu5_g;_A&PzkHMr81?Dja=Gl!l}Tca z&pL)@!wpJ@h)djvo#$gHUZ;sU--yw*^2{d9^ZunR~Q-c9p#n7x>%*dVqet z`jm7B8omU&hH!Fc3K#)FD8 z$@#P&<{I%~@Q>M&Q0+rA$^dhOsam8qS4^Rgp(B#s@f{A3S{QDPbI##X0g? zf%_ZNl;m=qqCu?;O*oYHNgY=E__*sy4?VG3!aJpQ4`qT{g)>huhLJ5rtyXv>liyjJ zTDt9imIuFi=RWpJhg5ll#;snI&Z_FE*>P>u{*R|p4of)f?wjbxgke~Zb~mgd)_xa! z!j`X3$^3l$H157pAwC@f{(1($B=Bh8@PY@EQz05pL+dLkrgp6zV8ZNoX0;9 z&54k#6Mkgdz)?Ce9g-36^eb_NAvtwEI-e*jk;l@V#g5$k!YF0~rrSa`@QMP|8_VFy zB;|RHR?5hw?<9+y&9=yukeTIXQEG&DW6=BuxRF$*RyLMzom>-!1XF6Z+*A#C6##=X z32Grc4|hCvnr>#fe;UO10Cy-C7@Jt5A!~S#MM1{ z$Vm-b&4}6nvr|?PAAB_|fc1rVh~GYN`duM5Pt~_br}R2Wh^V77*(h-O#Xhzph|XeW z6{^6>=NrW)Ble&nGAq3Y|8dqzdJb{WnGq2vG`q}0 zzLlbhqm|Np^atdCMX(2sbSx_#*Mq79R|^Y+hWYplG$?^by1P6%qtA(^ell#G*)!zt z-52NB97QoMoLA(>sIc#0TU<56iJBxBk1agpP{fN{*>&YpY4DNMx>g4|s`O8QVIsZb z=1LU>bwVug5&^rel@ntxkoF_&_;h(??Q(N@BfN}Ud|vMZ7qa3A+%2L6;wl*H%t|ac z+Kx10;t`%zMC4P3^L4RDy=A#fbeWrT+mF~Dcuqao<*!Pbx!_kO3l-KJ>F)oO2ff*x z4>P$EHss2o8(A&Q&89g5_NMbl8&qOAxajI}LxWE^-+$nfEBBi5mhcRSX5YA{@oC84 zTd9(P*>>tB?d@v*p-MWdEQ>Ik$OV2RJWP1JMA^erm}c8ZCon(MVfEeeA?P!pH1pHL zuVrVw#-nNvT8kU4P-X^j9%*1&`kE&|2#P5&d09rC*fehA9Vy~K+&v+#ags(>p?~~v z0DJ)tR5dcq-zA}~qa#S(yJkI+!xC%`u^>6utTae4)v4u)!xQ&UAOCcet*3B6W&=B| z=1}P~;b1T7FTieDF|%_zNIa~pR!0oaATI9(Ypb;g_t@8}Dh(GYDTtkaLFZB}(7ht{ zSvQH&rT5(x?$jLNr5e|WQ+gMe9(#Fzsa95H?Gj6C>jmNP=$BqVV;niY!$5flt_`Zw zp1X|Ow^2--8s$iN(=r68n#qSCd&8KeqzMkK6}Y6P&}K0B`@F}cOh1kK60FR&f~zgf zw0`YrsE{YfXkaH`z#aBFEIy&ys#HI2TAxXNe$78_Bry?{GJ&wZX}s_Cw!l3XijvD~ zQ|zU5twN@-7rFQ6rbz`LJTnl#>}U_OOS{P{eVZD7KN6O?x#G<*r;>2X7)kp@o5SSh zl0JXQuXt#U`(zmiVzf)r{haM61eRTghJ@E?q2h;;EU65GJ}tqM{rjr!WQ{E9cH0eh zA*B!**7g!d$@9T|&g2}0_Y>VE)cI9O)eIA{%pc>o6t^tbdd~U1zH-z+>r)}i=7Q~- z$6MYuNOOxSJM+y*S+?_HRNiCH^m|N-pCBvxmYT&dkJSth3aAY234GQ1!6XfxwM$AP zVFOh;dZe}s{Ul9bSl@|c3NL9;4)*jR&sG3b@T4;~Mcih)n`1m7w@W4$;3C(p!$gB9 z_6E;^-JAkqS^Bepux1}$lFpw#gR~UaXY{q8W^AerkH~dlsb#TyNgA>D%W%l%Q7PA! zZ+Lo~q@~UzBYM_e6V>W&wv*8kmj-O+*m|hD5D(4uFjBdc>rf22o7Dzsyvy%IPs*ip zH*Zujyxlt?hmip+c%zTr)(iXq<|VajVMt{Vxb?;dsGdvoC7_$~w5F?ha{(P?Q8Q<2pwzNEyUzP})-tBqJ*26zN8Jd9SP=1Ws zBmXjB-OO_`Ob#t5$mnn;8074B5QuIGcLiI~lI3f5)rdQbK*ickfn+PpNf-|%L|+-V zZea$B*VCq2%1p=-PmugKHk1-(5odj%mQ+(Rx7lbv+oscv=nbh*xcVurgVgmm;Sth4H++hBMmkzQ zPc`Jnc5Vsh&v&4fVliCdRQoQt+O{vR{k7DG!Gt-rPVUzeCGrgS(YdT&YQ;j(>OHGsz69#8w=fj24ub5{vkwW_p zGxi!h>}^lAZ~M0zP6`O=a0hD=@U9{Gw{RSOhlsxM@3z4#%Q{E5SStSxktKH_vK%hp zu6dUaH68*0xOXAa)X3H7Un%m@e@>AJNeVX9lEgtjZyp8?wAv0R4!mIez#M?HtVimN z&xq$)0g@7mA;n|76=jM}E%iCsr!nK=em(GPY^=d^nm%&f{`978CaUOXQN!40o{T=5 zLF{f^RS3bHo$(IcjgI_GTAV@JeE^x{0(2?E^ z8fp5GLESZ=XH!izjbD7Cp^(D@m*k)FjPK-a8XeTR_v_oP$ty-zdFUaXpaol$xn6Cg!DJ3Tg^O1bMj*jLKPQsq&a4@aWWi6gP30UrmG?%%|Nrc9@BTDU-`Y z@Pmm6kw#e#^r4^gAV50(!Zm!@I@Jn@h={e9Z=b2z($HAZJTergcMUosuE_mOCfa=`JuvW3|C4h`@m>gj`CYnDkYW(z# zY1-28j z@^y-uJ$V8%6g=L{wZoLoyrt;5VLd^5{38jRM}gKFCeM&dOxZq$`UJ>ADh23@w`n<1 zEV8R;kw|ZIYfdQ8PvSwpm{nVZZHb@C3&>IgF$JI2>G+woSbjx#OvMyFj;NUI_OP37 ztg*fhz{@*=bGX}H*IVyE((pnxZRonye773(N6Wk3R*`}t(!TUm9&eR+Z;5i}Z9!`7 zna>wbdiExtr^%w?JFv2U?Y{_X@ecL)+`#kNws<}JD@uURe`)9E(l1;gO7YH^_80lV zd6j>>p0hSpt?-JCz>}Pn1zyIdY27#=bk*V!6 z%dcdIc%Ho3Ok>56+Q>6^#(a2mdG1`X$|8f7AeQd?HnmZ4)&8(88$4{tp%pjm7m|ZQ zv81x<1LDhAgW|auSc$~zbZmLe|-pYFTVU2=)ECnhz#~u78<0+x~kye6ZU#8K8B+9Wz_&XfL?h=bDlhv zwA}~KdG|NE7K&AMIQ22;h4m?yvm_TICt|bSX^IJcCIM2LNnFt@H^OdM3T9PGex4nF zE;XiV`Q_0wyi`2txy9Jt@a5wkxp6f_xV>E16OXi@0hGe&b8|NJ@e^-pD>D>4!9`|5 zvy09M&tKll#D@ZhRLFztGA=B#jj{a+cq!$Jot^r^Aq(GWif^3(r+J&M=|K1vdNkf; zXl#frc_D+gEMdt|?M2<@UMy_RIyJP?@$#PXK7~o3+e#v4i|&Lm!-5E69Q7zm$%~IE z(M_nzuzh`ZJ9_%9bCtUfGk1Ly8G_qBEbF0{-azXdR``@5&Br~5?5~yca0`v z6ey`C3f}YE73wMZc3qj4c2tt9#l!w+a6X}A3HA9{ zHaBL{A1RC+UWqNvHJPv3>~sL$nghr*4v@rKnxCV`S)BmivuJ+$a*W zoz51MV5Wk3wPtlQKe=&6+hBUyM%xXuzJcU?4-Pr`e4YuO6{XA+zuuyHBk$U2)hV#A zK)MUx7{mMiWdEpt`N38n$~7<(xd71P&+;oxTN=N`RZ#$9(P95zB?iS^xd8wG?{<#= zpRa#a8leBGH2zne@mJR0z3i_nnY)_g-_CXi`_tL}AK2d&_*dBMeZc?F;~no$J^qac Ty3;NIfPJ^N?(R0?KY#xKL6u=1 literal 0 HcmV?d00001 diff --git a/kort3.html b/kort3.html new file mode 100644 index 0000000..05ff152 --- /dev/null +++ b/kort3.html @@ -0,0 +1,262 @@ + + + + + + + +GPS demo + + + + + + + + + + + +
+ +
+ + + +
+ + + + + + diff --git a/kort4.html b/kort4.html new file mode 100644 index 0000000..6af3b0f --- /dev/null +++ b/kort4.html @@ -0,0 +1,382 @@ + + + + + + GPS demo med trails + + + + + + + + +
+ +
+ + + +
+ + + + + diff --git a/kort5.html b/kort5.html new file mode 100644 index 0000000..a0af4a6 --- /dev/null +++ b/kort5.html @@ -0,0 +1,240 @@ + + + + + + + +GPS demo + + + + + + + + + + + +
+ + + + + + diff --git a/kort6.html b/kort6.html new file mode 100644 index 0000000..1f70ddf --- /dev/null +++ b/kort6.html @@ -0,0 +1,75 @@ + + + + + + Vehicle stream demo + + + + + + + +
+ +
+
+ + + +
+
+
+ + + + + diff --git a/kort7/ARBEJDSPLAN.md b/kort7/ARBEJDSPLAN.md new file mode 100644 index 0000000..c78b249 --- /dev/null +++ b/kort7/ARBEJDSPLAN.md @@ -0,0 +1,88 @@ +# Arbejdsplan for kort7 + +## Mål +Bygge en løsning med to samtidige kortvisninger over samme positionsdata, hvor hvert kort repræsenterer sit eget perspektiv på usikkerhed. + +## Principper +- Samme køretøjspositioner og tidslinje i begge kort +- Hvert kort har sit eget usikkerhedssæt +- Zoom og pan skal kunne ske uafhængigt i hvert kort +- Visning skal være viewport-drevet, ikke bundet til et bestemt geografisk udsnit +- UI'et skal beskytte læsbarheden, også ved meget store usikkerheder + +## Funktionel plan + +### 1. Datamodel +- Udvide `data.js`, så hver observation har: + - fælles position, retning, hastighed, timestamp + - `uncertaintyA` + - `uncertaintyB` +- Sikre at begge usikkerheder hører til samme observation i streamen + +### 2. Layout +- Opdatere `kort7.html` til to separate kortcontainere +- Give hvert kort sin egen HUD/indikatorzone for køretøjer uden for viewport +- Beholde fælles styring til play/pause/reset + +### 3. Rendering-arkitektur +- Omskrive `app.js`, så rendering sker per map-instans i stedet for globalt +- Lade begge kort abonnere på samme simulerede tid og samme positionsstream +- Lade hvert kort bruge sit eget usikkerhedsfelt + +### 4. Viewport-logik pr. kort +For hvert køretøj og for hvert kort skal systemet afgøre: +- **inside**: køretøjet er inden for viewporten +- **possible**: køretøjet er uden for viewporten, men usikkerheden overlapper viewporten +- **outside**: køretøjet og dets usikkerhed er irrelevante for viewporten + +Denne vurdering skal genberegnes: +- ved hver dataopdatering +- ved zoom +- ved pan + +### 5. Visuel adfærd +- **Inside:** + - vis køretøjet på kortet + - vis trail + - vis transparent usikkerhedscirkel omkring køretøjet +- **Possible:** + - skjul køretøjets normale markør på kortet + - vis en hjørneindikator i det relevante kort + - indikatoren skal vise hvilket køretøj der muligvis kan være i området + - gerne med farve, navn og retning/side i forhold til viewporten + - usikkerhed på kortet kan evt. stadig vises diskret, hvis det giver mening +- **Outside:** + - vis hverken markør eller hjørneindikator + - trail kan tones ned eller skjules + +### 6. Håndtering af store usikkerheder +- Undgå at enorme usikkerheder ødelægger læsbarheden +- Klip visning naturligt til viewporten +- Brug lav opacity for store usikkerhedsflader +- Lad hjørneindikatoren være den primære forklaring, når centrum ligger uden for kortet + +### 7. Sammenligning mellem perspektiver +- Et køretøj må gerne være: + - synligt på kort A + - kun en mulig tilstedeværelse på kort B + - eller omvendt +- Dette er en ønsket effekt, fordi forskellen mellem perspektiverne netop skal være tydelig + +## Implementeringsrækkefølge +1. Gemme arbejdsplanen +2. Opdatere datamodellen i `data.js` +3. Splitte `kort7.html` til to kortviews +4. Refaktorere `app.js` til to renderingskontekster +5. Implementere usikkerhedscirkler per kort +6. Implementere hjørneindikatorer per kort +7. Teste zoom/pan og dynamiske overgange mellem inside/possible/outside +8. Finjustere styling og læsbarhed + +## Åbne designvalg +- Om hjørneindikatorer skal ligge i et fast hjørne eller langs den nærmeste kant +- Om store usikkerheder altid skal tegnes som cirkler på kortet, eller nogle gange kun repræsenteres via indikator +- Hvordan vi bedst navngiver de to perspektiver i UI’et +- Om trails skal vises ens i begge kort eller tones forskelligt + +## Kort version +Samme data. To kort. To forskellige usikkerhedsperspektiver. Dynamisk viewport-logik. Hjørneindikatorer for mulige off-map køretøjer. \ No newline at end of file diff --git a/kort7/app.js b/kort7/app.js new file mode 100644 index 0000000..fbd983f --- /dev/null +++ b/kort7/app.js @@ -0,0 +1,406 @@ +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 alertsEl = document.getElementById("presenceAlerts"); +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: ` +
+ +
+ ` + }); +} + +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 ` +
+
${name}
+
Tid: ${fmtTs(state.current.ts)}
+
Lat: ${state.displayLat.toFixed(5)}
+
Lon: ${state.displayLon.toFixed(5)}
+
Hastighed: ${state.current.speedMps} m/s
+
Retning: ${state.displayHeading.toFixed(0)}°
+
Usikkerhed: ${state.current.uncertaintyM} m
+
På kortet: ${state.presenceStateLabel || "ja"}
+
+ `; +} + +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; +} + +function metersBetween(lat1, lon1, lat2, lon2) { + const meanLatRad = ((lat1 + lat2) / 2) * Math.PI / 180; + const metersPerDegLat = 111320; + const metersPerDegLon = 111320 * Math.cos(meanLatRad); + + const dLatM = (lat2 - lat1) * metersPerDegLat; + const dLonM = (lon2 - lon1) * metersPerDegLon; + + return Math.hypot(dLatM, dLonM); +} + +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} + +function distanceToBoundsMeters(lat, lon, bounds) { + const south = bounds.getSouth(); + const north = bounds.getNorth(); + const west = bounds.getWest(); + const east = bounds.getEast(); + + const clampedLat = clamp(lat, south, north); + const clampedLon = clamp(lon, west, east); + + return metersBetween(lat, lon, clampedLat, clampedLon); +} + +function getPresenceState(lat, lon, uncertaintyM) { + const bounds = map.getBounds(); + const centerInside = bounds.contains([lat, lon]); + + if (centerInside) { + return { + code: "inside", + label: "ja" + }; + } + + if (uncertaintyM > 0) { + const distToBounds = distanceToBoundsMeters(lat, lon, bounds); + if (distToBounds <= uncertaintyM) { + return { + code: "possible", + label: "muligvis" + }; + } + } + + return { + code: "outside", + label: "nej" + }; +} + +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 (first.uncertaintyM > 0) { + 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, + presenceState: "inside", + presenceStateLabel: "ja" + }; + + marker.bindPopup(popupHtml(name, vehicles[name])); + setHeading(marker, first.headingDeg); +} + +let simTime = 0; +let lastFrameTime = null; +let running = true; +const TIME_SCALE = 0.175; + +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.presenceState = "inside"; + v.presenceStateLabel = "ja"; + + 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]]); + v.trail.setStyle({ opacity: 0.85 }); + + if (v.uncertaintyCircle) { + v.uncertaintyCircle.setLatLng([first.lat, first.lon]); + v.uncertaintyCircle.setRadius(first.uncertaintyM); + v.uncertaintyCircle.setStyle({ opacity: 0.7, fillOpacity: 0.08 }); + } + + if (!map.hasLayer(v.marker)) { + v.marker.addTo(map); + } + } + + 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 { + const age = simTime - v.current.ts; + const predictFor = Math.min(age, 8); + const meters = v.current.speedMps * predictFor; + + 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; + + const presence = getPresenceState(lat, lon, v.current.uncertaintyM); + v.presenceState = presence.code; + v.presenceStateLabel = presence.label; + + if (presence.code === "inside") { + if (!map.hasLayer(v.marker)) { + v.marker.addTo(map); + } + v.marker.setLatLng([lat, lon]); + setHeading(v.marker, heading); + v.trail.setStyle({ opacity: 0.85 }); + } else { + if (map.hasLayer(v.marker)) { + map.removeLayer(v.marker); + } + v.trail.setStyle({ opacity: 0.25 }); + } + + v.marker.setPopupContent(popupHtml(v.name, v)); + + if (v.uncertaintyCircle) { + v.uncertaintyCircle.setLatLng([lat, lon]); + v.uncertaintyCircle.setRadius(v.current.uncertaintyM); + + if (presence.code === "inside") { + v.uncertaintyCircle.setStyle({ opacity: 0.7, fillOpacity: 0.08 }); + } else if (presence.code === "possible") { + v.uncertaintyCircle.setStyle({ opacity: 0.35, fillOpacity: 0.03 }); + } else { + v.uncertaintyCircle.setStyle({ opacity: 0.15, fillOpacity: 0.01 }); + } + } +} + +function updateAlerts() { + const possibleVehicles = vehicleNames + .map(name => vehicles[name]) + .filter(v => v.presenceState === "possible"); + + if (possibleVehicles.length === 0) { + alertsEl.innerHTML = ""; + return; + } + + alertsEl.innerHTML = possibleVehicles.map(v => ` +
+ ${v.name}: mulig tilstedeværelse + Estimatet er udenfor kortet, men usikkerheden overlapper det viste område. +
+ `).join(""); +} + +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}\n` + + `Afspilning: 2x`; + + updateAlerts(); +} + +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); +}); + +map.on("moveend zoomend", () => { + for (const name of vehicleNames) { + updateVehicleDisplay(vehicles[name]); + } + updateStatus(); +}); + +resetDemo(); +requestAnimationFrame(tick); diff --git a/kort7/app.js.orig b/kort7/app.js.orig new file mode 100644 index 0000000..2eb754d --- /dev/null +++ b/kort7/app.js.orig @@ -0,0 +1,406 @@ +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 alertsEl = document.getElementById("presenceAlerts"); +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: ` +
+ +
+ ` + }); +} + +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 ` +
+
${name}
+
Tid: ${fmtTs(state.current.ts)}
+
Lat: ${state.displayLat.toFixed(5)}
+
Lon: ${state.displayLon.toFixed(5)}
+
Hastighed: ${state.current.speedMps} m/s
+
Retning: ${state.displayHeading.toFixed(0)}°
+
Usikkerhed: ${state.current.uncertaintyM} m
+
På kortet: ${state.presenceStateLabel || "ja"}
+
+ `; +} + +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; +} + +function metersBetween(lat1, lon1, lat2, lon2) { + const meanLatRad = ((lat1 + lat2) / 2) * Math.PI / 180; + const metersPerDegLat = 111320; + const metersPerDegLon = 111320 * Math.cos(meanLatRad); + + const dLatM = (lat2 - lat1) * metersPerDegLat; + const dLonM = (lon2 - lon1) * metersPerDegLon; + + return Math.hypot(dLatM, dLonM); +} + +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} + +function distanceToBoundsMeters(lat, lon, bounds) { + const south = bounds.getSouth(); + const north = bounds.getNorth(); + const west = bounds.getWest(); + const east = bounds.getEast(); + + const clampedLat = clamp(lat, south, north); + const clampedLon = clamp(lon, west, east); + + return metersBetween(lat, lon, clampedLat, clampedLon); +} + +function getPresenceState(lat, lon, uncertaintyM) { + const bounds = map.getBounds(); + const centerInside = bounds.contains([lat, lon]); + + if (centerInside) { + return { + code: "inside", + label: "ja" + }; + } + + if (uncertaintyM > 0) { + const distToBounds = distanceToBoundsMeters(lat, lon, bounds); + if (distToBounds <= uncertaintyM) { + return { + code: "possible", + label: "muligvis" + }; + } + } + + return { + code: "outside", + label: "nej" + }; +} + +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, + presenceState: "inside", + presenceStateLabel: "ja" + }; + + marker.bindPopup(popupHtml(name, vehicles[name])); + setHeading(marker, first.headingDeg); +} + +let simTime = 0; +let lastFrameTime = null; +let running = true; +const TIME_SCALE = 0.175; + +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.presenceState = "inside"; + v.presenceStateLabel = "ja"; + + 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]]); + v.trail.setStyle({ opacity: 0.85 }); + + if (v.uncertaintyCircle) { + v.uncertaintyCircle.setLatLng([first.lat, first.lon]); + v.uncertaintyCircle.setRadius(first.uncertaintyM); + v.uncertaintyCircle.setStyle({ opacity: 0.7, fillOpacity: 0.08 }); + } + + if (!map.hasLayer(v.marker)) { + v.marker.addTo(map); + } + } + + 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 { + const age = simTime - v.current.ts; + const predictFor = Math.min(age, 8); + const meters = v.current.speedMps * predictFor; + + 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; + + const presence = getPresenceState(lat, lon, v.current.uncertaintyM); + v.presenceState = presence.code; + v.presenceStateLabel = presence.label; + + if (presence.code === "inside") { + if (!map.hasLayer(v.marker)) { + v.marker.addTo(map); + } + v.marker.setLatLng([lat, lon]); + setHeading(v.marker, heading); + v.trail.setStyle({ opacity: 0.85 }); + } else { + if (map.hasLayer(v.marker)) { + map.removeLayer(v.marker); + } + v.trail.setStyle({ opacity: 0.25 }); + } + + v.marker.setPopupContent(popupHtml(v.name, v)); + + if (v.uncertaintyCircle) { + v.uncertaintyCircle.setLatLng([lat, lon]); + v.uncertaintyCircle.setRadius(v.current.uncertaintyM); + + if (presence.code === "inside") { + v.uncertaintyCircle.setStyle({ opacity: 0.7, fillOpacity: 0.08 }); + } else if (presence.code === "possible") { + v.uncertaintyCircle.setStyle({ opacity: 0.35, fillOpacity: 0.03 }); + } else { + v.uncertaintyCircle.setStyle({ opacity: 0.15, fillOpacity: 0.01 }); + } + } +} + +function updateAlerts() { + const possibleVehicles = vehicleNames + .map(name => vehicles[name]) + .filter(v => v.presenceState === "possible"); + + if (possibleVehicles.length === 0) { + alertsEl.innerHTML = ""; + return; + } + + alertsEl.innerHTML = possibleVehicles.map(v => ` +
+ ${v.name}: mulig tilstedeværelse + Estimatet er udenfor kortet, men usikkerheden overlapper det viste område. +
+ `).join(""); +} + +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}\n` + + `Afspilning: 2x`; + + updateAlerts(); +} + +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); +}); + +map.on("moveend zoomend", () => { + for (const name of vehicleNames) { + updateVehicleDisplay(vehicles[name]); + } + updateStatus(); +}); + +resetDemo(); +requestAnimationFrame(tick); diff --git a/kort7/data.js b/kort7/data.js new file mode 100644 index 0000000..6d6a86a --- /dev/null +++ b/kort7/data.js @@ -0,0 +1,74 @@ +const VEHICLE_EVENTS = [ + { vehicle: "Bil 3", ts: 0, lat: 55.6780, lon: 12.5600, speedMps: 8, headingDeg: 190, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 2, lat: 55.6748, lon: 12.5650, speedMps: 9, headingDeg: 45, uncertaintyM: 1200 }, + { vehicle: "Bil 1", ts: 4, lat: 55.6761, lon: 12.5683, speedMps: 12, headingDeg: 90, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 5, lat: 55.6728, lon: 12.5710, speedMps: 7, headingDeg: 330, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 6, lat: 55.6795, lon: 12.5845, speedMps: 11, headingDeg: 260, uncertaintyM: 0 }, + + { vehicle: "Bil 6", ts: 8, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, + + { vehicle: "Bil 7", ts: 10, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, + + { vehicle: "Bil 3", ts: 12, lat: 55.6772, lon: 12.5603, speedMps: 8, headingDeg: 195, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 13, lat: 55.6760, lon: 12.5670, speedMps: 10, headingDeg: 50, uncertaintyM: 1000 }, + { vehicle: "Bil 1", ts: 15, lat: 55.6762, lon: 12.5720, speedMps: 13, headingDeg: 95, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 16, lat: 55.6736, lon: 12.5698, speedMps: 8, headingDeg: 340, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 18, lat: 55.6793, lon: 12.5815, speedMps: 12, headingDeg: 255, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 24, lat: 55.6765, lon: 12.5610, speedMps: 9, headingDeg: 200, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 26, lat: 55.6772, lon: 12.5692, speedMps: 11, headingDeg: 55, uncertaintyM: 900 }, + { vehicle: "Bil 1", ts: 27, lat: 55.6763, lon: 12.5758, speedMps: 14, headingDeg: 100, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 28, lat: 55.6746, lon: 12.5688, speedMps: 8, headingDeg: 350, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 29, lat: 55.6790, lon: 12.5784, speedMps: 12, headingDeg: 250, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 38, lat: 55.6758, lon: 12.5620, speedMps: 9, headingDeg: 210, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 40, lat: 55.6785, lon: 12.5716, speedMps: 12, headingDeg: 60, uncertaintyM: 850 }, + { vehicle: "Bil 1", ts: 41, lat: 55.6764, lon: 12.5798, speedMps: 14, headingDeg: 105, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 42, lat: 55.6757, lon: 12.5682, speedMps: 9, headingDeg: 0, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 43, lat: 55.6786, lon: 12.5753, speedMps: 11, headingDeg: 245, uncertaintyM: 0 }, + + { vehicle: "Bil 6", ts: 44, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, + { vehicle: "Bil 7", ts: 46, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, + + { vehicle: "Bil 3", ts: 50, lat: 55.6752, lon: 12.5634, speedMps: 10, headingDeg: 220, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 54, lat: 55.6796, lon: 12.5744, speedMps: 12, headingDeg: 70, uncertaintyM: 800 }, + { vehicle: "Bil 1", ts: 55, lat: 55.6766, lon: 12.5837, speedMps: 15, headingDeg: 110, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 56, lat: 55.6769, lon: 12.5682, speedMps: 9, headingDeg: 10, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 57, lat: 55.6781, lon: 12.5722, speedMps: 10, headingDeg: 240, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 66, lat: 55.6747, lon: 12.5652, speedMps: 10, headingDeg: 230, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 68, lat: 55.6804, lon: 12.5776, speedMps: 11, headingDeg: 85, uncertaintyM: 950 }, + { vehicle: "Bil 1", ts: 69, lat: 55.6768, lon: 12.5875, speedMps: 14, headingDeg: 115, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 70, lat: 55.6781, lon: 12.5688, speedMps: 10, headingDeg: 20, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 72, lat: 55.6776, lon: 12.5692, speedMps: 10, headingDeg: 235, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 82, lat: 55.6744, lon: 12.5673, speedMps: 9, headingDeg: 240, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 84, lat: 55.6810, lon: 12.5808, speedMps: 10, headingDeg: 100, uncertaintyM: 1100 }, + { vehicle: "Bil 1", ts: 85, lat: 55.6770, lon: 12.5912, speedMps: 13, headingDeg: 120, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 87, lat: 55.6792, lon: 12.5700, speedMps: 10, headingDeg: 30, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 88, lat: 55.6771, lon: 12.5662, speedMps: 9, headingDeg: 230, uncertaintyM: 0 }, + + { vehicle: "Bil 6", ts: 90, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, + { vehicle: "Bil 7", ts: 92, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, + + { vehicle: "Bil 3", ts: 96, lat: 55.6742, lon: 12.5696, speedMps: 8, headingDeg: 250, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 98, lat: 55.6812, lon: 12.5842, speedMps: 10, headingDeg: 115, uncertaintyM: 1200 }, + { vehicle: "Bil 1", ts: 99, lat: 55.6772, lon: 12.5948, speedMps: 12, headingDeg: 125, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 101,lat: 55.6800, lon: 12.5716, speedMps: 9, headingDeg: 40, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 102,lat: 55.6766, lon: 12.5634, speedMps: 9, headingDeg: 225, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 112,lat: 55.6742, lon: 12.5720, speedMps: 8, headingDeg: 260, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 114,lat: 55.6811, lon: 12.5877, speedMps: 9, headingDeg: 125, uncertaintyM: 1000 }, + { vehicle: "Bil 1", ts: 115,lat: 55.6775, lon: 12.5982, speedMps: 11, headingDeg: 130, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 117,lat: 55.6806, lon: 12.5736, speedMps: 8, headingDeg: 50, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 118,lat: 55.6761, lon: 12.5608, speedMps: 8, headingDeg: 220, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 }, + { vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 }, + + { vehicle: "Bil 6", ts: 134,lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, + { vehicle: "Bil 7", ts: 134,lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 } +]; diff --git a/kort7/data.js.orig b/kort7/data.js.orig new file mode 100644 index 0000000..345d701 --- /dev/null +++ b/kort7/data.js.orig @@ -0,0 +1,61 @@ +const VEHICLE_EVENTS = [ + { vehicle: "Bil 3", ts: 0, lat: 55.6780, lon: 12.5600, speedMps: 8, headingDeg: 190, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 2, lat: 55.6748, lon: 12.5650, speedMps: 9, headingDeg: 45, uncertaintyM: 1200 }, + { vehicle: "Bil 1", ts: 4, lat: 55.6761, lon: 12.5683, speedMps: 12, headingDeg: 90, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 5, lat: 55.6728, lon: 12.5710, speedMps: 7, headingDeg: 330, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 6, lat: 55.6795, lon: 12.5845, speedMps: 11, headingDeg: 260, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 12, lat: 55.6772, lon: 12.5603, speedMps: 8, headingDeg: 195, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 13, lat: 55.6760, lon: 12.5670, speedMps: 10, headingDeg: 50, uncertaintyM: 1000 }, + { vehicle: "Bil 1", ts: 15, lat: 55.6762, lon: 12.5720, speedMps: 13, headingDeg: 95, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 16, lat: 55.6736, lon: 12.5698, speedMps: 8, headingDeg: 340, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 18, lat: 55.6793, lon: 12.5815, speedMps: 12, headingDeg: 255, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 24, lat: 55.6765, lon: 12.5610, speedMps: 9, headingDeg: 200, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 26, lat: 55.6772, lon: 12.5692, speedMps: 11, headingDeg: 55, uncertaintyM: 900 }, + { vehicle: "Bil 1", ts: 27, lat: 55.6763, lon: 12.5758, speedMps: 14, headingDeg: 100, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 28, lat: 55.6746, lon: 12.5688, speedMps: 8, headingDeg: 350, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 29, lat: 55.6790, lon: 12.5784, speedMps: 12, headingDeg: 250, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 38, lat: 55.6758, lon: 12.5620, speedMps: 9, headingDeg: 210, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 40, lat: 55.6785, lon: 12.5716, speedMps: 12, headingDeg: 60, uncertaintyM: 850 }, + { vehicle: "Bil 1", ts: 41, lat: 55.6764, lon: 12.5798, speedMps: 14, headingDeg: 105, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 42, lat: 55.6757, lon: 12.5682, speedMps: 9, headingDeg: 0, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 43, lat: 55.6786, lon: 12.5753, speedMps: 11, headingDeg: 245, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 50, lat: 55.6752, lon: 12.5634, speedMps: 10, headingDeg: 220, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 54, lat: 55.6796, lon: 12.5744, speedMps: 12, headingDeg: 70, uncertaintyM: 800 }, + { vehicle: "Bil 1", ts: 55, lat: 55.6766, lon: 12.5837, speedMps: 15, headingDeg: 110, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 56, lat: 55.6769, lon: 12.5682, speedMps: 9, headingDeg: 10, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 57, lat: 55.6781, lon: 12.5722, speedMps: 10, headingDeg: 240, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 66, lat: 55.6747, lon: 12.5652, speedMps: 10, headingDeg: 230, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 68, lat: 55.6804, lon: 12.5776, speedMps: 11, headingDeg: 85, uncertaintyM: 950 }, + { vehicle: "Bil 1", ts: 69, lat: 55.6768, lon: 12.5875, speedMps: 14, headingDeg: 115, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 70, lat: 55.6781, lon: 12.5688, speedMps: 10, headingDeg: 20, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 72, lat: 55.6776, lon: 12.5692, speedMps: 10, headingDeg: 235, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 82, lat: 55.6744, lon: 12.5673, speedMps: 9, headingDeg: 240, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 84, lat: 55.6810, lon: 12.5808, speedMps: 10, headingDeg: 100, uncertaintyM: 1100 }, + { vehicle: "Bil 1", ts: 85, lat: 55.6770, lon: 12.5912, speedMps: 13, headingDeg: 120, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 87, lat: 55.6792, lon: 12.5700, speedMps: 10, headingDeg: 30, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 88, lat: 55.6771, lon: 12.5662, speedMps: 9, headingDeg: 230, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 96, lat: 55.6742, lon: 12.5696, speedMps: 8, headingDeg: 250, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 98, lat: 55.6812, lon: 12.5842, speedMps: 10, headingDeg: 115, uncertaintyM: 1200 }, + { vehicle: "Bil 1", ts: 99, lat: 55.6772, lon: 12.5948, speedMps: 12, headingDeg: 125, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 101,lat: 55.6800, lon: 12.5716, speedMps: 9, headingDeg: 40, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 102,lat: 55.6766, lon: 12.5634, speedMps: 9, headingDeg: 225, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 112,lat: 55.6742, lon: 12.5720, speedMps: 8, headingDeg: 260, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 114,lat: 55.6811, lon: 12.5877, speedMps: 9, headingDeg: 125, uncertaintyM: 1000 }, + { vehicle: "Bil 1", ts: 115,lat: 55.6775, lon: 12.5982, speedMps: 11, headingDeg: 130, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 117,lat: 55.6806, lon: 12.5736, speedMps: 8, headingDeg: 50, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 118,lat: 55.6761, lon: 12.5608, speedMps: 8, headingDeg: 220, uncertaintyM: 0 }, + + { vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 }, + { vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 }, + { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 } +]; diff --git a/kort7/data.js.rej b/kort7/data.js.rej new file mode 100644 index 0000000..c2c9c48 --- /dev/null +++ b/kort7/data.js.rej @@ -0,0 +1,19 @@ +--- data.js ++++ data.js +@@ -53,5 +53,15 @@ + { vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 }, + { vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 }, +- { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 } ++ { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 }, ++ ++ { vehicle: "Bil 6", ts: 8, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, ++ { vehicle: "Bil 6", ts: 44, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, ++ { vehicle: "Bil 6", ts: 90, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, ++ { vehicle: "Bil 6", ts: 134,lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, ++ ++ { vehicle: "Bil 7", ts: 10, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, ++ { vehicle: "Bil 7", ts: 46, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, ++ { vehicle: "Bil 7", ts: 92, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, ++ { vehicle: "Bil 7", ts: 134,lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 } diff --git a/kort7/kort6.html.orig b/kort7/kort6.html.orig new file mode 100644 index 0000000..1f70ddf --- /dev/null +++ b/kort7/kort6.html.orig @@ -0,0 +1,75 @@ + + + + + + Vehicle stream demo + + + + + + + +
+ +
+
+ + + +
+
+
+ + + + + diff --git a/kort7/kort7.html b/kort7/kort7.html new file mode 100644 index 0000000..a3b600a --- /dev/null +++ b/kort7/kort7.html @@ -0,0 +1,94 @@ + + + + + + Vehicle stream demo + + + + + + + +
+ +
+
+ + + +
+
+
+
+ + + + + diff --git a/kort7/patch b/kort7/patch new file mode 100644 index 0000000..58acdca --- /dev/null +++ b/kort7/patch @@ -0,0 +1,37 @@ +--- a/kort6.html ++++ b/kort6.html +@@ -41,6 +41,24 @@ + .hud .status { + margin-top: 8px; + white-space: pre-line; + } ++ ++ .hud .alerts { ++ margin-top: 8px; ++ display: grid; ++ gap: 6px; ++ max-width: 320px; ++ } ++ ++ .alert-item { ++ padding: 8px 10px; ++ border-radius: 8px; ++ border: 1px solid #e0b100; ++ background: rgba(255,248,204,0.96); ++ color: #5f4b00; ++ font: 12px/1.35 system-ui,sans-serif; ++ } ++ ++ .alert-item strong { display:block; margin-bottom:2px; } + + .car { + width: 24px; + height: 24px; +@@ -86,6 +104,7 @@ + + +
++
+ + + diff --git a/kort7/patch2 b/kort7/patch2 new file mode 100644 index 0000000..3ad61ef --- /dev/null +++ b/kort7/patch2 @@ -0,0 +1,20 @@ +--- a/data.js ++++ b/data.js +@@ -53,5 +53,15 @@ + { vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 }, + { vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 }, + { vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 }, + { vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 }, +- { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 } ++ { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 }, ++ ++ { vehicle: "Bil 6", ts: 8, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, ++ { vehicle: "Bil 6", ts: 44, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, ++ { vehicle: "Bil 6", ts: 90, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, ++ { vehicle: "Bil 6", ts: 134,lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 }, ++ ++ { vehicle: "Bil 7", ts: 10, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, ++ { vehicle: "Bil 7", ts: 46, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, ++ { vehicle: "Bil 7", ts: 92, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }, ++ { vehicle: "Bil 7", ts: 134,lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 } + ]; diff --git a/kort7/patch3 b/kort7/patch3 new file mode 100644 index 0000000..16df245 --- /dev/null +++ b/kort7/patch3 @@ -0,0 +1,11 @@ +--- a/app.js ++++ b/app.js +@@ -77,7 +77,7 @@ + }).addTo(map); + + let uncertaintyCircle = null; +- if (name === "Bil 2") { ++ if (first.uncertaintyM > 0) { + uncertaintyCircle = L.circle([first.lat, first.lon], { + radius: first.uncertaintyM, + color: color,