Fix map centering on mobile
This commit is contained in:
parent
7b32a47fc0
commit
58950fd0d1
2 changed files with 41 additions and 9 deletions
|
|
@ -515,6 +515,7 @@ export default function App() {
|
|||
onSelect={select}
|
||||
route={view === 'route' && routeTab === 'walk' ? route : null}
|
||||
journey={journey}
|
||||
sheet={sheetMode && !(guiding && view === 'route') ? sheet : null}
|
||||
routeEnds={view === 'route' ? routeEnds : null}
|
||||
altRoutes={view === 'route' && routeTab === 'walk' ? altRoutes : []}
|
||||
focus={focus}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ interface Props {
|
|||
reduceMotion: boolean;
|
||||
/** Public-transport journey to draw: walking legs, rides and the stops where you board / get off. */
|
||||
journey: JourneyOverlay | null;
|
||||
/** Phone bottom sheet that covers the lower part of the map (null when the panel is beside the map or hidden). */
|
||||
sheet: 'peek' | 'half' | 'full' | null;
|
||||
}
|
||||
|
||||
export interface JourneyOverlay {
|
||||
|
|
@ -95,7 +97,7 @@ export interface JourneyOverlay {
|
|||
stops: { at: LatLon; label: string }[];
|
||||
}
|
||||
|
||||
export function MapView({ places, showPlaces, selectedId, onSelect, route, routeEnds, altRoutes, focus, visible, bbox, online, basemap, onBasemap, userPos, follow, reach, hotspots, hotspotFocus, reduceMotion, journey }: Props) {
|
||||
export function MapView({ places, showPlaces, selectedId, onSelect, route, routeEnds, altRoutes, focus, visible, bbox, online, basemap, onBasemap, userPos, follow, reach, hotspots, hotspotFocus, reduceMotion, journey, sheet }: Props) {
|
||||
const { t, lang } = useI18n();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<MLMap | null>(null);
|
||||
|
|
@ -112,6 +114,25 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
const reduced = () => rmRef.current || window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
// The sheet hides the lower part of the map. Camera padding makes "centre" mean the middle of the part you can see, so a route fit
|
||||
// or a focused place lands there. The inset comes from the sheet's target size (not from the DOM), so it is right while the sheet
|
||||
// is still animating; every camera move syncs it first, and moving the sheet by hand re-centres smoothly.
|
||||
const sheetRef = useRef(sheet);
|
||||
sheetRef.current = sheet;
|
||||
const insetPx = (): number => {
|
||||
const h = ref.current?.clientHeight ?? 0;
|
||||
const s = sheetRef.current;
|
||||
if (!s) return 0;
|
||||
if (s === 'peek') return Math.min(h * 0.5, 6.5 * parseFloat(getComputedStyle(document.documentElement).fontSize));
|
||||
return h * (s === 'half' ? 0.55 : 0.85);
|
||||
};
|
||||
const padding = () => ({ top: 0, left: 0, right: 0, bottom: Math.round(insetPx()) });
|
||||
const syncInset = (map: MLMap) => map.setPadding(padding());
|
||||
const syncInsetRef = useRef(syncInset);
|
||||
syncInsetRef.current = syncInset;
|
||||
const ease = (map: MLMap, o: Parameters<MLMap['easeTo']>[0]) => { syncInset(map); map.easeTo(o); };
|
||||
const fit = (map: MLMap, b: maplibregl.LngLatBounds, o: Parameters<MLMap['fitBounds']>[1]) => { syncInset(map); map.fitBounds(b, o); };
|
||||
|
||||
// ---- init ----
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
|
@ -187,10 +208,12 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
if (id) onSelectRef.current(id);
|
||||
});
|
||||
applyBasemap(map, basemapRef.current);
|
||||
syncInsetRef.current(map);
|
||||
setReady(true);
|
||||
});
|
||||
mapRef.current = map;
|
||||
const ro = new ResizeObserver(() => map.resize());
|
||||
|
||||
const ro = new ResizeObserver(() => { map.resize(); if (!map.isMoving()) syncInsetRef.current(map); });
|
||||
ro.observe(ref.current);
|
||||
return () => { ro.disconnect(); markers.current.forEach((m) => m.remove()); map.remove(); mapRef.current = null; setReady(false); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -198,6 +221,14 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
|
||||
useEffect(() => { if (visible) mapRef.current?.resize(); }, [visible]);
|
||||
|
||||
// the sheet moved: keep what was centred centred in the visible part (skipped while a camera move is running, it already used the target inset)
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!ready || !map || map.isMoving()) return;
|
||||
map.easeTo({ padding: padding(), duration: reduced() ? 0 : 250 });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, sheet]);
|
||||
|
||||
// ---- places ----
|
||||
const fc = useMemo(
|
||||
() => ({
|
||||
|
|
@ -221,7 +252,7 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
if (!ready || !map) return;
|
||||
map.setFilter('places-selected', ['==', ['get', 'id'], showPlaces ? selectedId ?? '' : '']);
|
||||
const sel = selectedId ? places.find((p) => p.place.id === selectedId)?.place : undefined;
|
||||
if (sel && showPlaces) map.easeTo({ center: [sel.lon, sel.lat], zoom: Math.max(map.getZoom(), 17), duration: reduced() ? 0 : 500 });
|
||||
if (sel && showPlaces) ease(map, { center: [sel.lon, sel.lat], zoom: Math.max(map.getZoom(), 17), duration: reduced() ? 0 : 500 });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, selectedId, showPlaces]);
|
||||
|
||||
|
|
@ -280,7 +311,7 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
}
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
route.coords.forEach((c) => bounds.extend(lngLat(c)));
|
||||
map.fitBounds(bounds, { padding: 70, duration: reduced() ? 0 : 600, maxZoom: 17 });
|
||||
fit(map, bounds, { padding: 70, duration: reduced() ? 0 : 600, maxZoom: 17 });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, route, routeEnds, lang]);
|
||||
|
||||
|
|
@ -327,14 +358,14 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
}
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
[...journey.walks.flat(), ...journey.rides.flatMap((r) => r.path)].forEach((c) => bounds.extend(lngLat(c)));
|
||||
if (!bounds.isEmpty()) map.fitBounds(bounds, { padding: 70, duration: reduced() ? 0 : 600, maxZoom: 16 });
|
||||
if (!bounds.isEmpty()) fit(map, bounds, { padding: 70, duration: reduced() ? 0 : 600, maxZoom: 16 });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, journey, routeEnds, lang]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!ready || !map || !focus) return;
|
||||
map.easeTo({ center: lngLat(focus.at), zoom: Math.max(map.getZoom(), 18), duration: reduced() ? 0 : 500 });
|
||||
ease(map, { center: lngLat(focus.at), zoom: Math.max(map.getZoom(), 18), duration: reduced() ? 0 : 500 });
|
||||
}, [ready, focus]);
|
||||
|
||||
// ---- basemap (street map / satellite) ----
|
||||
|
|
@ -355,7 +386,7 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
el.setAttribute('aria-label', t('guide.you'));
|
||||
meMarker.current = new maplibregl.Marker({ element: el }).setLngLat(lngLat(userPos)).addTo(map);
|
||||
} else meMarker.current.setLngLat(lngLat(userPos));
|
||||
if (follow) map.easeTo({ center: lngLat(userPos), zoom: Math.max(map.getZoom(), 17), duration: reduced() ? 0 : 600 });
|
||||
if (follow) ease(map, { center: lngLat(userPos), zoom: Math.max(map.getZoom(), 17), duration: reduced() ? 0 : 600 });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, userPos, follow]);
|
||||
|
||||
|
|
@ -380,7 +411,7 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
if (reach && reach.length) {
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
reach.forEach(([a, b]) => { bounds.extend(lngLat(a)); bounds.extend(lngLat(b)); });
|
||||
map.fitBounds(bounds, { padding: 60, duration: reduced() ? 0 : 500, maxZoom: 17 });
|
||||
fit(map, bounds, { padding: 60, duration: reduced() ? 0 : 500, maxZoom: 17 });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, reach]);
|
||||
|
|
@ -405,7 +436,7 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
|
|||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!ready || !map || !hotspotFocus) return;
|
||||
map.easeTo({ center: lngLat(hotspotFocus.at), zoom: 18, duration: reduced() ? 0 : 500 });
|
||||
ease(map, { center: lngLat(hotspotFocus.at), zoom: 18, duration: reduced() ? 0 : 500 });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, hotspotFocus]);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue