import { climbMetrics, climbSample, formatDistance, formatElevation } from "./ride-detail-logic.js"; import { requireElement } from "./dom.js"; import type { BoundarySource, BoundaryTarget, ClimbBounds, RideProfilePoint } from "./types.js"; interface ActiveBoundary { form: HTMLFormElement; target: BoundaryTarget; source: BoundarySource; } interface RideBoundaryControllerOptions { forms: HTMLFormElement[]; points: RideProfilePoint[]; climbBounds: ClimbBounds[]; onBoundaryChanged: (index: number) => void; onPointSelected: (index: number) => void; setMessage: (message: string) => void; } export class RideBoundaryController { private readonly forms: HTMLFormElement[]; private readonly points: RideProfilePoint[]; private readonly climbBounds: ClimbBounds[]; private readonly onBoundaryChanged: (index: number) => void; private readonly onPointSelected: (index: number) => void; private readonly setMessage: (message: string) => void; private activeBoundary: ActiveBoundary | null; constructor({ forms, points, climbBounds, onBoundaryChanged, onPointSelected, setMessage }: RideBoundaryControllerOptions) { this.forms = forms; this.points = points; this.climbBounds = climbBounds; this.onBoundaryChanged = onBoundaryChanged; this.onPointSelected = onPointSelected; this.setMessage = setMessage; this.activeBoundary = null; this.initialize(); } initialize(): void { for (const form of this.forms) { const item = form.closest("[data-climb-item]"); if (!item) continue; const climbIndex = Number.parseInt(item.dataset.climbIndex ?? "", 10); this.climbBounds[climbIndex] = { startIndex: Number.parseInt( requireElement(form.querySelector('[data-boundary-input="start"]'), "start boundary input").value, 10, ), endIndex: Number.parseInt( requireElement(form.querySelector('[data-boundary-input="end"]'), "end boundary input").value, 10, ), }; this.updatePreview(form); const buttons = [...form.querySelectorAll("[data-boundary-button]")]; for (const button of buttons) { button.addEventListener("click", () => { this.clearSelection(); this.activeBoundary = { form, target: button.dataset.boundaryButton as BoundaryTarget, source: button.dataset.boundarySource as BoundarySource, }; button.classList.add("active"); const surface = this.activeBoundary.source === "map" ? "map" : "profile"; this.setMessage(`Click the ${surface} to select the ${this.activeBoundary.target} point.`); }); } } } isSelecting(source: BoundarySource): boolean { return this.activeBoundary?.source === source; } clearSelection(): void { this.activeBoundary = null; for (const form of this.forms) { for (const button of form.querySelectorAll("[data-boundary-button]")) button.classList.remove("active"); } } choosePoint(index: number): void { if (!this.activeBoundary) return; const { form, target } = this.activeBoundary; const item = form.closest("[data-climb-item]"); if (!item) return; const climbIndex = Number.parseInt(item.dataset.climbIndex ?? "", 10); const input = requireElement(form.querySelector(`[data-boundary-input="${target}"]`), `${target} boundary input`); const output = requireElement(form.querySelector(`[data-boundary-output="${target}"]`), `${target} boundary output`); input.value = String(index); output.textContent = this.boundaryLabel(index); if (target === "start") this.climbBounds[climbIndex].startIndex = index; else this.climbBounds[climbIndex].endIndex = index; this.updatePreview(form); this.onBoundaryChanged(climbIndex); this.clearSelection(); this.onPointSelected(index); } updatePreview(form: HTMLFormElement): void { const preview = requireElement(form.querySelector("[data-boundary-preview]"), "boundary preview"); const item = form.closest("[data-climb-item]"); if (!item) return; const climbIndex = Number.parseInt(item.dataset.climbIndex ?? "", 10); const bounds = this.climbBounds[climbIndex]; const metrics = climbMetrics(this.points, bounds); if (!bounds || !metrics) { preview.textContent = "Choose an end point after the start point."; return; } this.syncSnapshot(form, bounds); const summary = requireElement(item.querySelector("[data-climb-summary]"), "climb summary"); const metricsOutput = requireElement(item.querySelector("[data-climb-metrics]"), "climb metrics"); summary.textContent = `${formatDistance(metrics.start.distanceKm)}–${formatDistance(metrics.end.distanceKm)}`; metricsOutput.textContent = `${metrics.category} · ${formatDistance(metrics.distanceKm)} at ${metrics.slope.toFixed(1)}% · Cotacol ${metrics.cotacol.toFixed(1)}`; preview.textContent = `Preview: ${formatDistance(metrics.distanceKm)} · ${metrics.elevationGain >= 0 ? "+" : ""}${formatElevation(metrics.elevationGain)} · ${metrics.slope.toFixed(1)}% · Cotacol ${metrics.cotacol.toFixed(1)}`; } // syncSnapshot fills the fields the preview save posts: the preview ride is never persisted. syncSnapshot(form: HTMLFormElement, bounds: ClimbBounds): void { const sample = climbSample(this.points, bounds); if (!sample) return; const snapshot: Array<[string, number]> = [ ["start_lat", this.points[bounds.startIndex].latitude], ["start_lon", this.points[bounds.startIndex].longitude], ["end_lat", this.points[bounds.endIndex].latitude], ["end_lon", this.points[bounds.endIndex].longitude], ["sample_distance_m", sample.distanceM], ["sample_elevation_gain_m", sample.elevationGainM], ]; for (const [name, value] of snapshot) { const input = form.querySelector(`input[name="${name}"]`); if (input) input.value = String(value); } } boundaryLabel(index: number): string { const point = this.points[index]; const idx = point.index ?? index; return `#${idx} · ${formatDistance(point.distanceKm)} · ${formatElevation(point.elevationM)}`; } }