From b626f15b6e40311143cdec91728046dc79e0d328 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Mon, 17 Aug 2026 13:05:50 +0200 Subject: feat(web): display nearby passes on ride maps --- web/frontend/ride-detail-map.ts | 18 ++++++++++++++++-- web/frontend/ride-detail.ts | 8 ++++++-- web/frontend/types.d.ts | 7 +++++++ 3 files changed, 29 insertions(+), 4 deletions(-) (limited to 'web/frontend') diff --git a/web/frontend/ride-detail-map.ts b/web/frontend/ride-detail-map.ts index dcc2b60..81367f3 100644 --- a/web/frontend/ride-detail-map.ts +++ b/web/frontend/ride-detail-map.ts @@ -1,5 +1,5 @@ import { clamp } from "./ride-detail-logic.js"; -import type { ClimbBounds, RideMapColors, RideProfilePoint, RideRoute } from "./types.js"; +import type { ClimbBounds, RideMapColors, RideMapPass, RideProfilePoint, RideRoute } from "./types.js"; import type { CircleMarker, LeafletMouseEvent, Map as LeafletMap, Polyline } from "leaflet"; type LeafletApi = typeof import("leaflet"); @@ -10,6 +10,7 @@ interface RideDetailMapOptions { route: RideRoute; points: RideProfilePoint[]; climbs: ClimbBounds[]; + passes: RideMapPass[]; colors: RideMapColors; } @@ -21,7 +22,7 @@ export class RideDetailMap { private readonly climbLayers: (Polyline | null)[]; private readonly routeCursor: CircleMarker; - constructor({ leaflet, element, route, points, climbs, colors }: RideDetailMapOptions) { + constructor({ leaflet, element, route, points, climbs, passes, colors }: RideDetailMapOptions) { this.leaflet = leaflet; this.points = points; this.colors = colors; @@ -37,6 +38,19 @@ export class RideDetailMap { }) .addTo(this.map); const bounds = routeLayer.getBounds(); + const passIcon = leaflet.divIcon({ + className: "ride-map-pass-icon", + html: '', + iconSize: [28, 28], + iconAnchor: [14, 14], + }); + for (const pass of passes) { + const marker = leaflet.marker([pass.latitude, pass.longitude], { icon: passIcon, alt: pass.name || "Mountain pass" }).addTo(this.map); + const tooltip = document.createElement("span"); + tooltip.textContent = `${pass.name || "Mountain pass"} | ${Math.round(pass.elevationM)} m`; + marker.bindTooltip(tooltip); + bounds.extend(marker.getLatLng()); + } if (bounds.isValid()) this.map.fitBounds(bounds, { padding: [24, 24], maxZoom: 15 }); this.climbLayers = climbs.map((climb) => this.createClimbLayer(climb)); this.routeCursor = leaflet diff --git a/web/frontend/ride-detail.ts b/web/frontend/ride-detail.ts index 7394d5a..045372e 100644 --- a/web/frontend/ride-detail.ts +++ b/web/frontend/ride-detail.ts @@ -3,17 +3,19 @@ import { RideBoundaryController } from "./ride-detail-boundaries.js"; import { RideProfileCanvas } from "./ride-detail-canvas.js"; import { RideDetailMap } from "./ride-detail-map.js"; import { formatDistance, formatElevation } from "./ride-detail-logic.js"; -import type { ClimbBounds, RideMapColors, RideProfile, RideProfileColors, RideProfilePoint, RideRoute } from "./types.js"; +import type { ClimbBounds, RideMapColors, RideMapPass, RideProfile, RideProfileColors, RideProfilePoint, RideRoute } from "./types.js"; export const mountRideDetail = (): void => { const mapElement = document.getElementById("ride-map"); const routeElement = document.getElementById("ride-route"); const profileElement = document.getElementById("ride-profile"); + const passesElement = document.getElementById("ride-passes"); const canvas = document.getElementById("ride-profile-chart"); const hoverOutput = document.getElementById("ride-profile-hover"); - if (!mapElement || !routeElement || !profileElement || !canvas || !hoverOutput) return; + if (!mapElement || !routeElement || !profileElement || !passesElement || !canvas || !hoverOutput) return; const routeScript = routeElement as HTMLScriptElement; const profileScript = profileElement as HTMLScriptElement; + const passesScript = passesElement as HTMLScriptElement; const profileCanvasElement = canvas as HTMLCanvasElement; const leaflet = window.L; if (!leaflet) return; @@ -49,6 +51,7 @@ export const mountRideDetail = (): void => { const route = JSON.parse(routeScript.textContent ?? "") as RideRoute; const profile = JSON.parse(profileScript.textContent ?? "") as RideProfile; + const passes = JSON.parse(passesScript.textContent ?? "") as RideMapPass[]; const points: RideProfilePoint[] = profile.points; if (points.length === 0) return; @@ -69,6 +72,7 @@ export const mountRideDetail = (): void => { route, points, climbs: climbBounds, + passes, colors: mapColors, }); const officialProfileController = new OfficialClimbProfileController(points); diff --git a/web/frontend/types.d.ts b/web/frontend/types.d.ts index 0b59dac..07c8204 100644 --- a/web/frontend/types.d.ts +++ b/web/frontend/types.d.ts @@ -57,6 +57,13 @@ export interface RideRouteFeature { properties?: Record; } +export interface RideMapPass { + name: string; + elevationM: number; + latitude: number; + longitude: number; +} + export interface ClimbBounds { startIndex: number; endIndex: number; -- cgit v1.2.3