From ee7cbd83d037df2fd5e1fe3bf7088c96c59ca3e4 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 29 May 2025 17:52:01 +0200 Subject: [PATCH 1/4] initial implementation --- src/layers/ContextMenu.tsx | 77 ++++++++++++++++++++++++++++------- src/sidebar/SettingsBox.tsx | 7 ++++ src/stores/MapOptionsStore.ts | 4 +- src/stores/SettingsStore.ts | 10 ++++- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/src/layers/ContextMenu.tsx b/src/layers/ContextMenu.tsx index df87b7cf..5bda2fe0 100644 --- a/src/layers/ContextMenu.tsx +++ b/src/layers/ContextMenu.tsx @@ -1,11 +1,15 @@ -import { Map, Overlay } from 'ol' -import { ContextMenuContent } from '@/map/ContextMenuContent' -import { useEffect, useRef, useState } from 'react' -import { QueryPoint } from '@/stores/QueryStore' -import { fromLonLat, toLonLat } from 'ol/proj' +import {Map, Overlay} from 'ol' +import {ContextMenuContent} from '@/map/ContextMenuContent' +import {useContext, useEffect, useRef, useState} from 'react' +import {QueryPoint} from '@/stores/QueryStore' +import {fromLonLat, toLonLat} from 'ol/proj' import styles from '@/layers/ContextMenu.module.css' -import { RouteStoreState } from '@/stores/RouteStore' -import { Coordinate } from '@/utils' +import {RouteStoreState} from '@/stores/RouteStore' +import {Coordinate} from '@/utils' +import Dispatcher from "@/stores/Dispatcher"; +import {AddPoint, SetPoint} from "@/actions/Actions"; +import {coordinateToText} from "@/Converters"; +import {SettingsContext} from "@/contexts/SettingsContext"; interface ContextMenuProps { map: Map @@ -17,19 +21,61 @@ const overlay = new Overlay({ autoPan: true, }) -export default function ContextMenu({ map, route, queryPoints }: ContextMenuProps) { +export default function ContextMenu({map, route, queryPoints}: ContextMenuProps) { const [menuCoordinate, setMenuCoordinate] = useState(null) const container = useRef(null) + const settings = useContext(SettingsContext) + + const queryPointsRef = useRef(queryPoints) + queryPointsRef.current = queryPoints + const settingsRef = useRef(settings) + settingsRef.current = settings const openContextMenu = (e: any) => { e.preventDefault() const coordinate = map.getEventCoordinate(e) const lonLat = toLonLat(coordinate) - setMenuCoordinate({ lng: lonLat[0], lat: lonLat[1] }) + setMenuCoordinate({lng: lonLat[0], lat: lonLat[1]}) } - const closeContextMenu = () => { - setMenuCoordinate(null) + const handleClick = (e: any) => { + if (e.dragging) return + + // If click is inside the context menu, do nothing + const clickedElement = document.elementFromPoint(e.pixel[0], e.pixel[1]) + if (container.current?.contains(clickedElement)) { + return + } + + if (menuCoordinate) { + // Context menu is open -> close it and skip adding a point + setMenuCoordinate(null) + return + } + + if (!settingsRef.current.addPointOnClick) return + + const lonLat = toLonLat(e.coordinate) + const myCoord = {lng: lonLat[0], lat: lonLat[1]} + + const points = queryPointsRef.current + let idx = points.length + if (idx == 2) { + if (!points[1].isInitialized) idx--; + } + if (idx == 1) { + if (!points[0].isInitialized) idx--; + } + if (idx < 2) { + const setPoint = new SetPoint({ + ...points[idx], + coordinate: myCoord, + queryText: coordinateToText(myCoord), + isInitialized: true + }, false); + Dispatcher.dispatch(setPoint) + } else + Dispatcher.dispatch(new AddPoint(idx, myCoord, true, false)) } useEffect(() => { @@ -39,7 +85,7 @@ export default function ContextMenu({ map, route, queryPoints }: ContextMenuProp const longTouchHandler = new LongTouchHandler(e => openContextMenu(e)) function onMapTargetChange() { - // it is important to setup new listeners whenever the map target changes, like when we switch between the + // it is important to set up new listeners whenever the map target changes, like when we switch between the // small and large screen layout, see #203 // we cannot listen to right-click simply using map.on('contextmenu') and need to add the listener to @@ -51,13 +97,14 @@ export default function ContextMenu({ map, route, queryPoints }: ContextMenuProp map.getTargetElement().addEventListener('touchmove', () => longTouchHandler.onTouchEnd()) map.getTargetElement().addEventListener('touchend', () => longTouchHandler.onTouchEnd()) - map.getTargetElement().addEventListener('click', closeContextMenu) + map.on('singleclick', handleClick) } + map.on('change:target', onMapTargetChange) return () => { map.getTargetElement().removeEventListener('contextmenu', openContextMenu) - map.getTargetElement().removeEventListener('click', closeContextMenu) + map.un('singleclick', handleClick) map.removeOverlay(overlay) map.un('change:target', onMapTargetChange) } @@ -74,7 +121,7 @@ export default function ContextMenu({ map, route, queryPoints }: ContextMenuProp coordinate={menuCoordinate!} queryPoints={queryPoints} route={route} - onSelect={closeContextMenu} + onSelect={() => setMenuCoordinate(null)} /> )} diff --git a/src/sidebar/SettingsBox.tsx b/src/sidebar/SettingsBox.tsx index eb36ae57..9338ce7b 100644 --- a/src/sidebar/SettingsBox.tsx +++ b/src/sidebar/SettingsBox.tsx @@ -51,6 +51,13 @@ export default function SettingsBox({ profile }: { profile: RoutingProfile }) { Dispatcher.dispatch(new UpdateSettings({ showDistanceInMiles: !settings.showDistanceInMiles })) } /> + + Dispatcher.dispatch(new UpdateSettings({ addPointOnClick: !settings.addPointOnClick })) + } + />
{tr('settings_gpx_export')}
diff --git a/src/stores/MapOptionsStore.ts b/src/stores/MapOptionsStore.ts index e2f27676..e082df93 100644 --- a/src/stores/MapOptionsStore.ts +++ b/src/stores/MapOptionsStore.ts @@ -5,7 +5,7 @@ import { SelectMapLayer, ToggleExternalMVTLayer, ToggleRoutingGraph, - ToggleUrbanDensityLayer, + ToggleUrbanDensityLayer, UpdateSettings, } from '@/actions/Actions' import config from 'config' @@ -198,6 +198,8 @@ export default class MapOptionsStore extends Store { ...state, selectedStyle: styleOption, } + } else if (action instanceof UpdateSettings) { + } else if (action instanceof ToggleRoutingGraph) { return { ...state, diff --git a/src/stores/SettingsStore.ts b/src/stores/SettingsStore.ts index 406c0bb9..4902e219 100644 --- a/src/stores/SettingsStore.ts +++ b/src/stores/SettingsStore.ts @@ -1,10 +1,12 @@ import Store from '@/stores/Store' -import { Action } from '@/stores/Dispatcher' -import { SetCustomModelEnabled, UpdateSettings } from '@/actions/Actions' +import {Action} from '@/stores/Dispatcher' +import {SetCustomModelEnabled, UpdateSettings} from '@/actions/Actions' +import {getMap} from "@/map/map"; export interface Settings { showDistanceInMiles: boolean drawAreasEnabled: boolean + addPointOnClick: boolean gpxExportRte: boolean gpxExportWpt: boolean gpxExportTrk: boolean @@ -13,6 +15,7 @@ export interface Settings { export const defaultSettings: Settings = { showDistanceInMiles: false, drawAreasEnabled: false, + addPointOnClick: false, gpxExportRte: false, gpxExportWpt: false, gpxExportTrk: true, @@ -31,6 +34,9 @@ export default class SettingsStore extends Store { drawAreasEnabled: false, } } else if (action instanceof UpdateSettings) { + if ('addPointOnClick' in action.updatedSettings) + getMap().getViewport().style.cursor = action.updatedSettings.addPointOnClick ? 'crosshair' : '' + return { ...state, ...action.updatedSettings, From f1c2a63fa85175b83b963fd5e475cadb533abc90 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 29 May 2025 18:00:39 +0200 Subject: [PATCH 2/4] clean up --- src/layers/ContextMenu.tsx | 65 +++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/layers/ContextMenu.tsx b/src/layers/ContextMenu.tsx index 5bda2fe0..65b89f76 100644 --- a/src/layers/ContextMenu.tsx +++ b/src/layers/ContextMenu.tsx @@ -1,6 +1,6 @@ import {Map, Overlay} from 'ol' import {ContextMenuContent} from '@/map/ContextMenuContent' -import {useContext, useEffect, useRef, useState} from 'react' +import {useCallback, useContext, useEffect, useRef, useState} from 'react' import {QueryPoint} from '@/stores/QueryStore' import {fromLonLat, toLonLat} from 'ol/proj' import styles from '@/layers/ContextMenu.module.css' @@ -26,19 +26,14 @@ export default function ContextMenu({map, route, queryPoints}: ContextMenuProps) const container = useRef(null) const settings = useContext(SettingsContext) - const queryPointsRef = useRef(queryPoints) - queryPointsRef.current = queryPoints - const settingsRef = useRef(settings) - settingsRef.current = settings - - const openContextMenu = (e: any) => { + const openContextMenu = useCallback((e: any) => { e.preventDefault() const coordinate = map.getEventCoordinate(e) const lonLat = toLonLat(coordinate) setMenuCoordinate({lng: lonLat[0], lat: lonLat[1]}) - } + }, [map]) - const handleClick = (e: any) => { + const handleClick = useCallback((e: any) => { if (e.dragging) return // If click is inside the context menu, do nothing @@ -53,22 +48,21 @@ export default function ContextMenu({map, route, queryPoints}: ContextMenuProps) return } - if (!settingsRef.current.addPointOnClick) return + if (!settings.addPointOnClick) return const lonLat = toLonLat(e.coordinate) const myCoord = {lng: lonLat[0], lat: lonLat[1]} - const points = queryPointsRef.current - let idx = points.length + let idx = queryPoints.length if (idx == 2) { - if (!points[1].isInitialized) idx--; + if (!queryPoints[1].isInitialized) idx--; } if (idx == 1) { - if (!points[0].isInitialized) idx--; + if (!queryPoints[0].isInitialized) idx--; } if (idx < 2) { const setPoint = new SetPoint({ - ...points[idx], + ...queryPoints[idx], coordinate: myCoord, queryText: coordinateToText(myCoord), isInitialized: true @@ -76,7 +70,7 @@ export default function ContextMenu({map, route, queryPoints}: ContextMenuProps) Dispatcher.dispatch(setPoint) } else Dispatcher.dispatch(new AddPoint(idx, myCoord, true, false)) - } + }, [menuCoordinate, settings.addPointOnClick, queryPoints]) useEffect(() => { overlay.setElement(container.current!) @@ -85,30 +79,43 @@ export default function ContextMenu({map, route, queryPoints}: ContextMenuProps) const longTouchHandler = new LongTouchHandler(e => openContextMenu(e)) function onMapTargetChange() { - // it is important to set up new listeners whenever the map target changes, like when we switch between the - // small and large screen layout, see #203 - - // we cannot listen to right-click simply using map.on('contextmenu') and need to add the listener to - // the map container instead - // https://github.com/openlayers/openlayers/issues/12512#issuecomment-879403189 - map.getTargetElement().addEventListener('contextmenu', openContextMenu) + const targetElement = map.getTargetElement() + if (!targetElement) { + console.warn('Map target element is null. Delaying event listeners setup.'); + return; + } - map.getTargetElement().addEventListener('touchstart', e => longTouchHandler.onTouchStart(e)) - map.getTargetElement().addEventListener('touchmove', () => longTouchHandler.onTouchEnd()) - map.getTargetElement().addEventListener('touchend', () => longTouchHandler.onTouchEnd()) + targetElement.addEventListener('contextmenu', openContextMenu) + targetElement.addEventListener('touchstart', e => longTouchHandler.onTouchStart(e)) + targetElement.addEventListener('touchmove', () => longTouchHandler.onTouchEnd()) + targetElement.addEventListener('touchend', () => longTouchHandler.onTouchEnd()) map.on('singleclick', handleClick) } + const cleanupMapTarget = () => { + const targetElement = map.getTargetElement() + if (targetElement) { + targetElement.removeEventListener('contextmenu', openContextMenu) + targetElement.removeEventListener('touchstart', e => longTouchHandler.onTouchStart(e)) + targetElement.removeEventListener('touchmove', () => longTouchHandler.onTouchEnd()) + targetElement.removeEventListener('touchend', () => longTouchHandler.onTouchEnd()) + } + map.un('singleclick', handleClick) + } + + // Set up initial listeners + onMapTargetChange() + + // Listen for target changes map.on('change:target', onMapTargetChange) return () => { - map.getTargetElement().removeEventListener('contextmenu', openContextMenu) - map.un('singleclick', handleClick) + cleanupMapTarget() map.removeOverlay(overlay) map.un('change:target', onMapTargetChange) } - }, [map]) + }, [map, openContextMenu, handleClick]) useEffect(() => { overlay.setPosition(menuCoordinate ? fromLonLat([menuCoordinate.lng, menuCoordinate.lat]) : undefined) From 26d947c037f9da15231af62986f5022d13db4743 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 29 May 2025 18:04:06 +0200 Subject: [PATCH 3/4] format --- src/layers/ContextMenu.tsx | 114 ++++++++++++++++++---------------- src/layers/UsePathsLayer.tsx | 2 +- src/stores/MapOptionsStore.ts | 4 +- src/stores/SettingsStore.ts | 6 +- 4 files changed, 67 insertions(+), 59 deletions(-) diff --git a/src/layers/ContextMenu.tsx b/src/layers/ContextMenu.tsx index 65b89f76..5b461bbb 100644 --- a/src/layers/ContextMenu.tsx +++ b/src/layers/ContextMenu.tsx @@ -1,15 +1,15 @@ -import {Map, Overlay} from 'ol' -import {ContextMenuContent} from '@/map/ContextMenuContent' -import {useCallback, useContext, useEffect, useRef, useState} from 'react' -import {QueryPoint} from '@/stores/QueryStore' -import {fromLonLat, toLonLat} from 'ol/proj' +import { Map, Overlay } from 'ol' +import { ContextMenuContent } from '@/map/ContextMenuContent' +import { useCallback, useContext, useEffect, useRef, useState } from 'react' +import { QueryPoint } from '@/stores/QueryStore' +import { fromLonLat, toLonLat } from 'ol/proj' import styles from '@/layers/ContextMenu.module.css' -import {RouteStoreState} from '@/stores/RouteStore' -import {Coordinate} from '@/utils' -import Dispatcher from "@/stores/Dispatcher"; -import {AddPoint, SetPoint} from "@/actions/Actions"; -import {coordinateToText} from "@/Converters"; -import {SettingsContext} from "@/contexts/SettingsContext"; +import { RouteStoreState } from '@/stores/RouteStore' +import { Coordinate } from '@/utils' +import Dispatcher from '@/stores/Dispatcher' +import { AddPoint, SetPoint } from '@/actions/Actions' +import { coordinateToText } from '@/Converters' +import { SettingsContext } from '@/contexts/SettingsContext' interface ContextMenuProps { map: Map @@ -21,56 +21,64 @@ const overlay = new Overlay({ autoPan: true, }) -export default function ContextMenu({map, route, queryPoints}: ContextMenuProps) { +export default function ContextMenu({ map, route, queryPoints }: ContextMenuProps) { const [menuCoordinate, setMenuCoordinate] = useState(null) const container = useRef(null) const settings = useContext(SettingsContext) - const openContextMenu = useCallback((e: any) => { - e.preventDefault() - const coordinate = map.getEventCoordinate(e) - const lonLat = toLonLat(coordinate) - setMenuCoordinate({lng: lonLat[0], lat: lonLat[1]}) - }, [map]) + const openContextMenu = useCallback( + (e: any) => { + e.preventDefault() + const coordinate = map.getEventCoordinate(e) + const lonLat = toLonLat(coordinate) + setMenuCoordinate({ lng: lonLat[0], lat: lonLat[1] }) + }, + [map] + ) - const handleClick = useCallback((e: any) => { - if (e.dragging) return + const handleClick = useCallback( + (e: any) => { + if (e.dragging) return - // If click is inside the context menu, do nothing - const clickedElement = document.elementFromPoint(e.pixel[0], e.pixel[1]) - if (container.current?.contains(clickedElement)) { - return - } + // If click is inside the context menu, do nothing + const clickedElement = document.elementFromPoint(e.pixel[0], e.pixel[1]) + if (container.current?.contains(clickedElement)) { + return + } - if (menuCoordinate) { - // Context menu is open -> close it and skip adding a point - setMenuCoordinate(null) - return - } + if (menuCoordinate) { + // Context menu is open -> close it and skip adding a point + setMenuCoordinate(null) + return + } - if (!settings.addPointOnClick) return + if (!settings.addPointOnClick) return - const lonLat = toLonLat(e.coordinate) - const myCoord = {lng: lonLat[0], lat: lonLat[1]} + const lonLat = toLonLat(e.coordinate) + const myCoord = { lng: lonLat[0], lat: lonLat[1] } - let idx = queryPoints.length - if (idx == 2) { - if (!queryPoints[1].isInitialized) idx--; - } - if (idx == 1) { - if (!queryPoints[0].isInitialized) idx--; - } - if (idx < 2) { - const setPoint = new SetPoint({ - ...queryPoints[idx], - coordinate: myCoord, - queryText: coordinateToText(myCoord), - isInitialized: true - }, false); - Dispatcher.dispatch(setPoint) - } else - Dispatcher.dispatch(new AddPoint(idx, myCoord, true, false)) - }, [menuCoordinate, settings.addPointOnClick, queryPoints]) + let idx = queryPoints.length + if (idx == 2) { + if (!queryPoints[1].isInitialized) idx-- + } + if (idx == 1) { + if (!queryPoints[0].isInitialized) idx-- + } + if (idx < 2) { + const setPoint = new SetPoint( + { + ...queryPoints[idx], + coordinate: myCoord, + queryText: coordinateToText(myCoord), + isInitialized: true, + }, + false + ) + Dispatcher.dispatch(setPoint) + } else Dispatcher.dispatch(new AddPoint(idx, myCoord, true, false)) + }, + [menuCoordinate, settings.addPointOnClick, queryPoints] + ) useEffect(() => { overlay.setElement(container.current!) @@ -81,8 +89,8 @@ export default function ContextMenu({map, route, queryPoints}: ContextMenuProps) function onMapTargetChange() { const targetElement = map.getTargetElement() if (!targetElement) { - console.warn('Map target element is null. Delaying event listeners setup.'); - return; + console.warn('Map target element is null. Delaying event listeners setup.') + return } targetElement.addEventListener('contextmenu', openContextMenu) diff --git a/src/layers/UsePathsLayer.tsx b/src/layers/UsePathsLayer.tsx index a9629b3e..c502c48c 100644 --- a/src/layers/UsePathsLayer.tsx +++ b/src/layers/UsePathsLayer.tsx @@ -133,7 +133,7 @@ function addAccessNetworkLayer(map: Map, selectedPath: Path, queryPoints: QueryP }) layer.setStyle(style) for (let i = 0; i < selectedPath.snapped_waypoints.coordinates.length; i++) { - if(i >= queryPoints.length) break // can happen if deleted too fast + if (i >= queryPoints.length) break // can happen if deleted too fast const start = fromLonLat([queryPoints[i].coordinate.lng, queryPoints[i].coordinate.lat]) const end = fromLonLat(selectedPath.snapped_waypoints.coordinates[i]) layer.getSource()?.addFeature(new Feature(createBezierLineString(start, end))) diff --git a/src/stores/MapOptionsStore.ts b/src/stores/MapOptionsStore.ts index e082df93..df41eeb9 100644 --- a/src/stores/MapOptionsStore.ts +++ b/src/stores/MapOptionsStore.ts @@ -5,7 +5,8 @@ import { SelectMapLayer, ToggleExternalMVTLayer, ToggleRoutingGraph, - ToggleUrbanDensityLayer, UpdateSettings, + ToggleUrbanDensityLayer, + UpdateSettings, } from '@/actions/Actions' import config from 'config' @@ -199,7 +200,6 @@ export default class MapOptionsStore extends Store { selectedStyle: styleOption, } } else if (action instanceof UpdateSettings) { - } else if (action instanceof ToggleRoutingGraph) { return { ...state, diff --git a/src/stores/SettingsStore.ts b/src/stores/SettingsStore.ts index 4902e219..2f7adef7 100644 --- a/src/stores/SettingsStore.ts +++ b/src/stores/SettingsStore.ts @@ -1,7 +1,7 @@ import Store from '@/stores/Store' -import {Action} from '@/stores/Dispatcher' -import {SetCustomModelEnabled, UpdateSettings} from '@/actions/Actions' -import {getMap} from "@/map/map"; +import { Action } from '@/stores/Dispatcher' +import { SetCustomModelEnabled, UpdateSettings } from '@/actions/Actions' +import { getMap } from '@/map/map' export interface Settings { showDistanceInMiles: boolean From ed77f5f7d204520fc195926ee14bcf96fb8f511e Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 29 May 2025 18:09:20 +0200 Subject: [PATCH 4/4] clean up --- src/layers/ContextMenu.tsx | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/layers/ContextMenu.tsx b/src/layers/ContextMenu.tsx index 5b461bbb..ff426a03 100644 --- a/src/layers/ContextMenu.tsx +++ b/src/layers/ContextMenu.tsx @@ -87,21 +87,22 @@ export default function ContextMenu({ map, route, queryPoints }: ContextMenuProp const longTouchHandler = new LongTouchHandler(e => openContextMenu(e)) function onMapTargetChange() { - const targetElement = map.getTargetElement() - if (!targetElement) { - console.warn('Map target element is null. Delaying event listeners setup.') - return - } + // it is important to setup new listeners whenever the map target changes, like when we switch between the + // small and large screen layout, see #203 - targetElement.addEventListener('contextmenu', openContextMenu) - targetElement.addEventListener('touchstart', e => longTouchHandler.onTouchStart(e)) - targetElement.addEventListener('touchmove', () => longTouchHandler.onTouchEnd()) - targetElement.addEventListener('touchend', () => longTouchHandler.onTouchEnd()) + // we cannot listen to right-click simply using map.on('contextmenu') and need to add the listener to + // the map container instead + // https://github.com/openlayers/openlayers/issues/12512#issuecomment-879403189 + map.getTargetElement().addEventListener('contextmenu', openContextMenu) - map.on('singleclick', handleClick) + map.getTargetElement().addEventListener('touchstart', e => longTouchHandler.onTouchStart(e)) + map.getTargetElement().addEventListener('touchmove', () => longTouchHandler.onTouchEnd()) + map.getTargetElement().addEventListener('touchend', () => longTouchHandler.onTouchEnd()) } + map.on('singleclick', handleClick) + map.on('change:target', onMapTargetChange) - const cleanupMapTarget = () => { + return () => { const targetElement = map.getTargetElement() if (targetElement) { targetElement.removeEventListener('contextmenu', openContextMenu) @@ -110,16 +111,6 @@ export default function ContextMenu({ map, route, queryPoints }: ContextMenuProp targetElement.removeEventListener('touchend', () => longTouchHandler.onTouchEnd()) } map.un('singleclick', handleClick) - } - - // Set up initial listeners - onMapTargetChange() - - // Listen for target changes - map.on('change:target', onMapTargetChange) - - return () => { - cleanupMapTarget() map.removeOverlay(overlay) map.un('change:target', onMapTargetChange) }