Fix waypoint drag creating unwanted split points

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) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-29 12:34:09 +02:00
parent 26dd1a4d83
commit 400189d9c1
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
2 changed files with 29 additions and 5 deletions

View file

@ -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}
/>
</>

View file

@ -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<boolean>;
}
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<L.Marker | null>(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;
}