kort/kort4.html

383 lines
10 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 trails</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
html, body {
height: 100%;
margin: 0;
}
#map {
height: 100%;
}
.hud {
position: absolute;
top: 10px;
left: 10px;
z-index: 1000;
background: rgba(255,255,255,0.95);
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #ccc;
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;
}
.car {
width: 24px;
height: 24px;
transform-origin: 12px 12px;
will-change: transform;
filter: drop-shadow(0 1px 1px rgba(0,0,0,.35));
}
.car svg {
width: 24px;
height: 24px;
display: block;
}
.car-label {
position: relative;
top: -2px;
left: 26px;
display: inline-block;
font: 12px/1.2 system-ui, sans-serif;
background: rgba(255,255,255,0.9);
padding: 2px 6px;
border-radius: 10px;
border: 1px solid #ccc;
white-space: nowrap;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="hud">
<button id="play">Play</button>
<button id="pause">Pause</button>
<button id="reset">Reset</button>
</div>
<script>
// 10 koordinatsæt (frames)
const FRAMES = [
{
"Bil 1": {lat:55.6761, lon:12.5683, speedMps:12},
"Bil 2": {lat:55.6748, lon:12.5650, speedMps: 9},
"Bil 3": {lat:55.6780, lon:12.5600, speedMps: 8},
"Bil 4": {lat:55.6728, lon:12.5710, speedMps: 7},
"Bil 5": {lat:55.6795, lon:12.5845, speedMps:11}
},
{
"Bil 1": {lat:55.6762, lon:12.5720, speedMps:13},
"Bil 2": {lat:55.6760, lon:12.5670, speedMps:10},
"Bil 3": {lat:55.6772, lon:12.5603, speedMps: 8},
"Bil 4": {lat:55.6736, lon:12.5698, speedMps: 8},
"Bil 5": {lat:55.6793, lon:12.5815, speedMps:12}
},
{
"Bil 1": {lat:55.6763, lon:12.5758, speedMps:14},
"Bil 2": {lat:55.6772, lon:12.5692, speedMps:11},
"Bil 3": {lat:55.6765, lon:12.5610, speedMps: 9},
"Bil 4": {lat:55.6746, lon:12.5688, speedMps: 8},
"Bil 5": {lat:55.6790, lon:12.5784, speedMps:12}
},
{
"Bil 1": {lat:55.6764, lon:12.5798, speedMps:14},
"Bil 2": {lat:55.6785, lon:12.5716, speedMps:12},
"Bil 3": {lat:55.6758, lon:12.5620, speedMps: 9},
"Bil 4": {lat:55.6757, lon:12.5682, speedMps: 9},
"Bil 5": {lat:55.6786, lon:12.5753, speedMps:11}
},
{
"Bil 1": {lat:55.6766, lon:12.5837, speedMps:15},
"Bil 2": {lat:55.6796, lon:12.5744, speedMps:12},
"Bil 3": {lat:55.6752, lon:12.5634, speedMps:10},
"Bil 4": {lat:55.6769, lon:12.5682, speedMps: 9},
"Bil 5": {lat:55.6781, lon:12.5722, speedMps:10}
},
{
"Bil 1": {lat:55.6768, lon:12.5875, speedMps:14},
"Bil 2": {lat:55.6804, lon:12.5776, speedMps:11},
"Bil 3": {lat:55.6747, lon:12.5652, speedMps:10},
"Bil 4": {lat:55.6781, lon:12.5688, speedMps:10},
"Bil 5": {lat:55.6776, lon:12.5692, speedMps:10}
},
{
"Bil 1": {lat:55.6770, lon:12.5912, speedMps:13},
"Bil 2": {lat:55.6810, lon:12.5808, speedMps:10},
"Bil 3": {lat:55.6744, lon:12.5673, speedMps: 9},
"Bil 4": {lat:55.6792, lon:12.5700, speedMps:10},
"Bil 5": {lat:55.6771, lon:12.5662, speedMps: 9}
},
{
"Bil 1": {lat:55.6772, lon:12.5948, speedMps:12},
"Bil 2": {lat:55.6812, lon:12.5842, speedMps:10},
"Bil 3": {lat:55.6742, lon:12.5696, speedMps: 8},
"Bil 4": {lat:55.6800, lon:12.5716, speedMps: 9},
"Bil 5": {lat:55.6766, lon:12.5634, speedMps: 9}
},
{
"Bil 1": {lat:55.6775, lon:12.5982, speedMps:11},
"Bil 2": {lat:55.6811, lon:12.5877, speedMps: 9},
"Bil 3": {lat:55.6742, lon:12.5720, speedMps: 8},
"Bil 4": {lat:55.6806, lon:12.5736, speedMps: 8},
"Bil 5": {lat:55.6761, lon:12.5608, speedMps: 8}
},
{
"Bil 1": {lat:55.6778, lon:12.6016, speedMps:10},
"Bil 2": {lat:55.6808, lon:12.5910, speedMps: 8},
"Bil 3": {lat:55.6744, lon:12.5742, speedMps: 7},
"Bil 4": {lat:55.6809, lon:12.5758, speedMps: 7},
"Bil 5": {lat:55.6756, lon:12.5584, speedMps: 8}
}
];
const COLORS = {
"Bil 1": "#ff0000",
"Bil 2": "#00c853",
"Bil 3": "#2979ff",
"Bil 4": "#2979ff",
"Bil 5": "#2979ff"
};
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", {
attribution: "&copy; OpenStreetMap contributors",
maxZoom: 19
}).addTo(map);
function carIcon(label, color) {
return L.divIcon({
className: "",
iconSize: [1, 1],
iconAnchor: [12, 12],
html: `
<div class="car" data-role="car">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 2 L20 22 L12 18 L4 22 Z"
fill="${color}"
stroke="#ffffff"
stroke-width="1.5"
/>
</svg>
</div>
<div class="car-label">${label}</div>
`
});
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function smooth(t) {
return t * t * (3 - 2 * t);
}
function bearing(p0, p1) {
const dy = p1.lat - p0.lat;
const dx = p1.lon - p0.lon;
const angle = Math.atan2(dx, dy) * 180 / Math.PI;
return (angle + 360) % 360;
}
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 formatPopupHtml(name, point, headingDeg, frameIndex) {
return `
<div style="font:13px/1.4 system-ui,sans-serif; min-width:160px;">
<div style="font-weight:600; margin-bottom:6px;">${name}</div>
<div><strong>Frame:</strong> ${frameIndex + 1} / ${FRAMES.length}</div>
<div><strong>Lat:</strong> ${point.lat.toFixed(5)}</div>
<div><strong>Lon:</strong> ${point.lon.toFixed(5)}</div>
<div><strong>Hastighed:</strong> ${point.speedMps} m/s</div>
<div><strong>Retning:</strong> ${headingDeg.toFixed(0)}°</div>
</div>
`;
}
const cars = Object.keys(FRAMES[0]);
const markers = {};
const trails = {};
const currentState = {};
for (const name of cars) {
const p = FRAMES[0][name];
const marker = L.marker([p.lat, p.lon], {
icon: carIcon(name, COLORS[name])
}).addTo(map);
const line = L.polyline([[p.lat, p.lon]], {
color: COLORS[name],
weight: 3,
opacity: 0.75
}).addTo(map);
const initialHeading = bearing(FRAMES[0][name], FRAMES[1][name]);
marker.bindPopup(formatPopupHtml(name, p, initialHeading, 0));
marker.on("click", () => {
const s = currentState[name];
marker.setPopupContent(formatPopupHtml(name, s.point, s.headingDeg, s.frameIndex));
});
markers[name] = marker;
trails[name] = line;
currentState[name] = {
point: p,
headingDeg: initialHeading,
frameIndex: 0
};
setHeading(marker, initialHeading);
}
let frame = 0;
let start = performance.now();
let running = true;
let rafId = null;
function resetDemo() {
frame = 0;
start = performance.now();
for (const name of cars) {
const p = FRAMES[0][name];
const initialHeading = bearing(FRAMES[0][name], FRAMES[1][name]);
markers[name].setLatLng([p.lat, p.lon]);
setHeading(markers[name], initialHeading);
trails[name].setLatLngs([[p.lat, p.lon]]);
currentState[name] = {
point: p,
headingDeg: initialHeading,
frameIndex: 0
};
markers[name].setPopupContent(formatPopupHtml(name, p, initialHeading, 0));
}
}
function tick(now) {
if (!running) return;
const next = frame + 1;
if (next >= FRAMES.length) return;
let tRaw = (now - start) / STEP_MS;
if (tRaw > 1) tRaw = 1;
const t = smooth(tRaw);
for (const name of cars) {
const p0 = FRAMES[frame][name];
const p1 = FRAMES[next][name];
const lat = lerp(p0.lat, p1.lat, t);
const lon = lerp(p0.lon, p1.lon, t);
const hdg = bearing(p0, p1);
markers[name].setLatLng([lat, lon]);
setHeading(markers[name], hdg);
currentState[name] = {
point: {
lat,
lon,
speedMps: p1.speedMps
},
headingDeg: hdg,
frameIndex: frame
};
}
if (tRaw >= 1) {
frame++;
start = now;
for (const name of cars) {
const p = FRAMES[frame][name];
const line = trails[name];
line.addLatLng([p.lat, p.lon]);
const nextHeading =
frame < FRAMES.length - 1
? bearing(FRAMES[frame][name], FRAMES[frame + 1][name])
: currentState[name].headingDeg;
currentState[name] = {
point: p,
headingDeg: nextHeading,
frameIndex: frame
};
markers[name].setPopupContent(formatPopupHtml(name, p, nextHeading, frame));
}
}
rafId = requestAnimationFrame(tick);
}
document.getElementById("play").addEventListener("click", () => {
if (running) return;
running = true;
rafId = requestAnimationFrame(tick);
});
document.getElementById("pause").addEventListener("click", () => {
running = false;
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
});
document.getElementById("reset").addEventListener("click", () => {
running = false;
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
resetDemo();
running = true;
rafId = requestAnimationFrame(tick);
});
resetDemo();
rafId = requestAnimationFrame(tick);
</script>
</body>
</html>