From 400189d9c105508855ea3053be978b1acc61aeaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 29 Mar 2026 12:34:09 +0200 Subject: [PATCH] Fix waypoint drag creating unwanted split points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ghost marker (route split indicator) was interfering with waypoint dragging because it sat above waypoints (zIndexOffset 1000) and stayed active during drag operations. Adopt brouter-web's suspension pattern: - Suspend route interaction on waypoint mouseover (not just dragstart), unsuspend on mouseout — ghost marker hides before any click/drag - Lower ghost marker zIndexOffset to -100 so waypoints always receive clicks first - Keep drag guards as a safety net Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/planner/app/components/PlannerMap.tsx | 17 +++++++++++++++++ .../planner/app/components/RouteInteraction.tsx | 17 ++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/planner/app/components/PlannerMap.tsx b/apps/planner/app/components/PlannerMap.tsx index ed1693f..1597cf5 100644 --- a/apps/planner/app/components/PlannerMap.tsx +++ b/apps/planner/app/components/PlannerMap.tsx @@ -179,6 +179,8 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition }: PlannerMa const [noGoDrawing, setNoGoDrawing] = useState(false); const toggleNoGoDraw = useCallback(() => setNoGoDrawing((v) => !v), []); const suppressMapClickRef = useRef(false); + const routeInteractionSuspendedRef = useRef(false); + const waypointDraggingRef = useRef(false); // Sync waypoints from Yjs useEffect(() => { @@ -324,7 +326,21 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition }: PlannerMa draggable icon={waypointIcon(i)} eventHandlers={{ + mouseover: () => { + routeInteractionSuspendedRef.current = true; + }, + mouseout: () => { + if (!waypointDraggingRef.current) { + routeInteractionSuspendedRef.current = false; + } + }, + dragstart: () => { + waypointDraggingRef.current = true; + routeInteractionSuspendedRef.current = true; + }, dragend: (e) => { + waypointDraggingRef.current = false; + routeInteractionSuspendedRef.current = false; const { lat, lng } = e.target.getLatLng(); moveWaypoint(i, lat, lng); }, @@ -347,6 +363,7 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition }: PlannerMa coordinates={routeCoordinates} segmentBoundaries={segmentBoundaries} onInsertWaypoint={handleRouteInsert} + suspendedRef={routeInteractionSuspendedRef} disabled={noGoDrawing} /> diff --git a/apps/planner/app/components/RouteInteraction.tsx b/apps/planner/app/components/RouteInteraction.tsx index 6179973..0689ca7 100644 --- a/apps/planner/app/components/RouteInteraction.tsx +++ b/apps/planner/app/components/RouteInteraction.tsx @@ -7,6 +7,8 @@ interface RouteInteractionProps { segmentBoundaries: number[]; onInsertWaypoint: (pointIndex: number, lat: number, lon: number) => void; disabled?: boolean; + /** Set true to suspend ghost marker (e.g. while hovering/dragging a waypoint) */ + suspendedRef?: React.RefObject; } const SNAP_TOLERANCE_PX = 15; @@ -26,13 +28,14 @@ const ghostIcon = L.divIcon({ /** * Route interaction layer: shows a ghost marker on hover that can be * clicked or dragged to insert a waypoint. Follows brouter-web's pattern - * of a single persistent draggable Marker with distance-based mouseout. + * of suspending when the mouse is over a waypoint marker. */ export function RouteInteraction({ coordinates, segmentBoundaries, onInsertWaypoint, disabled, + suspendedRef, }: RouteInteractionProps) { const map = useMap(); const markerRef = useRef(null); @@ -86,7 +89,7 @@ export function RouteInteraction({ icon: ghostIcon, draggable: true, interactive: true, - zIndexOffset: 1000, + zIndexOffset: -100, }); markerRef.current = marker; @@ -115,7 +118,11 @@ export function RouteInteraction({ // Listen for mousemove on the map (distance-based approach, not polyline events) const onMouseMove = (e: L.LeafletMouseEvent) => { - if (draggingRef.current) return; + // Suspended while hovering over or dragging a waypoint marker (brouter-web pattern) + if (draggingRef.current || suspendedRef?.current) { + hideMarker(); + return; + } const snap = findClosestOnRoute(e.latlng); if (!snap || snap.distPx > SNAP_TOLERANCE_PX) { @@ -131,7 +138,7 @@ export function RouteInteraction({ hideMarker(); }; - // Drag events on the ghost marker (Leaflet's L.Draggable handles text selection) + // Drag events on the ghost marker marker.on("dragstart", () => { draggingRef.current = true; trailer1.setStyle({ opacity: 0.6 }); @@ -200,7 +207,7 @@ export function RouteInteraction({ trailer2.remove(); markerRef.current = null; }; - }, [map, disabled, findClosestOnRoute]); + }, [map, disabled, findClosestOnRoute, suspendedRef]); return null; }