|
| 1 | +import { useMap } from 'react-map-gl/maplibre'; |
| 2 | +import { Crosshair } from 'lucide-react'; |
| 3 | +import { useCommonStore } from '@/stores/common-store'; |
| 4 | +import { useDirectionsStore } from '@/stores/directions-store'; |
| 5 | +import { CustomControl, ControlButton } from '../custom-control'; |
| 6 | + |
| 7 | +export function RecenterControl() { |
| 8 | + // Check if a route exists. successful turns false when user resets waypoints. |
| 9 | + const hasRoute = useDirectionsStore((state) => state.successful); |
| 10 | + |
| 11 | + // useMap has to be called here, before the early return below. |
| 12 | + const { current: map } = useMap(); |
| 13 | + |
| 14 | + // This hides the button when no route is present. |
| 15 | + if (!hasRoute) return null; |
| 16 | + |
| 17 | + const handleRecenter = () => { |
| 18 | + if (!map) return; |
| 19 | + // Read latest store values at click time instead of using stale closures. |
| 20 | + const state = useCommonStore.getState(); |
| 21 | + const { coordinates } = state; |
| 22 | + const dpOpen = state.directionsPanelOpen; |
| 23 | + const spOpen = state.settingsPanelOpen; |
| 24 | + |
| 25 | + if (!coordinates || coordinates.length === 0) return; |
| 26 | + |
| 27 | + const firstCoord = coordinates[0]; |
| 28 | + if (!firstCoord || !firstCoord[0] || !firstCoord[1]) return; |
| 29 | + |
| 30 | + // Walk every route point and grow a bounding box around all of them. |
| 31 | + const bounds: [[number, number], [number, number]] = coordinates.reduce< |
| 32 | + [[number, number], [number, number]] |
| 33 | + >( |
| 34 | + (acc, coord) => { |
| 35 | + if (!coord || !coord[0] || !coord[1]) return acc; |
| 36 | + return [ |
| 37 | + [Math.min(acc[0][0], coord[1]), Math.min(acc[0][1], coord[0])], |
| 38 | + [Math.max(acc[1][0], coord[1]), Math.max(acc[1][1], coord[0])], |
| 39 | + ]; |
| 40 | + }, |
| 41 | + [ |
| 42 | + [firstCoord[1], firstCoord[0]], |
| 43 | + [firstCoord[1], firstCoord[0]], |
| 44 | + ] |
| 45 | + ); |
| 46 | + |
| 47 | + const paddingTopLeft = [screen.width < 550 ? 50 : dpOpen ? 420 : 50, 50]; |
| 48 | + const paddingBottomRight = [ |
| 49 | + screen.width < 550 ? 50 : spOpen ? 420 : 50, |
| 50 | + 50, |
| 51 | + ]; |
| 52 | + |
| 53 | + map.fitBounds(bounds, { |
| 54 | + padding: { |
| 55 | + top: paddingTopLeft[1] as number, |
| 56 | + bottom: paddingBottomRight[1] as number, |
| 57 | + left: paddingTopLeft[0] as number, |
| 58 | + right: paddingBottomRight[0] as number, |
| 59 | + }, |
| 60 | + maxZoom: coordinates.length === 1 ? 11 : 18, |
| 61 | + duration: 800, |
| 62 | + }); |
| 63 | + }; |
| 64 | + |
| 65 | + // Render inside the MapLibre top-right control group so it sits with the zoom buttons. |
| 66 | + return ( |
| 67 | + <CustomControl position="topRight"> |
| 68 | + <ControlButton |
| 69 | + title="Recenter to route" |
| 70 | + icon={<Crosshair size={15} />} |
| 71 | + onClick={handleRecenter} |
| 72 | + data-testid="recenter-button" |
| 73 | + /> |
| 74 | + </CustomControl> |
| 75 | + ); |
| 76 | +} |
0 commit comments