mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
4c0583845a
Selecting a place no longer spiderfies a city-sized cluster: the reveal only fans open bubbles of up to 12 stops, or stacks whose stops all share one coordinate, so a zoomed-out trip with hundreds of places stays usable (#2990). The selected pin now carries a white border inside a pulsing blue ring on both renderers (static under reduced motion), and the map measures the place card instead of assuming a fixed 320px, so the selection pans the pin above even a tall card on portrait tablets and eases it back out when the card grows over it later (#2991). Closes #2990 Closes #2991
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
import type L from 'leaflet';
|
|
import { useEffect, useRef } from 'react';
|
|
import { useMap } from 'react-leaflet';
|
|
import type { Place } from '../../types';
|
|
import { frameLng } from '../../utils/lngFrame';
|
|
import type { ViewportPadding } from '../../utils/mapViewport';
|
|
import { useLngFrame } from './lngFrameContext';
|
|
import { isUnderChrome } from './mapChromePadding';
|
|
import { selectedPlaceTarget } from './selectedPlaceTarget';
|
|
|
|
interface SelectionControllerProps {
|
|
/** False while the traveller locked the map (#2010): a pick changes nothing on it. */
|
|
follow: boolean;
|
|
places: Place[];
|
|
selectedPlaceId: number | null;
|
|
dayPlaces: Place[];
|
|
selectedPlace: Place | null;
|
|
paddingBox: ViewportPadding;
|
|
}
|
|
|
|
/**
|
|
* Where the Leaflet map goes when a place is selected: there, and out from under the
|
|
* panels. The GL twin is `useSelectionCameraGL`.
|
|
*/
|
|
export function SelectionController({
|
|
follow,
|
|
places,
|
|
selectedPlaceId,
|
|
dayPlaces,
|
|
selectedPlace,
|
|
paddingBox,
|
|
}: SelectionControllerProps) {
|
|
const map = useMap();
|
|
const frame = useLngFrame();
|
|
const prev = useRef<number | null>(null);
|
|
/** The panel geometry the last pan was measured against. */
|
|
const pannedWith = useRef(paddingBox);
|
|
const target = selectedPlaceTarget(selectedPlaceId, places, dayPlaces, selectedPlace);
|
|
const lat = target?.lat ?? null;
|
|
const lng = target?.lng == null ? null : frameLng(frame, target.lng);
|
|
|
|
useEffect(() => {
|
|
const isNew = selectedPlaceId !== prev.current;
|
|
prev.current = selectedPlaceId;
|
|
if (!isNew || !follow || !selectedPlaceId || lat == null || lng == null) return;
|
|
pannedWith.current = paddingBox;
|
|
panToPlace(map, [lat, lng], paddingBox);
|
|
}, [selectedPlaceId, follow, lat, lng, map, paddingBox]);
|
|
|
|
// The place card only learns its height after it laid out, and grows again when its
|
|
// details arrive. If it grew over the pin the selection placed, bring the pin back out;
|
|
// a pin still in the open part of the map stays where the traveller left it (#2991).
|
|
useEffect(() => {
|
|
if (pannedWith.current === paddingBox) return;
|
|
pannedWith.current = paddingBox;
|
|
if (!follow || !selectedPlaceId || lat == null || lng == null) return;
|
|
if (typeof map.latLngToContainerPoint !== 'function') return;
|
|
if (isUnderChrome(map.latLngToContainerPoint([lat, lng]), map.getSize(), paddingBox)) {
|
|
panToPlace(map, [lat, lng], paddingBox);
|
|
}
|
|
}, [paddingBox, follow, selectedPlaceId, lat, lng, map]);
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Pan to a place without changing zoom. The centre is offset by the side panels and the
|
|
* bottom card, so the pin lands in the middle of the *visible* map area rather than the
|
|
* geometric centre, where the card would cover it.
|
|
*/
|
|
function panToPlace(map: L.Map, latlng: [number, number], padding: ViewportPadding): void {
|
|
if (typeof map.project !== 'function' || typeof map.unproject !== 'function') {
|
|
map.panTo(latlng, { animate: true });
|
|
return;
|
|
}
|
|
const { top, right, bottom, left } = padding;
|
|
const point = map.project(latlng).add([(right - left) / 2, (bottom - top) / 2]);
|
|
map.panTo(map.unproject(point), { animate: true });
|
|
}
|