mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
51af575c9a
A right-click, click or drag on a repeated world copy reported the longitude it has on that copy (274.9 for -85.1). The place was saved there, the geocoder found nothing, and a route to a place on the real world ran across the whole map (#2976). Longitudes are now brought back onto [-180, 180] where the map hands them out (Leaflet and GL), where they are stored (places, booking endpoints, collection places, so REST, MCP, plugin RPC and importers alike) and before reverse geocoding. A migration moves the ones already stored. Leaflet draws a layer at its literal longitude only, so a trip from Los Angeles to Auckland had its pins at opposite ends of the world while the flight ran off the edge to the next copy (#2975). A trip that crosses the antimeridian is now drawn in one longitude frame, seamed in the widest stretch it leaves empty, so pins, routes, flights and every overlay lie on one copy and the map opens across the Pacific. Every other trip has no frame and draws exactly as before. The GL day fit also takes the short way. A new install now starts with Journey and Collections switched on, next to the trip addons (all but Road trip), Atlas, Vacay and the Naver import that already were. Existing installs keep their addons as they are. Closes #2975 Closes #2976
133 lines
5.4 KiB
TypeScript
133 lines
5.4 KiB
TypeScript
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
|
|
import { onRealWorld } from '../../utils/worldCopy';
|
|
import { bindPinHoverLabel, mapHoverLabel } from './mapHoverLabel';
|
|
import type { MapViewProps } from './mapViewContract';
|
|
import { plannerPinBadge, plannerPinShape } from './plannerPinShape';
|
|
import { wirePlannerWaypointDrag } from './plannerWaypointDrag';
|
|
|
|
/** The slice of the GL map the waypoint pins need: its box and screen-to-ground lookup. */
|
|
interface PlannerWaypointMap {
|
|
getContainer: () => HTMLElement;
|
|
unproject: (point: [number, number]) => { lat: number; lng: number };
|
|
/** Optional so a stand-in map without events still takes the pins. */
|
|
on?: (type: 'movestart', listener: () => void) => unknown;
|
|
off?: (type: 'movestart', listener: () => void) => unknown;
|
|
}
|
|
|
|
/** The slice of a hand-positioned pin the waypoint markers drive. */
|
|
interface PlannerWaypointPin {
|
|
remove: () => void;
|
|
setLngLat: (value: [number, number]) => void;
|
|
}
|
|
|
|
interface PlannerWaypointMarkersOptions<Pin extends PlannerWaypointPin> {
|
|
mapRef: { readonly current: PlannerWaypointMap | null };
|
|
mapReady: boolean;
|
|
/** The pins live in the caller's ref so its render-clock reposition can reach them. */
|
|
pinsRef: { current: Pin[] };
|
|
/** Puts a waypoint element on the map at the given position. */
|
|
attachPin: (el: HTMLElement, lng: number, lat: number) => Pin;
|
|
/** The pin's inline style for a size and selection; the renderer owns the colour. */
|
|
pinCss: (size: number, selected: boolean) => string;
|
|
plannerWaypoints: NonNullable<MapViewProps['plannerWaypoints']>;
|
|
selectedPlannerWaypointId: string | null;
|
|
onPlannerWaypointClick: MapViewProps['onPlannerWaypointClick'];
|
|
onPlannerWaypointMove: MapViewProps['onPlannerWaypointMove'];
|
|
}
|
|
|
|
/**
|
|
* The numbered control points of a list-first route editor, drawn as pins on the GL map.
|
|
*
|
|
* The handlers are read through a ref so a parent re-rendering with fresh callbacks does
|
|
* not tear the pins down mid-drag; only a change in whether they can be dragged does.
|
|
*/
|
|
export function usePlannerWaypointMarkersGL<Pin extends PlannerWaypointPin>({
|
|
mapRef,
|
|
mapReady,
|
|
pinsRef,
|
|
attachPin,
|
|
pinCss,
|
|
plannerWaypoints,
|
|
selectedPlannerWaypointId,
|
|
onPlannerWaypointClick,
|
|
onPlannerWaypointMove,
|
|
}: PlannerWaypointMarkersOptions<Pin>): void {
|
|
const plannerWaypointHandlersRef = useRef({ onPlannerWaypointClick, onPlannerWaypointMove });
|
|
useLayoutEffect(() => {
|
|
plannerWaypointHandlersRef.current = { onPlannerWaypointClick, onPlannerWaypointMove };
|
|
}, [onPlannerWaypointClick, onPlannerWaypointMove]);
|
|
const plannerWaypointsDraggable = !!onPlannerWaypointMove;
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current;
|
|
if (!map || !mapReady) return;
|
|
pinsRef.current.forEach((pin) => pin.remove());
|
|
pinsRef.current = [];
|
|
const dragCleanups: (() => void)[] = [];
|
|
const hoverLabel = mapHoverLabel(map, 'planner-waypoint-label');
|
|
// A label left behind by a pan or a zoom would float over the wrong spot.
|
|
const hideOnMove = () => hoverLabel.hide();
|
|
map.on?.('movestart', hideOnMove);
|
|
dragCleanups.push(() => map.off?.('movestart', hideOnMove));
|
|
|
|
for (const [index, point] of plannerWaypoints.entries()) {
|
|
const selected = point.id === selectedPlannerWaypointId;
|
|
const { size, text } = plannerPinShape(point, index, selected);
|
|
const el = document.createElement('button');
|
|
el.type = 'button';
|
|
el.textContent = text;
|
|
const name = point.name ?? `Waypoint ${index + 1}`;
|
|
el.setAttribute('aria-label', name);
|
|
el.style.cssText = `${pinCss(size, selected)};cursor:pointer;padding:0`;
|
|
// MDT-01: the moon where a stage ends; the pin is positioned, so the badge sits at its corner.
|
|
const badge = plannerPinBadge(point);
|
|
if (badge) el.insertAdjacentHTML('beforeend', badge);
|
|
const swallow = (event: Event) => event.stopPropagation();
|
|
const select = (event: Event) => {
|
|
event.stopPropagation();
|
|
plannerWaypointHandlersRef.current.onPlannerWaypointClick?.(point.id);
|
|
};
|
|
el.addEventListener('pointerdown', swallow);
|
|
el.addEventListener('touchstart', swallow, { passive: true });
|
|
el.addEventListener('click', select);
|
|
const pin = attachPin(el, point.lng, point.lat);
|
|
pinsRef.current.push(pin);
|
|
dragCleanups.push(bindPinHoverLabel(el, hoverLabel, map.getContainer(), { name, detail: point.detail ?? null }));
|
|
if (plannerWaypointsDraggable) {
|
|
el.style.cursor = 'grab';
|
|
dragCleanups.push(
|
|
wirePlannerWaypointDrag({
|
|
element: el,
|
|
initial: { lat: point.lat, lng: point.lng },
|
|
coordinateAt: (clientX, clientY) => {
|
|
const rect = map.getContainer().getBoundingClientRect();
|
|
const at = map.unproject([clientX - rect.left, clientY - rect.top]);
|
|
return onRealWorld({ lat: at.lat, lng: at.lng });
|
|
},
|
|
setPosition: ({ lat, lng }) => pin.setLngLat([lng, lat]),
|
|
onCommit: ({ lat, lng }) =>
|
|
plannerWaypointHandlersRef.current.onPlannerWaypointMove?.(point.id, lat, lng) ?? false,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
return () => {
|
|
dragCleanups.forEach((cleanup) => cleanup());
|
|
hoverLabel.remove();
|
|
pinsRef.current.forEach((pin) => pin.remove());
|
|
pinsRef.current = [];
|
|
};
|
|
}, [
|
|
attachPin,
|
|
mapReady,
|
|
mapRef,
|
|
pinCss,
|
|
pinsRef,
|
|
plannerWaypoints,
|
|
plannerWaypointsDraggable,
|
|
selectedPlannerWaypointId,
|
|
]);
|
|
}
|