325 lines
9.6 KiB
HTML
325 lines
9.6 KiB
HTML
<!doctype html>
|
|
<html lang="da">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<title>GPS demo med 5 biler og 10 position-sæt</title>
|
|
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin=""
|
|
/>
|
|
<script
|
|
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""
|
|
></script>
|
|
|
|
<style>
|
|
html, body {
|
|
height: 100%;
|
|
margin: 0;
|
|
}
|
|
|
|
#map {
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.hud {
|
|
position: absolute;
|
|
z-index: 1000;
|
|
top: 10px;
|
|
left: 10px;
|
|
background: rgba(255,255,255,0.92);
|
|
padding: 10px 12px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.15);
|
|
font: 13px/1.35 system-ui, sans-serif;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.hud button {
|
|
margin-right: 8px;
|
|
padding: 6px 10px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.hud .status {
|
|
margin-top: 6px;
|
|
color: #333;
|
|
}
|
|
|
|
.car {
|
|
width: 22px;
|
|
height: 22px;
|
|
transform-origin: 11px 11px;
|
|
will-change: transform;
|
|
filter: drop-shadow(0 1px 1px rgba(0,0,0,.35));
|
|
}
|
|
|
|
.car svg {
|
|
display: block;
|
|
width: 22px;
|
|
height: 22px;
|
|
overflow: visible;
|
|
}
|
|
|
|
.car-label {
|
|
position: relative;
|
|
top: -2px;
|
|
left: 26px;
|
|
font: 12px/1.2 system-ui, sans-serif;
|
|
background: rgba(255,255,255,0.88);
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.15);
|
|
white-space: nowrap;
|
|
display: inline-block;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="map"></div>
|
|
|
|
<div class="hud">
|
|
<div>
|
|
<button id="btnPlay">Play</button>
|
|
<button id="btnPause">Pause</button>
|
|
<button id="btnReset">Reset</button>
|
|
</div>
|
|
<div class="status">
|
|
10 sæt positioner (a → b → c ...) med 5 biler.
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 10 "frames" / sæt positioner
|
|
// Hver frame indeholder 5 biler med lat, lon, headingDeg og speedMps
|
|
const FRAMES = [
|
|
{ // a
|
|
"Bil 1": {lat:55.6761, lon:12.5683, headingDeg: 90, speedMps:12},
|
|
"Bil 2": {lat:55.6748, lon:12.5650, headingDeg: 40, speedMps: 9},
|
|
"Bil 3": {lat:55.6780, lon:12.5600, headingDeg:180, speedMps: 8},
|
|
"Bil 4": {lat:55.6728, lon:12.5710, headingDeg:330, speedMps: 7},
|
|
"Bil 5": {lat:55.6795, lon:12.5845, headingDeg:260, speedMps:11},
|
|
},
|
|
{ // b
|
|
"Bil 1": {lat:55.6762, lon:12.5720, headingDeg: 95, speedMps:13},
|
|
"Bil 2": {lat:55.6760, lon:12.5670, headingDeg: 45, speedMps:10},
|
|
"Bil 3": {lat:55.6772, lon:12.5603, headingDeg:190, speedMps: 8},
|
|
"Bil 4": {lat:55.6736, lon:12.5698, headingDeg:340, speedMps: 8},
|
|
"Bil 5": {lat:55.6793, lon:12.5815, headingDeg:255, speedMps:12},
|
|
},
|
|
{ // c
|
|
"Bil 1": {lat:55.6763, lon:12.5758, headingDeg:100, speedMps:14},
|
|
"Bil 2": {lat:55.6772, lon:12.5692, headingDeg: 50, speedMps:11},
|
|
"Bil 3": {lat:55.6765, lon:12.5610, headingDeg:200, speedMps: 9},
|
|
"Bil 4": {lat:55.6746, lon:12.5688, headingDeg:350, speedMps: 8},
|
|
"Bil 5": {lat:55.6790, lon:12.5784, headingDeg:250, speedMps:12},
|
|
},
|
|
{ // d
|
|
"Bil 1": {lat:55.6764, lon:12.5798, headingDeg:105, speedMps:14},
|
|
"Bil 2": {lat:55.6785, lon:12.5716, headingDeg: 55, speedMps:12},
|
|
"Bil 3": {lat:55.6758, lon:12.5620, headingDeg:210, speedMps: 9},
|
|
"Bil 4": {lat:55.6757, lon:12.5682, headingDeg: 0, speedMps: 9},
|
|
"Bil 5": {lat:55.6786, lon:12.5753, headingDeg:245, speedMps:11},
|
|
},
|
|
{ // e
|
|
"Bil 1": {lat:55.6766, lon:12.5837, headingDeg:110, speedMps:15},
|
|
"Bil 2": {lat:55.6796, lon:12.5744, headingDeg: 60, speedMps:12},
|
|
"Bil 3": {lat:55.6752, lon:12.5634, headingDeg:220, speedMps:10},
|
|
"Bil 4": {lat:55.6769, lon:12.5682, headingDeg: 10, speedMps: 9},
|
|
"Bil 5": {lat:55.6781, lon:12.5722, headingDeg:240, speedMps:10},
|
|
},
|
|
{ // f
|
|
"Bil 1": {lat:55.6768, lon:12.5875, headingDeg:115, speedMps:14},
|
|
"Bil 2": {lat:55.6804, lon:12.5776, headingDeg: 70, speedMps:11},
|
|
"Bil 3": {lat:55.6747, lon:12.5652, headingDeg:230, speedMps:10},
|
|
"Bil 4": {lat:55.6781, lon:12.5688, headingDeg: 20, speedMps:10},
|
|
"Bil 5": {lat:55.6776, lon:12.5692, headingDeg:235, speedMps:10},
|
|
},
|
|
{ // g
|
|
"Bil 1": {lat:55.6770, lon:12.5912, headingDeg:120, speedMps:13},
|
|
"Bil 2": {lat:55.6810, lon:12.5808, headingDeg: 85, speedMps:10},
|
|
"Bil 3": {lat:55.6744, lon:12.5673, headingDeg:240, speedMps: 9},
|
|
"Bil 4": {lat:55.6792, lon:12.5700, headingDeg: 30, speedMps:10},
|
|
"Bil 5": {lat:55.6771, lon:12.5662, headingDeg:230, speedMps: 9},
|
|
},
|
|
{ // h
|
|
"Bil 1": {lat:55.6772, lon:12.5948, headingDeg:125, speedMps:12},
|
|
"Bil 2": {lat:55.6812, lon:12.5842, headingDeg:100, speedMps:10},
|
|
"Bil 3": {lat:55.6742, lon:12.5696, headingDeg:250, speedMps: 8},
|
|
"Bil 4": {lat:55.6800, lon:12.5716, headingDeg: 40, speedMps: 9},
|
|
"Bil 5": {lat:55.6766, lon:12.5634, headingDeg:225, speedMps: 9},
|
|
},
|
|
{ // i
|
|
"Bil 1": {lat:55.6775, lon:12.5982, headingDeg:130, speedMps:11},
|
|
"Bil 2": {lat:55.6811, lon:12.5877, headingDeg:115, speedMps: 9},
|
|
"Bil 3": {lat:55.6742, lon:12.5720, headingDeg:260, speedMps: 8},
|
|
"Bil 4": {lat:55.6806, lon:12.5736, headingDeg: 50, speedMps: 8},
|
|
"Bil 5": {lat:55.6761, lon:12.5608, headingDeg:220, speedMps: 8},
|
|
},
|
|
{ // j
|
|
"Bil 1": {lat:55.6778, lon:12.6016, headingDeg:135, speedMps:10},
|
|
"Bil 2": {lat:55.6808, lon:12.5910, headingDeg:130, speedMps: 8},
|
|
"Bil 3": {lat:55.6744, lon:12.5742, headingDeg:270, speedMps: 7},
|
|
"Bil 4": {lat:55.6809, lon:12.5758, headingDeg: 60, speedMps: 7},
|
|
"Bil 5": {lat:55.6756, lon:12.5584, headingDeg:215, speedMps: 8},
|
|
},
|
|
];
|
|
|
|
// Varighed pr. overgang mellem a->b, b->c osv.
|
|
const STEP_MS = 1500;
|
|
|
|
const map = L.map('map').setView([55.6761, 12.5683], 13);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(map);
|
|
|
|
function carDivIcon(label) {
|
|
const html = `
|
|
<div class="car" data-role="car">
|
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
<path d="M12 2 L20 22 L12 18 L4 22 Z" fill="#1565c0"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="car-label">${label}</div>
|
|
`;
|
|
return L.divIcon({
|
|
html,
|
|
className: '',
|
|
iconSize: [1, 1],
|
|
iconAnchor: [11, 11]
|
|
});
|
|
}
|
|
|
|
function smoothstep(t) {
|
|
if (t < 0) t = 0;
|
|
if (t > 1) t = 1;
|
|
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 setMarkerHeading(marker, headingDeg) {
|
|
const el = marker.getElement();
|
|
if (!el) return;
|
|
|
|
const carEl = el.querySelector('[data-role="car"]');
|
|
if (!carEl) return;
|
|
|
|
carEl.style.transform = `rotate(${headingDeg}deg)`;
|
|
}
|
|
|
|
const carNames = Object.keys(FRAMES[0]);
|
|
const markers = new Map();
|
|
|
|
for (const name of carNames) {
|
|
const pos = FRAMES[0][name];
|
|
const marker = L.marker([pos.lat, pos.lon], {
|
|
icon: carDivIcon(name)
|
|
}).addTo(map);
|
|
|
|
markers.set(name, marker);
|
|
setMarkerHeading(marker, pos.headingDeg);
|
|
}
|
|
|
|
let playing = false;
|
|
let rafId = null;
|
|
let frameIdx = 0;
|
|
let segStartT = performance.now();
|
|
|
|
function reset() {
|
|
playing = false;
|
|
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
rafId = null;
|
|
}
|
|
|
|
frameIdx = 0;
|
|
segStartT = performance.now();
|
|
|
|
for (const name of carNames) {
|
|
const p = FRAMES[0][name];
|
|
const marker = markers.get(name);
|
|
marker.setLatLng([p.lat, p.lon]);
|
|
setMarkerHeading(marker, p.headingDeg);
|
|
}
|
|
}
|
|
|
|
function play() {
|
|
if (playing) return;
|
|
playing = true;
|
|
rafId = requestAnimationFrame(tick);
|
|
}
|
|
|
|
function pause() {
|
|
playing = false;
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
rafId = null;
|
|
}
|
|
}
|
|
|
|
function tick(now) {
|
|
if (!playing) return;
|
|
|
|
const currentFrame = frameIdx;
|
|
const nextFrame = currentFrame + 1;
|
|
|
|
if (nextFrame >= FRAMES.length) {
|
|
playing = false;
|
|
return;
|
|
}
|
|
|
|
const rawAlpha = (now - segStartT) / STEP_MS;
|
|
const alpha = smoothstep(rawAlpha);
|
|
|
|
for (const name of carNames) {
|
|
const p0 = FRAMES[currentFrame][name];
|
|
const p1 = FRAMES[nextFrame][name];
|
|
|
|
const lat = lerp(p0.lat, p1.lat, alpha);
|
|
const lon = lerp(p0.lon, p1.lon, alpha);
|
|
const heading = lerpAngleDeg(p0.headingDeg, p1.headingDeg, alpha);
|
|
|
|
const marker = markers.get(name);
|
|
marker.setLatLng([lat, lon]);
|
|
setMarkerHeading(marker, heading);
|
|
}
|
|
|
|
if (rawAlpha >= 1) {
|
|
frameIdx += 1;
|
|
segStartT = now;
|
|
}
|
|
|
|
rafId = requestAnimationFrame(tick);
|
|
}
|
|
|
|
document.getElementById('btnPlay').addEventListener('click', play);
|
|
document.getElementById('btnPause').addEventListener('click', pause);
|
|
document.getElementById('btnReset').addEventListener('click', () => {
|
|
reset();
|
|
play();
|
|
});
|
|
|
|
reset();
|
|
play();
|
|
</script>
|
|
</body>
|
|
</html>
|