diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-11 22:19:22 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-11 22:19:22 +0200 |
| commit | 991ab2f179b2d84e8b0ce6d0634b4033f20e4b86 (patch) | |
| tree | 225d8b5e8b46245ece9f2d8002b7db47727c3ee3 /web/templates.templ | |
| parent | 690ced7aea43c1187a66d9f7ef813bd6daa13195 (diff) | |
refactor(web): extract ride detail script
Diffstat (limited to 'web/templates.templ')
| -rw-r--r-- | web/templates.templ | 250 |
1 files changed, 2 insertions, 248 deletions
diff --git a/web/templates.templ b/web/templates.templ index 8e7c0f1..515c482 100644 --- a/web/templates.templ +++ b/web/templates.templ @@ -88,254 +88,8 @@ templ RideDetailContent(data RideDetailView) { </section> @templ.JSONScript("ride-route", data.Route) @templ.JSONScript("ride-profile", data.Profile) - <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> - <script> - (() => { - const mapElement = document.getElementById("ride-map"); - const routeElement = document.getElementById("ride-route"); - const profileElement = document.getElementById("ride-profile"); - const canvas = document.getElementById("ride-profile-chart"); - const hoverOutput = document.getElementById("ride-profile-hover"); - if (!mapElement || !routeElement || !profileElement || !canvas || !hoverOutput) return; - const colorProbe = document.createElement("span"); - colorProbe.hidden = true; - document.body.append(colorProbe); - const resolveColor = (name) => { - colorProbe.style.color = `var(${name})`; - return getComputedStyle(colorProbe).color; - }; - const colors = { - accent: resolveColor("--color-accent"), - forest: resolveColor("--color-forest"), - subtle: resolveColor("--color-subtle"), - plotSurface: resolveColor("--color-plot-surface"), - plotSurfaceOverlay: resolveColor("--color-plot-surface-overlay"), - grid: resolveColor("--color-plot-grid"), - accentFill: resolveColor("--color-accent-fill"), - climbLabel: resolveColor("--color-climb-label"), - crossing: resolveColor("--color-crossing"), - crossingLabel: resolveColor("--color-crossing-label"), - hoverLine: resolveColor("--color-hover-line"), - }; - colorProbe.remove(); - const route = JSON.parse(routeElement.textContent); - const profile = JSON.parse(profileElement.textContent); - const map = L.map(mapElement); - const tiles = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { - maxZoom: 19, - attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', - }); - tiles.addTo(map); - const routeLayer = L.geoJSON(route, { - style: { color: colors.accent, weight: 4, opacity: 0.9 }, - }).addTo(map); - const bounds = routeLayer.getBounds(); - if (bounds.isValid()) { - map.fitBounds(bounds, { padding: [24, 24], maxZoom: 15 }); - } - - const points = profile.points || []; - if (points.length === 0) return; - const routeCursor = L.circleMarker([points[0].latitude, points[0].longitude], { - color: colors.forest, - fillColor: colors.accent, - fillOpacity: 0, - opacity: 0, - radius: 7, - weight: 3, - interactive: false, - }).addTo(map); - - const context = canvas.getContext("2d"); - if (!context) return; - const state = { hoveredIndex: -1, plot: null, width: 0, height: 0 }; - const minDistance = points[0].distanceKm; - const maxDistance = points[points.length - 1].distanceKm; - let minElevation = points[0].elevationM; - let maxElevation = points[0].elevationM; - for (const point of points) { - minElevation = Math.min(minElevation, point.elevationM); - maxElevation = Math.max(maxElevation, point.elevationM); - } - const elevationPadding = Math.max((maxElevation - minElevation) * 0.1, 20); - minElevation -= elevationPadding; - maxElevation += elevationPadding; - - const formatDistance = (distance) => `${distance.toFixed(distance < 10 ? 1 : 0)} km`; - const formatElevation = (elevation) => `${Math.round(elevation)} m`; - const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); - const xForDistance = (distance) => { - const span = Math.max(maxDistance - minDistance, 1); - return state.plot.left + (distance - minDistance) / span * (state.plot.right - state.plot.left); - }; - const yForElevation = (elevation) => { - const span = Math.max(maxElevation - minElevation, 1); - return state.plot.bottom - (elevation - minElevation) / span * (state.plot.bottom - state.plot.top); - }; - const nearestPointIndex = (distance) => { - let low = 0; - let high = points.length - 1; - while (low < high) { - const middle = Math.floor((low + high) / 2); - if (points[middle].distanceKm < distance) low = middle + 1; - else high = middle; - } - if (low === 0) return low; - const previous = points[low - 1]; - return distance - previous.distanceKm < points[low].distanceKm - distance ? low - 1 : low; - }; - - const draw = () => { - const rect = canvas.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) return; - const ratio = window.devicePixelRatio || 1; - canvas.width = Math.floor(rect.width * ratio); - canvas.height = Math.floor(rect.height * ratio); - context.setTransform(ratio, 0, 0, ratio, 0, 0); - state.width = rect.width; - state.height = rect.height; - state.plot = { left: 52, right: rect.width - 20, top: 24, bottom: rect.height - 34 }; - const plot = state.plot; - context.clearRect(0, 0, rect.width, rect.height); - context.fillStyle = colors.plotSurface; - context.fillRect(0, 0, rect.width, rect.height); - - context.font = "12px system-ui, sans-serif"; - context.textBaseline = "middle"; - for (let step = 0; step <= 4; step++) { - const fraction = step / 4; - const y = plot.top + fraction * (plot.bottom - plot.top); - const elevation = maxElevation - fraction * (maxElevation - minElevation); - context.strokeStyle = colors.grid; - context.lineWidth = 1; - context.beginPath(); - context.moveTo(plot.left, y); - context.lineTo(plot.right, y); - context.stroke(); - context.fillStyle = colors.subtle; - context.textAlign = "right"; - context.fillText(formatElevation(elevation), plot.left - 8, y); - } - - for (const climb of profile.climbs || []) { - const startX = clamp(xForDistance(climb.startKm), plot.left, plot.right); - const endX = clamp(xForDistance(climb.endKm), plot.left, plot.right); - context.fillStyle = colors.accentFill; - context.fillRect(startX, plot.top, Math.max(endX - startX, 1), plot.bottom - plot.top); - const label = climb.name || `${climb.category} ${Math.round(climb.score)}`; - context.fillStyle = colors.climbLabel; - context.textAlign = "center"; - context.textBaseline = "top"; - context.fillText(label, clamp((startX + endX) / 2, plot.left + 24, plot.right - 24), plot.top + 4); - } - - for (const crossing of profile.crossings || []) { - const x = xForDistance(crossing.distanceKm); - context.strokeStyle = colors.crossing; - context.lineWidth = 1; - context.setLineDash([4, 3]); - context.beginPath(); - context.moveTo(x, plot.top); - context.lineTo(x, plot.bottom); - context.stroke(); - context.setLineDash([]); - } - - context.beginPath(); - context.moveTo(xForDistance(points[0].distanceKm), plot.bottom); - for (const point of points) context.lineTo(xForDistance(point.distanceKm), yForElevation(point.elevationM)); - context.lineTo(xForDistance(points[points.length - 1].distanceKm), plot.bottom); - context.closePath(); - context.fillStyle = colors.accentFill; - context.fill(); - context.beginPath(); - for (let i = 0; i < points.length; i++) { - const point = points[i]; - if (i === 0) context.moveTo(xForDistance(point.distanceKm), yForElevation(point.elevationM)); - else context.lineTo(xForDistance(point.distanceKm), yForElevation(point.elevationM)); - } - context.strokeStyle = colors.accent; - context.lineWidth = 2.5; - context.stroke(); - for (const crossing of profile.crossings || []) { - const x = xForDistance(crossing.distanceKm); - const label = `${crossing.name} ${Math.round(crossing.passElevationM)} m`; - const labelWidth = context.measureText(label).width; - const labelX = clamp(x, plot.left + labelWidth / 2 + 3, plot.right - labelWidth / 2 - 3); - const labelY = plot.top - 6; - context.fillStyle = colors.plotSurfaceOverlay; - context.fillRect(labelX - labelWidth / 2 - 3, labelY - 15, labelWidth + 6, 16); - context.fillStyle = colors.crossingLabel; - context.textAlign = "center"; - context.textBaseline = "bottom"; - context.fillText(label, labelX, labelY); - } - - context.fillStyle = colors.subtle; - context.textAlign = "center"; - context.textBaseline = "top"; - for (let step = 0; step <= 4; step++) { - const distance = minDistance + step / 4 * (maxDistance - minDistance); - context.fillText(formatDistance(distance), xForDistance(distance), plot.bottom + 10); - } - - if (state.hoveredIndex >= 0) { - const point = points[state.hoveredIndex]; - const x = xForDistance(point.distanceKm); - const y = yForElevation(point.elevationM); - context.strokeStyle = colors.hoverLine; - 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.forest; - context.beginPath(); - context.arc(x, y, 5, 0, 2 * Math.PI); - context.fill(); - } - }; - - const clearHover = () => { - state.hoveredIndex = -1; - routeCursor.setStyle({ opacity: 0, fillOpacity: 0 }); - hoverOutput.textContent = "Hover or focus the profile to inspect elevation."; - draw(); - }; - const showPoint = (index) => { - state.hoveredIndex = clamp(index, 0, points.length - 1); - const point = points[state.hoveredIndex]; - routeCursor.setLatLng([point.latitude, point.longitude]); - routeCursor.setStyle({ opacity: 1, fillOpacity: 0.9 }); - hoverOutput.textContent = `${formatDistance(point.distanceKm)} ยท ${formatElevation(point.elevationM)}`; - draw(); - }; - canvas.addEventListener("pointermove", (event) => { - if (!state.plot) return; - const rect = canvas.getBoundingClientRect(); - const x = event.clientX - rect.left; - if (x < state.plot.left || x > state.plot.right) { - clearHover(); - return; - } - const distance = minDistance + (x - state.plot.left) / (state.plot.right - state.plot.left) * (maxDistance - minDistance); - showPoint(nearestPointIndex(distance)); - }); - canvas.addEventListener("pointerleave", clearHover); - canvas.addEventListener("pointercancel", clearHover); - canvas.addEventListener("keydown", (event) => { - if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; - event.preventDefault(); - const direction = event.key === "ArrowRight" ? 1 : -1; - const index = state.hoveredIndex < 0 ? (direction > 0 ? 0 : points.length - 1) : state.hoveredIndex + direction; - showPoint(index); - }); - window.addEventListener("resize", draw); - draw(); - })(); - </script> + <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" defer></script> + <script src="/static/ride-detail.js" defer></script> } } |