summaryrefslogtreecommitdiff
path: root/web/static/ride-detail-map.js
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-16 13:25:21 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-16 13:25:21 +0200
commit2d18d177f48285eb123e7da8680fc00e7a6fe19c (patch)
tree2b339bcec21c244242f6e26d4444feebdf3b73d0 /web/static/ride-detail-map.js
parentbe78280c669a04c112afaf1801f22ed2eec7c55a (diff)
refactor(web): split ride detail JavaScript
Diffstat (limited to 'web/static/ride-detail-map.js')
-rw-r--r--web/static/ride-detail-map.js106
1 files changed, 106 insertions, 0 deletions
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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);
+ }
+}