commit 7133b57e248ec34ed6e35270a8112ad72dee5cf5 Author: Morten V. Christiansen Date: Sun Apr 5 12:06:27 2026 +0200 init version 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 0000000..950bf0e Binary files /dev/null and b/kort2.zip differ 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,