"use strict";
const shareButton = document.getElementById("copy-run-link");
if (shareButton) shareButton.addEventListener("click", async () => {
const status = document.getElementById("share-status");
const fallback = document.getElementById("share-fallback");
const input = document.getElementById("share-link-value");
shareButton.disabled = true;
fallback.hidden = true;
status.textContent = "Creating share link…";
try {
const response = await fetch(shareButton.dataset.url, {
method: "POST", headers: {"X-CSRFToken": shareButton.dataset.csrf},
});
if (!response.ok || response.redirected) throw new Error("Share unavailable");
const data = await response.json();
input.value = data.url;
document.getElementById("revoke-share").hidden = false;
try {
await navigator.clipboard.writeText(data.url);
status.textContent = "Link copied! Anyone with the link can view this run.";
} catch {
// Clipboard permissions and non-HTTPS hosts can prevent automatic copying.
fallback.hidden = false;
input.focus();
input.select();
status.textContent = "Link ready. Copy it below to share this run without sign-in.";
}
} catch {
status.textContent = "Couldn’t create a share link. Please try again.";
} finally {
shareButton.disabled = false;
}
});
const valid = v => typeof v === "number" && Number.isFinite(v);
const paceLabel = v => {const s = Math.round(v); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;};
async function showActivity() {
const container = document.getElementById("route-map"), status = document.getElementById("map-status");
try {
const response = await fetch(container.dataset.url);
if (!response.ok) throw new Error("Activity unavailable");
const data = await response.json();
let locate = () => {};
if (data.segments.length && window.L) {
container.replaceChildren();
const map = L.map(container, {scrollWheelZoom: false, preferCanvas: true});
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {maxZoom: 19, attribution: '© OpenStreetMap'}).addTo(map);
const outline = L.polyline(data.segments, {color: "#263d32", weight: 8, opacity: .35}).addTo(map);
const fit = () => map.fitBounds(outline.getBounds(), {padding: [40, 40], maxZoom: 16});
fit();
document.getElementById("fit-route").onclick = fit;
const colours = ["#356ac3", "#319dc1", "#37b5a0", "#94bf6e", "#e2c954", "#eea044", "#de6744", "#b83d50"];
const heat = L.layerGroup().addTo(map), arrows = L.layerGroup().addTo(map);
const samples = data.route_segments.flat();
const format = (v, key) => !valid(v) ? "Unavailable" : key === "speed" ? `${(v * 3.6).toFixed(1)} km/h` : key === "grade" ? `${v.toFixed(1)}%` : `${Math.round(v)} bpm`;
const colourRoute = () => {
heat.clearLayers();
const key = document.getElementById("map-metric").value;
const values = samples.map(s => s[key]).filter(valid).sort((a,b) => a-b);
const low = values[Math.floor((values.length-1)*.1)], high = values[Math.floor((values.length-1)*.9)];
document.getElementById("legend-low").textContent = format(low, key);
document.getElementById("legend-high").textContent = format(high, key);
document.getElementById("legend-ramp").style.background = `linear-gradient(to right, ${colours.join(",")})`;
for (const segment of data.route_segments) {
for (let i = 1; i < segment.length; i++) {
const a = segment[i-1], b = segment[i], value = b[key];
const ratio = high > low ? (value-low)/(high-low) : .5;
const colour = valid(value) ? colours[Math.max(0,Math.min(7,Math.floor(ratio*8)))] : "#969c99";
L.polyline([a.point,b.point], {color: colour, weight: 5, opacity: 1}).bindTooltip(format(value,key)).addTo(heat);
}
}
};
colourRoute();
document.getElementById("map-metric").onchange = colourRoute;
const spacing = Math.max(1, Math.floor(samples.length / 16));
let passed = 0;
for (const segment of data.route_segments) {
for (let i = 1; i < segment.length; i++, passed++) {
if (passed % spacing) continue;
const a = map.project(segment[i-1].point, 14), b = map.project(segment[i].point, 14);
if (a.distanceTo(b) < .01) continue;
const angle = Math.atan2(b.y-a.y,b.x-a.x)*180/Math.PI;
L.marker(segment[i].point, {interactive:false, icon:L.divIcon({className:"route-arrow", html:`➤`, iconSize:[20,20], iconAnchor:[10,10]})}).addTo(arrows);
}
}
document.getElementById("show-directions").onchange = e => e.target.checked ? arrows.addTo(map) : map.removeLayer(arrows);
for (const [row, label, name] of [[samples[0],"S","Start"],[samples.at(-1),"F","Finish"]]) {
L.marker(row.point, {icon:L.divIcon({className:`route-end route-${label}`, html:label, iconSize:[25,25], iconAnchor:label === "S" ? [27,13] : [-2,13]})}).bindTooltip(name).addTo(map);
}
const cursor = L.circleMarker(samples[0].point,{radius:7,color:"#fff",weight:3,fillColor:"#263d32",fillOpacity:1,interactive:false});
locate = distance => {
let closest = null, delta = Infinity;
for (const sample of samples) if (valid(sample.distance) && Math.abs(sample.distance-distance) ({label,key,borderColor:colour,unit,yAxisID:axis});
const chart = (id, series, reverse=false) => {
const canvas = document.getElementById(id);
if (!series.some(s=>data.chart.some(row=>valid(row[s.key])))) {
const message=document.createElement("p"); message.className="chart-empty";message.textContent="Not recorded by your watch for this activity.";canvas.replaceWith(message);return;
}
const datasets = series.map(s=>({...s,data:data.chart.filter(r=>valid(r.distance)).map(r=>({x:r.distance/1000,y:valid(r[s.key])?r[s.key]:null})),borderWidth:1.6,pointRadius:0,pointHitRadius:8,tension:.15,spanGaps:false}));
const axis = right => ({position:right?"right":"left",reverse:reverse&&!right,grid:{color:"#30383b",drawOnChartArea:!right},border:{display:false},ticks:{color:"#a0aaac",font:{size:10},callback:v=>reverse&&!right?paceLabel(v):Number(v.toFixed(2))}});
new Chart(canvas,{type:"line",data:{datasets},options:{responsive:true,maintainAspectRatio:false,animation:false,parsing:false,interaction:{mode:"index",intersect:false},onHover:(_event,items,instance)=>{if(items.length)locate(instance.data.datasets[items[0].datasetIndex].data[items[0].index].x*1000);},plugins:{legend:{position:"bottom",labels:{usePointStyle:true,boxWidth:7,font:{size:11}}},tooltip:{callbacks:{title:items=>`${items[0].parsed.x.toFixed(2)} km`,label:item=>`${item.dataset.label}: ${item.dataset.unit==="/km"?paceLabel(item.parsed.y):item.parsed.y.toFixed(1)} ${item.dataset.unit}`}}},scales:{x:{type:"linear",grid:{display:false},border:{display:false},ticks:{maxTicksLimit:7,color:"#a0aaac",font:{size:10},callback:v=>`${v} km`}},y:axis(false),...(series.some(s=>s.yAxisID==="y2")?{y2:axis(true)}:{})}}});
};
chart("pace-chart",[line("Pace","pace","#ff9268","/km"),line("GAP estimate","gap","#bc9050","/km")],true);
chart("hr-chart",[line("Heart rate","heart_rate","#bd685b","bpm")]);
chart("elevation-chart",[line("Elevation","elevation","#a6c896","m"),line("Gradient","grade","#ba965a","%","y2")]);
chart("cadence-chart",[line("Cadence","cadence","#5a83ab","spm"),line("Step length","stride_length","#9b83b3","m","y2")]);
chart("power-chart",[line("Power","power","#b48a38","W")]);
chart("dynamics-chart",[line("Vertical oscillation","vertical_oscillation","#699f96","cm"),line("Ground contact","ground_contact","#a57b9c","ms","y2")]);
} catch (error) {
status.textContent = "We couldn’t finish loading this activity. Please refresh and try again.";
console.error("Activity display failed", error);
}
}
showActivity();