70 lines
4.2 KiB
JavaScript
70 lines
4.2 KiB
JavaScript
"use strict";
|
||
const volume = document.getElementById("volume-data");
|
||
if (volume && window.Chart) {
|
||
const data = JSON.parse(volume.textContent);
|
||
new Chart(document.getElementById("volume-chart"), {
|
||
type: "bar",
|
||
data: {labels: data.labels, datasets: [{data: data.values, backgroundColor: data.values.map((_, i) => i === 7 ? "#ff9268" : "#586367"), hoverBackgroundColor: "#ffb08b", borderRadius: 2, maxBarThickness: 34}]},
|
||
options: {animation: !window.matchMedia("(prefers-reduced-motion: reduce)").matches, responsive: true, maintainAspectRatio: false, plugins: {legend: {display: false}, tooltip: {callbacks: {label: context => `${context.raw.toFixed(2)} km`}}}, scales: {x: {grid: {display: false}, border: {display: false}, ticks: {font: {size: 10}, color: "#a0aaac"}}, y: {beginAtZero: true, border: {display: false}, grid: {color: "#30383b"}, ticks: {font: {size: 10}, color: "#a0aaac", callback: v => `${v} km`}}}}
|
||
});
|
||
}
|
||
|
||
async function showDashboardRoute() {
|
||
const container = document.getElementById("dashboard-route-map");
|
||
if (!container?.dataset.url) return;
|
||
const status = document.getElementById("dashboard-route-status");
|
||
try {
|
||
const response = await fetch(container.dataset.url);
|
||
if (!response.ok) throw new Error("Route unavailable");
|
||
const data = await response.json();
|
||
const segments = data.segments.filter(segment => segment.length);
|
||
if (!segments.length) { status.textContent = "No GPS route recorded for this run."; return; }
|
||
const map = L.map(container, {scrollWheelZoom: false});
|
||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||
maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||
}).addTo(map);
|
||
const route = L.polyline(segments, {color: "#ff9268", weight: 4, opacity: .85}).addTo(map);
|
||
const fit = () => map.fitBounds(route.getBounds(), {padding: [28, 28], maxZoom: 16});
|
||
fit();
|
||
new ResizeObserver(() => { map.invalidateSize(); fit(); }).observe(container);
|
||
const first = segments[0][0], last = segments.at(-1).at(-1);
|
||
L.circleMarker(first, {radius: 6, color: "#fff", fillColor: "#70b992", fillOpacity: 1}).bindTooltip("Start").addTo(map);
|
||
L.circleMarker(last, {radius: 6, color: "#fff", fillColor: "#ff9268", fillOpacity: 1}).bindTooltip("Finish").addTo(map);
|
||
const runner = L.marker(first, {interactive: false, icon: L.divIcon({
|
||
className: "dashboard-runner", html: '<span></span>', iconSize: [20,20], iconAnchor: [10,10],
|
||
})}).addTo(map);
|
||
// Measure each recorded segment separately so GPS gaps are never drawn across.
|
||
let length = 0;
|
||
const edges = [];
|
||
for (const segment of segments) for (let i = 1; i < segment.length; i++) {
|
||
const a = L.latLng(segment[i-1]), b = L.latLng(segment[i]), distance = a.distanceTo(b);
|
||
if (distance > 0) { edges.push({a, b, start: length, end: length + distance}); length += distance; }
|
||
}
|
||
if (!length) { status.textContent = "Recorded GPS location. No movement to replay."; return; }
|
||
const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||
let paused = preference.matches, elapsed = 0, previous = null, frame = null;
|
||
const tick = now => {
|
||
if (paused || document.hidden) { previous = null; frame = null; return; }
|
||
if (previous !== null) elapsed += now - previous;
|
||
previous = now;
|
||
const distance = ((elapsed % 20000) / 20000) * length;
|
||
const edge = edges.find(edge => edge.end >= distance) || edges.at(-1);
|
||
const fraction = (distance - edge.start) / (edge.end - edge.start);
|
||
runner.setLatLng([edge.a.lat + (edge.b.lat-edge.a.lat)*fraction, edge.a.lng + (edge.b.lng-edge.a.lng)*fraction]);
|
||
frame = requestAnimationFrame(tick);
|
||
};
|
||
const update = () => {
|
||
if (frame !== null) cancelAnimationFrame(frame);
|
||
frame = null; previous = null;
|
||
status.hidden = true;
|
||
if (!paused && !document.hidden) frame = requestAnimationFrame(tick);
|
||
};
|
||
preference.addEventListener("change", event => { paused = event.matches; update(); });
|
||
document.addEventListener("visibilitychange", update);
|
||
update();
|
||
} catch {
|
||
status.textContent = "Couldn’t load the route. Refresh to try again.";
|
||
}
|
||
}
|
||
showDashboardRoute();
|