From 1797e1c70d50536ef7c63a8f4fb8ecf7e3068423 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 21 Oct 2026 20:33:12 +0200 Subject: [PATCH] fix(journey): frame the first map view around the entries and the plan, and never hand a removed map the basemap it waited for --- client/src/components/Journey/JourneyMap.tsx | 39 ++++++++++--------- .../src/components/Journey/JourneyMapGL.tsx | 31 +++++++-------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/client/src/components/Journey/JourneyMap.tsx b/client/src/components/Journey/JourneyMap.tsx index 401203451..afedc547c 100644 --- a/client/src/components/Journey/JourneyMap.tsx +++ b/client/src/components/Journey/JourneyMap.tsx @@ -224,8 +224,6 @@ function JourneyMap( const tileLayerRef = useRef(null) // GL layer or the raster stand-in a browser without WebGL gets instead (#2288). const glLayerRef = useRef(null) - // The vector basemap loads async; a map torn down before it lands must not get one. - const cancelledRef = useRef(false) const containerRef = useRef(null) const mapRef = useRef(null) const markersRef = useRef>(new Map()) @@ -239,9 +237,10 @@ function JourneyMap( const markLayerRef = useRef(null) const onMarksPickRef = useRef(onMarksPick) const marksRef = useRef(marks) - // A journey with nothing located of its own opens on the world until its - // plan or stays arrive; those get the first view unless the reader moved it. - const awaitingMarksRef = useRef(false) + // The plan and the stays load after the entries, so the first view is framed + // again once they arrive, around both, unless the reader has moved it by then. + const entryCoordsRef = useRef([]) + const marksFramedRef = useRef(false) const readerMovedRef = useRef(false) // What each switchable layer drew, and which of them are on the map right now. const drawnRef = useRef(NOTHING_DRAWN()) @@ -307,8 +306,11 @@ function JourneyMap( }) L.control.attribution({ position: 'bottomright', prefix: false }).addTo(map) mapRef.current = map - cancelledRef.current = false - awaitingMarksRef.current = false + // The vector basemap loads async; a map torn down before it lands must not get + // one. Per build, not a shared ref: the next build would reset a shared flag and + // hand the old map, already removed, the layer it was still waiting for. + let cancelled = false + marksFramedRef.current = false readerMovedRef.current = false const readerMoved = () => { readerMovedRef.current = true } map.on('dragstart', readerMoved) @@ -317,7 +319,7 @@ function JourneyMap( // The basemap is a vector style unless the user brought their own raster // template, so which layer draws it is decided per template rather than once. if (isVectorStyle(tileUrlRef.current)) { - void attachVectorBasemap(map, tileUrlRef.current, glLayerRef, () => cancelledRef.current) + void attachVectorBasemap(map, tileUrlRef.current, glLayerRef, () => cancelled) } else { const tiles = L.tileLayer(tileUrlRef.current, { maxZoom: 18, @@ -455,13 +457,12 @@ function JourneyMap( map.invalidateSize() // Tracks only get a say when the journey has nothing located of its own — // a world view would be worse than framing the one thing on the map. - const fitCoords = allCoords.length > 0 ? allCoords : trackCoords.length > 0 ? trackCoords : markCoords(marksRef.current) - if (fitCoords.length > 0) { - map.fitBounds(L.latLngBounds(fitCoords), fitOptions(paddingBottom)) - } else { - map.setView([30, 0], 2) - awaitingMarksRef.current = true - } + entryCoordsRef.current = allCoords + const pointCoords = [...allCoords, ...markCoords(marksRef.current)] + const fitCoords = pointCoords.length > 0 ? pointCoords : trackCoords + marksFramedRef.current = (marksRef.current?.length ?? 0) > 0 + if (fitCoords.length > 0) map.fitBounds(L.latLngBounds(fitCoords), fitOptions(paddingBottom)) + else map.setView([30, 0], 2) } catch {} }) @@ -471,7 +472,7 @@ function JourneyMap( const container = containerRef.current return () => { - cancelledRef.current = true + cancelled = true container.removeEventListener('wheel', readerMoved) map.remove() mapRef.current = null @@ -573,9 +574,9 @@ function JourneyMap( } const coords = markCoords(marks) - if (awaitingMarksRef.current && coords.length > 0) { - awaitingMarksRef.current = false - if (!readerMovedRef.current) map.fitBounds(L.latLngBounds(coords), fitOptions(paddingBottom)) + if (!marksFramedRef.current && coords.length > 0) { + marksFramedRef.current = true + if (!readerMovedRef.current) map.fitBounds(L.latLngBounds([...entryCoordsRef.current, ...coords]), fitOptions(paddingBottom)) } draw() diff --git a/client/src/components/Journey/JourneyMapGL.tsx b/client/src/components/Journey/JourneyMapGL.tsx index ebf67f76e..bd0996381 100644 --- a/client/src/components/Journey/JourneyMapGL.tsx +++ b/client/src/components/Journey/JourneyMapGL.tsx @@ -148,8 +148,9 @@ const EMPTY_TRACKS: JourneyTrack[] = [] /** Fallback when a track carries no colour of its own, matching the planner's default. */ const TRACK_FALLBACK_COLOR = '#4f46e5' -function markBounds(gl: Props['gl'], marks: JourneyMapMark[]) { +function markBounds(gl: Props['gl'], marks: JourneyMapMark[], points: [number, number][] = []) { const bounds = new gl.LngLatBounds() + points.forEach(point => bounds.extend(point)) marks.forEach(mark => bounds.extend([mark.lng, mark.lat])) return bounds } @@ -200,9 +201,10 @@ function JourneyMapGL( const markMarkersRef = useRef<{ remove: () => void }[]>([]) const onMarksPickRef = useRef(onMarksPick) const marksRef = useRef(marks) - // As on the Leaflet map: nothing located of its own, so the plan or the - // stays get the first view once they arrive, unless the reader moved it. - const awaitingMarksRef = useRef(false) + // As on the Leaflet map: the plan and the stays arrive after the entries, so + // the first view is framed again around both, unless the reader moved it. + const entryPointsRef = useRef<[number, number][]>([]) + const marksFramedRef = useRef(false) const readerMovedRef = useRef(false) const visibilityRef = useRef(layerVisibility) const shownRef = useRef(layerVisibility) @@ -364,7 +366,8 @@ function JourneyMapGL( const map = new gl.Map(mapOptions as any) mapRef.current = map - awaitingMarksRef.current = false + marksFramedRef.current = false + entryPointsRef.current = [...items.map(i => [i.lng, i.lat] as [number, number]), ...stableTrail.map(p => [p.lng, p.lat] as [number, number])] readerMovedRef.current = false map.on('movestart', (ev: { originalEvent?: unknown }) => { if (ev.originalEvent) readerMovedRef.current = true @@ -496,16 +499,12 @@ function JourneyMapGL( }) // fit bounds to all points - if (hasPoints) { + const known = marksRef.current ?? [] + marksFramedRef.current = known.length > 0 + if (hasPoints || known.length > 0) { try { - map.fitBounds(bounds, fitOptions(paddingBottom, enableMapbox3d && fullScreen)) + map.fitBounds(markBounds(gl, known, entryPointsRef.current), fitOptions(paddingBottom, enableMapbox3d && fullScreen)) } catch { /* empty bounds */ } - } else if (marksRef.current?.length) { - try { - map.fitBounds(markBounds(gl, marksRef.current), fitOptions(paddingBottom, enableMapbox3d && fullScreen)) - } catch { /* empty bounds */ } - } else { - awaitingMarksRef.current = true } }) @@ -597,11 +596,11 @@ function JourneyMapGL( const draw = () => { clear() if (!marks?.length) return - if (awaitingMarksRef.current) { - awaitingMarksRef.current = false + if (!marksFramedRef.current) { + marksFramedRef.current = true if (!readerMovedRef.current) { try { - map.fitBounds(markBounds(gl, marks), fitOptions(paddingBottom, enableMapbox3d && fullScreen)) + map.fitBounds(markBounds(gl, marks, entryPointsRef.current), fitOptions(paddingBottom, enableMapbox3d && fullScreen)) } catch { /* empty bounds */ } } }