diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-17 13:05:50 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-17 13:05:50 +0200 |
| commit | b626f15b6e40311143cdec91728046dc79e0d328 (patch) | |
| tree | 9c7ff4f9a879785d5a63b3410e0aa02be1a5219d /web/frontend | |
| parent | 151853c458348a4afccd7677deba6366ee4181eb (diff) | |
feat(web): display nearby passes on ride maps
Diffstat (limited to 'web/frontend')
| -rw-r--r-- | web/frontend/ride-detail-map.ts | 18 | ||||
| -rw-r--r-- | web/frontend/ride-detail.ts | 8 | ||||
| -rw-r--r-- | web/frontend/types.d.ts | 7 |
3 files changed, 29 insertions, 4 deletions
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: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 19 10.5 6l3.25 5.5L16 8l5 11H3Z" fill="currentColor"/></svg>', + 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<string, unknown>; } +export interface RideMapPass { + name: string; + elevationM: number; + latitude: number; + longitude: number; +} + export interface ClimbBounds { startIndex: number; endIndex: number; |