From 2d18d177f48285eb123e7da8680fc00e7a6fe19c Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Sun, 16 Aug 2026 13:25:21 +0200 Subject: refactor(web): split ride detail JavaScript --- web/static/ride-detail-map.js | 106 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 web/static/ride-detail-map.js (limited to 'web/static/ride-detail-map.js') diff --git a/web/static/ride-detail-map.js b/web/static/ride-detail-map.js new file mode 100644 index 0000000..e8e7240 --- /dev/null +++ b/web/static/ride-detail-map.js @@ -0,0 +1,106 @@ +import { clamp } from "./ride-detail-logic.js"; + +export class RideDetailMap { + constructor({ leaflet, element, route, points, climbs, colors }) { + this.leaflet = leaflet; + this.points = points; + this.colors = colors; + this.map = leaflet.map(element); + const tiles = leaflet.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19, + attribution: '© OpenStreetMap contributors', + }); + tiles.addTo(this.map); + const routeLayer = leaflet + .geoJSON(route, { + style: { color: colors.accent, weight: 4, opacity: 0.9 }, + }) + .addTo(this.map); + const bounds = routeLayer.getBounds(); + if (bounds.isValid()) this.map.fitBounds(bounds, { padding: [24, 24], maxZoom: 15 }); + this.climbLayers = climbs.map((climb) => this.createClimbLayer(climb)); + this.routeCursor = leaflet + .circleMarker([points[0].latitude, points[0].longitude], { + color: colors.forest, + fillColor: colors.accent, + fillOpacity: 0, + opacity: 0, + radius: 7, + weight: 3, + interactive: false, + }) + .addTo(this.map); + } + + createClimbLayer(climb) { + if (!this.isValidBounds(climb)) return null; + const coordinates = this.points.slice(climb.startIndex, climb.endIndex + 1).map((point) => [point.latitude, point.longitude]); + return this.leaflet + .polyline(coordinates, { + color: this.colors.climbRoute, + weight: 7, + opacity: 0.65, + lineCap: "round", + lineJoin: "round", + interactive: false, + }) + .addTo(this.map); + } + + isValidBounds(bounds) { + return ( + bounds && + Number.isInteger(bounds.startIndex) && + Number.isInteger(bounds.endIndex) && + bounds.startIndex >= 0 && + bounds.endIndex < this.points.length && + bounds.startIndex < bounds.endIndex + ); + } + + updateClimbLayer(index, bounds, active) { + const layer = this.climbLayers[index]; + if (!layer) return; + if (!this.isValidBounds(bounds)) { + layer.setLatLngs([]); + return; + } + layer.setLatLngs(this.points.slice(bounds.startIndex, bounds.endIndex + 1).map((point) => [point.latitude, point.longitude])); + layer.setStyle({ weight: active ? 9 : 7, opacity: active ? 1 : 0.65 }); + } + + nearestPointIndex(latitude, longitude) { + let nearestIndex = 0; + let nearestDistance = Infinity; + for (let index = 0; index < this.points.length; index++) { + const point = this.points[index]; + const distance = this.leaflet.latLng(point.latitude, point.longitude).distanceTo([latitude, longitude]); + if (distance < nearestDistance) { + nearestIndex = index; + nearestDistance = distance; + } + } + return nearestIndex; + } + + showPoint(index) { + const point = this.points[clamp(index, 0, this.points.length - 1)]; + this.routeCursor.setLatLng([point.latitude, point.longitude]); + this.routeCursor.setStyle({ opacity: 1, fillOpacity: 0.9 }); + } + + clearPoint() { + this.routeCursor.setStyle({ opacity: 0, fillOpacity: 0 }); + } + + zoomToClimb(bounds) { + if (!this.isValidBounds(bounds)) return; + const climbPoints = this.points.slice(bounds.startIndex, bounds.endIndex + 1); + const mapBounds = this.leaflet.latLngBounds(climbPoints.map((point) => [point.latitude, point.longitude])); + if (mapBounds.isValid()) this.map.fitBounds(mapBounds, { padding: [32, 32], maxZoom: 15 }); + } + + onClick(listener) { + this.map.on("click", listener); + } +} -- cgit v1.2.3