import { drawOfficialSections, officialProfilePlot, resolveOfficialProfileColors } from "./official-climb-profile.js"; import { displayStepForLength, officialProfileSections } from "./official-climb-profile-logic.js"; import type { OfficialProfileSection } from "./official-climb-profile-logic.js"; import type { ProfilePoint } from "./types.js"; interface ClimbProfilePayload { distanceKm: number; elevations: number[]; } interface ClimbProfileState { canvas: HTMLCanvasElement; payload: ClimbProfilePayload; sections: OfficialProfileSection[]; hoverOutput: HTMLOutputElement; } const climbProfilePoints = (payload: ClimbProfilePayload): ProfilePoint[] => payload.elevations.map((elevationM, index) => ({ distanceKm: (payload.distanceKm * index) / (payload.elevations.length - 1), elevationM, })); const climbProfileSections = (payload: ClimbProfilePayload): OfficialProfileSection[] => { const points = climbProfilePoints(payload); if (points.length < 2) return []; return officialProfileSections(points, 0, points.length - 1, displayStepForLength(payload.distanceKm * 1000)); }; const elevationAtDistance = (sections: OfficialProfileSection[], distanceKm: number): number | null => { for (const section of sections) { if (distanceKm >= section.startDistanceKm && distanceKm <= section.endDistanceKm) { const span = section.endDistanceKm - section.startDistanceKm; const fraction = span > 0 ? (distanceKm - section.startDistanceKm) / span : 0; return section.startElevation + fraction * (section.endElevation - section.startElevation); } } return null; }; const drawHoverMarker = (state: ClimbProfileState, distanceKm: number): void => { const colors = resolveOfficialProfileColors(); const rect = state.canvas.getBoundingClientRect(); const context = state.canvas.getContext("2d"); if (!context) return; const plot = officialProfilePlot(state.sections, rect.width, rect.height); const elevationSpan = Math.max(plot.maxElevation - plot.minElevation, 1); const x = plot.left + ((distanceKm - plot.minDistanceKm) / plot.distanceSpan) * (plot.right - plot.left); const elevation = elevationAtDistance(state.sections, distanceKm); if (elevation === null) return; const y = plot.bottom - ((elevation - plot.minElevation) / elevationSpan) * (plot.bottom - plot.top); context.strokeStyle = colors.subtle; context.lineWidth = 1; context.setLineDash([3, 3]); context.beginPath(); context.moveTo(x, plot.top); context.lineTo(x, plot.bottom); context.stroke(); context.setLineDash([]); context.fillStyle = colors.accent; context.beginPath(); context.arc(x, y, 5, 0, 2 * Math.PI); context.fill(); state.hoverOutput.textContent = `${distanceKm.toFixed(2)} km ยท ${Math.round(elevation)} m`; }; const clearHover = (state: ClimbProfileState): void => { drawClimbProfile(state.canvas, state.payload, state.sections); state.hoverOutput.textContent = ""; }; const drawClimbProfile = (canvas: HTMLCanvasElement, payload: ClimbProfilePayload, sections?: OfficialProfileSection[]): void => { const profileSections = sections ?? climbProfileSections(payload); if (profileSections.length === 0) return; drawOfficialSections(canvas, profileSections, resolveOfficialProfileColors()); }; const bindHover = (state: ClimbProfileState): void => { state.canvas.addEventListener("pointermove", (event: PointerEvent) => { const rect = state.canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const plot = officialProfilePlot(state.sections, rect.width, rect.height); if (x < plot.left || x > plot.right) { clearHover(state); return; } const distanceKm = plot.minDistanceKm + ((x - plot.left) / (plot.right - plot.left)) * plot.distanceSpan; drawClimbProfile(state.canvas, state.payload, state.sections); drawHoverMarker(state, distanceKm); }); state.canvas.addEventListener("pointerleave", () => clearHover(state)); state.canvas.addEventListener("pointercancel", () => clearHover(state)); }; const init = (): void => { const payloadElement = document.getElementById("climb-profile-data"); const canvas = document.querySelector("[data-climb-profile]"); if (!payloadElement || !canvas) return; const payload = JSON.parse(payloadElement.textContent ?? "null") as ClimbProfilePayload | null; if (!payload || payload.elevations.length < 2) return; const sections = climbProfileSections(payload); if (sections.length === 0) return; const hoverOutput = document.querySelector("[data-climb-profile-hover]"); if (!hoverOutput) return; const state: ClimbProfileState = { canvas, payload, sections, hoverOutput }; drawClimbProfile(canvas, payload, sections); bindHover(state); }; init();