Files

75 lines
4.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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, preferCanvas: true});
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19, attribution: '&copy; <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;
let low = 0, high = edges.length - 1;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (edges[mid].end < distance) low = mid + 1; else high = mid;
}
const edge = edges[low];
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();