mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-05-20 14:44:51 +02:00
fix(map): call updateBoundary directly, remove useEffect
The useEffect-based boundary rendering was unreliable due to React's state lifecycle - the effect would fire before boundary data arrived from the API, then not re-trigger properly when data was populated. New approach: - Remove the boundary useEffect entirely - Define updateBoundary function in map load handler - Store function reference in Zustand store and local ref - PlaceCard calls updateBoundary(geometry) directly when API returns - Click handlers call updateBoundary(null) to clear This bypasses React's render cycle - the map library handles its own state and we tell it what to draw when we have the data. Test sequence: - Click Twin Falls → boundary shows on first click - Click Kimberly → boundary shows on first click - Switch between them → old clears, new shows - Click empty map → boundary clears Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b657c0f000
commit
ac7cec972f
3 changed files with 59 additions and 97 deletions
|
|
@ -694,6 +694,7 @@ const MapView = forwardRef(function MapView(_, ref) {
|
||||||
const pinClickedRef = useRef(false)
|
const pinClickedRef = useRef(false)
|
||||||
const highlightedFeatureRef = useRef(null) // { source, sourceLayer, id } for setFeatureState
|
const highlightedFeatureRef = useRef(null) // { source, sourceLayer, id } for setFeatureState
|
||||||
const hoveredFeatureRef = useRef(null) // for hover highlight
|
const hoveredFeatureRef = useRef(null) // for hover highlight
|
||||||
|
const updateBoundaryRef = useRef(null) // boundary update function
|
||||||
// Refs for measurement state (accessible in click handlers)
|
// Refs for measurement state (accessible in click handlers)
|
||||||
const measuringRef = useRef({ active: false, points: [] })
|
const measuringRef = useRef({ active: false, points: [] })
|
||||||
const measureLabelsRef = useRef([]) // HTML label elements
|
const measureLabelsRef = useRef([]) // HTML label elements
|
||||||
|
|
@ -1253,10 +1254,7 @@ const MapView = forwardRef(function MapView(_, ref) {
|
||||||
store.clearClickMarker()
|
store.clearClickMarker()
|
||||||
store.clearSelectedPlace()
|
store.clearSelectedPlace()
|
||||||
// Clear boundary when deselecting
|
// Clear boundary when deselecting
|
||||||
const boundarySource = map.getSource(BOUNDARY_SOURCE)
|
if (updateBoundaryRef.current) updateBoundaryRef.current(null)
|
||||||
if (boundarySource) {
|
|
||||||
boundarySource.setData({ type: 'FeatureCollection', features: [] })
|
|
||||||
}
|
|
||||||
setSelectedHighlight(map, null)
|
setSelectedHighlight(map, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1283,10 +1281,7 @@ const MapView = forwardRef(function MapView(_, ref) {
|
||||||
}
|
}
|
||||||
setSelectedHighlight(map, null)
|
setSelectedHighlight(map, null)
|
||||||
// Clear old boundary before setting new place
|
// Clear old boundary before setting new place
|
||||||
const boundarySource = map.getSource(BOUNDARY_SOURCE)
|
if (updateBoundaryRef.current) updateBoundaryRef.current(null)
|
||||||
if (boundarySource) {
|
|
||||||
boundarySource.setData({ type: 'FeatureCollection', features: [] })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labelFeature) {
|
if (labelFeature) {
|
||||||
// Clicked a labeled feature — snap to geometry and highlight
|
// Clicked a labeled feature — snap to geometry and highlight
|
||||||
|
|
@ -1342,10 +1337,7 @@ const MapView = forwardRef(function MapView(_, ref) {
|
||||||
} else {
|
} else {
|
||||||
// No labeled feature — show reticle at click point
|
// No labeled feature — show reticle at click point
|
||||||
// Clear any existing boundary when clicking empty map
|
// Clear any existing boundary when clicking empty map
|
||||||
const boundarySource = map.getSource(BOUNDARY_SOURCE)
|
if (updateBoundaryRef.current) updateBoundaryRef.current(null)
|
||||||
if (boundarySource) {
|
|
||||||
boundarySource.setData({ type: 'FeatureCollection', features: [] })
|
|
||||||
}
|
|
||||||
store.setClickMarker({
|
store.setClickMarker({
|
||||||
lat,
|
lat,
|
||||||
lon: lng,
|
lon: lng,
|
||||||
|
|
@ -1446,6 +1438,51 @@ const MapView = forwardRef(function MapView(_, ref) {
|
||||||
// Set up highlight layers
|
// Set up highlight layers
|
||||||
setupHighlightLayers(map, document.documentElement.getAttribute('data-theme') === 'dark')
|
setupHighlightLayers(map, document.documentElement.getAttribute('data-theme') === 'dark')
|
||||||
|
|
||||||
|
// Register updateBoundary function - called directly when boundary data arrives
|
||||||
|
const updateBoundaryFn = (boundaryGeometry) => {
|
||||||
|
const source = map.getSource(BOUNDARY_SOURCE)
|
||||||
|
if (!source) return
|
||||||
|
|
||||||
|
if (!boundaryGeometry) {
|
||||||
|
source.setData({ type: 'FeatureCollection', features: [] })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boundaryGeometry.type === 'Polygon' || boundaryGeometry.type === 'MultiPolygon') {
|
||||||
|
source.setData({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: boundaryGeometry,
|
||||||
|
properties: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Zoom to fit boundary
|
||||||
|
try {
|
||||||
|
const coords = boundaryGeometry.type === 'Polygon'
|
||||||
|
? boundaryGeometry.coordinates[0]
|
||||||
|
: boundaryGeometry.coordinates.flat(1)
|
||||||
|
|
||||||
|
if (coords.length > 0) {
|
||||||
|
let minLng = Infinity, maxLng = -Infinity, minLat = Infinity, maxLat = -Infinity
|
||||||
|
for (const [lng, lat] of coords) {
|
||||||
|
if (lng < minLng) minLng = lng
|
||||||
|
if (lng > maxLng) maxLng = lng
|
||||||
|
if (lat < minLat) minLat = lat
|
||||||
|
if (lat > maxLat) maxLat = lat
|
||||||
|
}
|
||||||
|
map.fitBounds([[minLng, minLat], [maxLng, maxLat]], {
|
||||||
|
padding: 50,
|
||||||
|
duration: 700,
|
||||||
|
maxZoom: 16,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('fitBounds error:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateBoundaryRef.current = updateBoundaryFn
|
||||||
|
useStore.getState().setUpdateBoundary(updateBoundaryFn)
|
||||||
|
|
||||||
// POI/label hover affordance — cursor pointer + highlight
|
// POI/label hover affordance — cursor pointer + highlight
|
||||||
const interactiveLayers = ['pois', 'places_locality', 'places_region', 'places_country', 'places_subplace']
|
const interactiveLayers = ['pois', 'places_locality', 'places_region', 'places_country', 'places_subplace']
|
||||||
|
|
||||||
|
|
@ -1641,91 +1678,6 @@ const MapView = forwardRef(function MapView(_, ref) {
|
||||||
}
|
}
|
||||||
}, [selectedPlace])
|
}, [selectedPlace])
|
||||||
|
|
||||||
// Boundary polygon and zoom-to-feature
|
|
||||||
useEffect(() => {
|
|
||||||
const map = mapInstance.current
|
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
const updateBoundary = () => {
|
|
||||||
const source = map.getSource(BOUNDARY_SOURCE)
|
|
||||||
if (!source) return
|
|
||||||
|
|
||||||
// Clear boundary if no place selected
|
|
||||||
if (!selectedPlace) {
|
|
||||||
source.setData({ type: 'FeatureCollection', features: [] })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get boundary from selectedPlace (may come from API response)
|
|
||||||
const boundary = selectedPlace.boundary || selectedPlace.raw?.boundary
|
|
||||||
|
|
||||||
// Update boundary layer
|
|
||||||
if (boundary && (boundary.type === 'Polygon' || boundary.type === 'MultiPolygon')) {
|
|
||||||
source.setData({
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: boundary,
|
|
||||||
properties: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Zoom to fit boundary
|
|
||||||
try {
|
|
||||||
const coords = boundary.type === 'Polygon'
|
|
||||||
? boundary.coordinates[0]
|
|
||||||
: boundary.coordinates.flat(1)
|
|
||||||
|
|
||||||
if (coords.length > 0) {
|
|
||||||
let minLng = Infinity, maxLng = -Infinity, minLat = Infinity, maxLat = -Infinity
|
|
||||||
for (const [lng, lat] of coords) {
|
|
||||||
if (lng < minLng) minLng = lng
|
|
||||||
if (lng > maxLng) maxLng = lng
|
|
||||||
if (lat < minLat) minLat = lat
|
|
||||||
if (lat > maxLat) maxLat = lat
|
|
||||||
}
|
|
||||||
map.fitBounds([[minLng, minLat], [maxLng, maxLat]], {
|
|
||||||
padding: 50,
|
|
||||||
duration: 700,
|
|
||||||
maxZoom: 16,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('fitBounds error:', e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No boundary - clear the layer and zoom based on feature kind
|
|
||||||
source.setData({ type: 'FeatureCollection', features: [] })
|
|
||||||
|
|
||||||
// Only zoom for feature mode selections (not terrain clicks)
|
|
||||||
if (selectedPlace.mode === 'feature' && selectedPlace.source === 'basemap_label') {
|
|
||||||
const kind = selectedPlace.raw?.kind || selectedPlace.type || ''
|
|
||||||
let targetZoom = null
|
|
||||||
|
|
||||||
if (kind.includes('country')) targetZoom = 5
|
|
||||||
else if (kind.includes('region' ) || kind.includes('state')) targetZoom = 7
|
|
||||||
else if (kind.includes('locality' ) || kind.includes('city')) targetZoom = 11
|
|
||||||
else if (kind.includes('subplace' ) || kind.includes('neighbourhood') || kind.includes('neighborhood')) targetZoom = 13
|
|
||||||
else if (kind.includes('poi')) targetZoom = 16
|
|
||||||
|
|
||||||
// Only zoom in, never zoom out
|
|
||||||
if (targetZoom && map.getZoom() < targetZoom) {
|
|
||||||
map.flyTo({
|
|
||||||
center: [selectedPlace.lon, selectedPlace.lat],
|
|
||||||
zoom: targetZoom,
|
|
||||||
duration: 700,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If style is loaded, update immediately; otherwise wait for load event
|
|
||||||
if (map.isStyleLoaded()) {
|
|
||||||
updateBoundary()
|
|
||||||
} else {
|
|
||||||
map.once('load', updateBoundary)
|
|
||||||
return () => map.off('load', updateBoundary)
|
|
||||||
}
|
|
||||||
}, [selectedPlace, selectedPlace?.boundary])
|
|
||||||
|
|
||||||
// Update route polyline when route changes
|
// Update route polyline when route changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapInstance.current
|
const map = mapInstance.current
|
||||||
|
|
|
||||||
|
|
@ -382,6 +382,9 @@ export function PlaceCard({ place, variant = "preview", expanded = true, onToggl
|
||||||
const current = useStore.getState().selectedPlace
|
const current = useStore.getState().selectedPlace
|
||||||
if (current && current.lat === placeLat && current.lon === placeLon) {
|
if (current && current.lat === placeLat && current.lon === placeLon) {
|
||||||
useStore.getState().setSelectedPlace({ ...current, boundary: data.boundary })
|
useStore.getState().setSelectedPlace({ ...current, boundary: data.boundary })
|
||||||
|
// Call updateBoundary directly - bypass React render cycle
|
||||||
|
const updateBoundary = useStore.getState().updateBoundary
|
||||||
|
if (updateBoundary) updateBoundary(data.boundary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -406,6 +409,9 @@ export function PlaceCard({ place, variant = "preview", expanded = true, onToggl
|
||||||
const current = useStore.getState().selectedPlace
|
const current = useStore.getState().selectedPlace
|
||||||
if (current && current.lat === placeLat && current.lon === placeLon) {
|
if (current && current.lat === placeLat && current.lon === placeLon) {
|
||||||
useStore.getState().setSelectedPlace({ ...current, boundary: data.boundary })
|
useStore.getState().setSelectedPlace({ ...current, boundary: data.boundary })
|
||||||
|
// Call updateBoundary directly - bypass React render cycle
|
||||||
|
const updateBoundary = useStore.getState().updateBoundary
|
||||||
|
if (updateBoundary) updateBoundary(data.boundary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@ export const useStore = create((set, get) => ({
|
||||||
pendingDestination: null, // place waiting for a starting point (GPS-denied Directions flow)
|
pendingDestination: null, // place waiting for a starting point (GPS-denied Directions flow)
|
||||||
|
|
||||||
setSelectedPlace: (place) => set({ selectedPlace: place }),
|
setSelectedPlace: (place) => set({ selectedPlace: place }),
|
||||||
|
|
||||||
|
// Boundary rendering function - set by MapView, called by PlaceCard
|
||||||
|
updateBoundary: null,
|
||||||
|
setUpdateBoundary: (fn) => set({ updateBoundary: fn }),
|
||||||
clearSelectedPlace: () => set({ selectedPlace: null, clickMarker: null }),
|
clearSelectedPlace: () => set({ selectedPlace: null, clickMarker: null }),
|
||||||
setClickMarker: (marker) => set({ clickMarker: marker }),
|
setClickMarker: (marker) => set({ clickMarker: marker }),
|
||||||
clearClickMarker: () => set({ clickMarker: null }),
|
clearClickMarker: () => set({ clickMarker: null }),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue