From 7005b3677fda2b329443fa8795d108bc6d79476d Mon Sep 17 00:00:00 2001 From: Christian Beutel <> Date: Sun, 12 Jan 2025 15:29:37 +0100 Subject: [PATCH] move crosshair on trail hover --- .../trail/map_with_elevation_maplibre.svelte | 47 ++++++++++++------- .../elevationprofile-control.ts | 26 +++++++++- .../elevationprofile.ts | 47 +++++++++++++++---- 3 files changed, 92 insertions(+), 28 deletions(-) diff --git a/web/src/lib/components/trail/map_with_elevation_maplibre.svelte b/web/src/lib/components/trail/map_with_elevation_maplibre.svelte index 430265a8..9d2865f4 100644 --- a/web/src/lib/components/trail/map_with_elevation_maplibre.svelte +++ b/web/src/lib/components/trail/map_with_elevation_maplibre.svelte @@ -97,7 +97,7 @@ !drawing && fitBounds !== "off" && data.some((d) => d.bbox !== undefined) - ) { + ) { focusTrail(trails[activeTrail]); } } else if (activeTrail === null && trails.length) { @@ -179,13 +179,13 @@ return r; } - function initMap(mapLoaded: boolean) { + function initMap(mapLoaded: boolean) { if (!map) { return; } refreshElevationProfile(); - if (showElevation) { + if (showElevation && data.length) { epc?.showProfile(); } @@ -201,22 +201,24 @@ removeTrailLayer(layerId); } }); - + if ( !drawing && fitBounds !== "off" && data.some((d) => d.bbox !== undefined) - ) { + ) { if (activeTrail !== null && mapLoaded) { focusTrail(trails[activeTrail]); } else { flyToBounds(); } + } else if(drawing && activeTrail !== null && mapLoaded) { + addCaretLayer(trails[activeTrail]?.id) } } - export function refreshElevationProfile() { - if (activeTrail !== null && data[activeTrail]) { + export function refreshElevationProfile() { + if (activeTrail !== null && data[activeTrail]) { epc?.setData(data[activeTrail]!, waypoints); } } @@ -283,6 +285,7 @@ map?.off("mouseleave", id, layers[id].listener.onLeave!); map?.off("mouseup", id, layers[id].listener.onMouseUp!); map?.off("mousemove", id, layers[id].listener.onMouseMove!); + map?.off("mousedown", id, layers[id].listener.onMouseDown!); delete layers[id]; } @@ -347,10 +350,9 @@ highlightTrail(id, trails[activeTrail ?? -1]?.id == id); layers[id].listener.onLeave = (e) => unHighlightTrail(id); layers[id].listener.onMouseUp = (e) => { - activeTrail = trails.indexOf(trail); + activeTrail = trails.findIndex(t => t.id == trail.id); }; - layers[id].listener.onMouseMove = - moveElevationMarkerToCursorPosition; + layers[id].listener.onMouseMove = moveCrosshairToCursorPosition; layers[id].listener.onMouseDown = (e) => handDragStart(e, id); map.on("mouseenter", id, layers[id].listener.onEnter); @@ -365,6 +367,11 @@ } } + function moveCrosshairToCursorPosition(e: M.MapMouseEvent) { + epc?.moveCrosshair(e.lngLat.lat, e.lngLat.lng); + moveElevationMarkerToCursorPosition(e); + } + function moveElevationMarkerToCursorPosition(e: M.MapMouseEvent) { elevationMarker.setLngLat(e.lngLat); } @@ -394,6 +401,7 @@ function handleDragEnd(e: M.MapMouseEvent) { map?.off("mousemove", moveElevationMarkerToCursorPosition); + epc?.hideCrosshair(); dispatch("segmentDragEnd", { segment: draggingSegment, event: e }); draggingSegment = null; } @@ -405,7 +413,7 @@ if (map.getLayer("direction-carets")) { removeCaretLayer(); } - + map.addLayer({ id: "direction-carets", type: "symbol", @@ -459,27 +467,26 @@ return; } elevationMarker.setOpacity("0"); + epc?.hideCrosshair(); hoveringTrail = false; map?.setPaintProperty(id, "line-width", 5); // map?.setPaintProperty(id, "line-color", "#648ad5"); } function focusTrail(trail: Trail) { - - activeTrail = trails.findIndex(t => t.id == trail.id); + activeTrail = trails.findIndex((t) => t.id == trail.id); if (activeTrail < 0) { activeTrail = null; return; } - + const currentlyFocussedTrail = trails[activeTrail]; if (currentlyFocussedTrail && currentlyFocussedTrail != trail) { unFocusTrail(currentlyFocussedTrail); } dispatch("select", trail); - + try { - highlightTrail(trail.id!); refreshElevationProfile(); if (showElevation) { epc?.showProfile(); @@ -488,7 +495,7 @@ addCaretLayer(trail.id!); flyToBounds(); } catch (e) { - console.warn(e) + console.warn(e); } } @@ -743,7 +750,11 @@ elevationMarker.setOpacity("0"); }, onMove: (data) => { - elevationMarker.setLngLat(data.position as M.LngLatLike); + if (!hoveringTrail) { + elevationMarker.setLngLat( + data.position as M.LngLatLike, + ); + } }, }); map.addControl(epc); diff --git a/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts b/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts index dfaaecaf..f733e99b 100644 --- a/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts +++ b/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts @@ -2,7 +2,7 @@ import { type ControlPosition, type IControl } from "maplibre-gl"; import * as M from "maplibre-gl"; import { ElevationProfile, type ElevationProfileOptions } from "./elevationprofile"; // @ts-ignore -import type { GeoJsonObject } from "geojson"; +import type { GeoJsonObject, Position } from "geojson"; import type { Waypoint } from "$lib/models/waypoint"; /** @@ -189,6 +189,30 @@ export class ElevationProfileControl implements IControl { this.isProfileShown = false; } + moveCrosshair(lat: number, lon: number) { + if (!this.elevationProfileChart) { + return; + } + const chart = this.elevationProfileChart.chart + const point = this.elevationProfileChart.getChartCoordinatesFromPosition(lat, lon) + if (point == null) { + return; + } + const rectangle = chart.canvas.getBoundingClientRect(); + + const mouseMoveEvent = new MouseEvent('mousemove', { + clientX: rectangle.left + point[0], + clientY: rectangle.top + point[1] + }); + + chart.canvas.dispatchEvent(mouseMoveEvent); + } + + hideCrosshair() { + const mouseOutEvent = new MouseEvent('mouseout'); + return this.elevationProfileChart?.chart.canvas.dispatchEvent(mouseOutEvent); + } + onRemove(): void { // remove button if (this.buttonContainer?.parentNode) { diff --git a/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts b/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts index a003feaf..c9cc8a19 100644 --- a/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts +++ b/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts @@ -358,7 +358,7 @@ const elevationProfileDefaultOptions: ElevationProfileOptions = { export class ElevationProfile { private canvas: HTMLCanvasElement; private settings: ElevationProfileOptions; - private chart: Chart<"line", Array, number>; + public chart: Chart<"line", Array, number>; private elevatedPositions: Position[] = []; private elevatedPositionsAdjustedUnit: Position[] = []; private cumulatedDistance: number[] = []; @@ -483,7 +483,6 @@ export class ElevationProfile { }, onHover: (_e, item) => { if (typeof this.settings.onMove !== "function") return; - try { const i = item[0].index; @@ -753,6 +752,33 @@ export class ElevationProfile { } } + getChartCoordinatesFromPosition(lat: number, lon: number) { + + let minDistance = Infinity + let bestCandiateIndex: number = -1 + for (let i = 0; i < this.elevatedPositionsAdjustedUnit.length; i++) { + const p = this.elevatedPositionsAdjustedUnit[i]; + const deltaLat = lat - p[1]; + const deltaLon = lon - p[0]; + const distance = deltaLat * deltaLat + deltaLon * deltaLon; + + if (distance < minDistance) { + minDistance = distance; + bestCandiateIndex = i + } + }; + + if (bestCandiateIndex < 0) { + return null; + } + const meta = this.chart.getDatasetMeta(0); + const element = meta.data[bestCandiateIndex]; + if (element) { + return [element.x, element.y]; + } + return null; + } + gradientFromElevation(chart: Chart, force: boolean = false) { const ctx = chart.ctx; const chartArea = chart.chartArea; @@ -864,7 +890,7 @@ export class ElevationProfile { async setData(data: GeoJsonObject, waypoints?: Waypoint[]) { // Concatenates the positions that may come from multiple LineStrings or MultiLineString const { positions, times } = geoJsonObjectToPositionsAndTimes(data); - + this.times = times; this.elevatedPositions = smoothElevations(positions, Math.ceil(positions.length / 100)); @@ -954,14 +980,17 @@ export class ElevationProfile { const timeStart = this.times[segmentStartIndex] const timeEnd = this.times[i] - const timeDelta = (timeEnd.getTime() - timeStart.getTime()) / 1000 + if (timeStart && timeEnd) { + const timeDelta = (timeEnd.getTime() - timeStart.getTime()) / 1000 - speed = (distanceDelta / timeDelta) - if (this.settings.unit === "imperial") { - speed = speed * MILES_HOUR_PER_METER_SECOND - } else { - speed = speed * KILOMETERS_HOUR_PER_METER_SECOND; + speed = (distanceDelta / timeDelta) + if (this.settings.unit === "imperial") { + speed = speed * MILES_HOUR_PER_METER_SECOND + } else { + speed = speed * KILOMETERS_HOUR_PER_METER_SECOND; + } } + }