From 1ad43e58cfaa10bb46df2d8c0434fc6ecec19698 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 2 May 2026 18:45:41 +0000 Subject: [PATCH] fix(map): Unify label/polygon click paths and fix boundary fitBounds Fix A: Label click now also queries polygon layers - When clicking park/forest/cemetery labels, also query landuse_park fill layer to get polygon geometry - If polygon found, use it as boundary directly from rendered tiles - Eliminates need for API round-trip for park boundaries Fix B: Boundary fitBounds behavior changed - If boundary EXISTS: ALWAYS fitBounds (zoom in OR out) to show the full boundary - the boundary defines what user should see - If NO boundary: NEVER change zoom for map clicks/label clicks - Search results: fly to center but preserve current zoom level Removed previous z14 cap and zoom-in-only logic - boundaries now always control the camera as expected. Co-Authored-By: Claude Opus 4.5 --- src/components/MapView.jsx | 52 +++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx index bf61605..6fb76ca 100644 --- a/src/components/MapView.jsx +++ b/src/components/MapView.jsx @@ -2233,6 +2233,25 @@ const MapView = forwardRef(function MapView(_, ref) { featureLat = geom.coordinates[1] } + // FIX A: For park-type features, also query polygon layers to get boundary geometry + const parkKinds = ['national_park', 'park', 'cemetery', 'protected_area', 'nature_reserve', 'forest', 'golf_course', 'wood', 'zoo', 'garden'] + let polygonGeometry = null + if (parkKinds.includes(props.kind)) { + // Query fill layers at the same point to find the polygon + const fillLayers = ['landuse_park', 'landuse_other'].filter(id => map.getLayer(id)) + if (fillLayers.length > 0) { + const fillFeatures = map.queryRenderedFeatures(e.point, { layers: fillLayers }) + // Find a polygon feature with matching name or at the same location + const matchingPolygon = fillFeatures.find(f => + f.properties?.name === props.name || + (f.geometry?.type === 'Polygon' || f.geometry?.type === 'MultiPolygon') + ) + if (matchingPolygon?.geometry) { + polygonGeometry = matchingPolygon.geometry + } + } + } + // Apply feature state highlight const featureId = labelFeature.id ?? props.mvt_id const sourceLayer = labelFeature.sourceLayer @@ -2251,6 +2270,11 @@ const MapView = forwardRef(function MapView(_, ref) { // For feature clicks, don't show pin marker store.clearClickMarker() + // If we found polygon geometry from the fill layer, use it as boundary directly + if (polygonGeometry && updateBoundaryRef.current) { + updateBoundaryRef.current(polygonGeometry) + } + store.setSelectedPlace({ lat: featureLat, lon: featureLon, @@ -2269,6 +2293,7 @@ const MapView = forwardRef(function MapView(_, ref) { kind: props.kind || null, kind_detail: props.kind_detail || null, elevation: props.elevation || null, + polygonGeometry: polygonGeometry || null, // Store polygon if found }, }) } else { @@ -2421,19 +2446,9 @@ const MapView = forwardRef(function MapView(_, ref) { // Validate bounds before fitting if (minLng >= -180 && maxLng <= 180 && minLat >= -90 && maxLat <= 90 && minLng < maxLng && minLat < maxLat) { + // FIX B: ALWAYS fitBounds when boundary exists - zoom in OR out + // The boundary defines what the user should see const bounds = [[minLng, minLat], [maxLng, maxLat]] - const currentZoom = map.getZoom() - const target = map.cameraForBounds(bounds, { padding: 50 }) - - // Zoom-in only: allow zoom in to show boundary, never zoom out - // - target.zoom > currentZoom → zoom IN to fit (always allowed) - // - target.zoom < currentZoom → DON'T zoom out (skip fitBounds) - // - target.zoom == currentZoom → pan only (allowed) - if (!target || target.zoom < currentZoom) { - // Would zoom out — just draw the boundary without moving camera - return - } - map.fitBounds(bounds, { padding: 50, duration: 700, @@ -2638,14 +2653,17 @@ const MapView = forwardRef(function MapView(_, ref) { } else { lastFlyTargetRef.current = placeKey - // Only fly to place if it came from search (not map-click which already centered) + // FIX B: Camera behavior depends on source and whether boundary exists + // - map_click / basemap_label: NO camera movement (boundary fitBounds handles it if exists) + // - search results: fly to center, but DON'T change zoom (user chose their zoom) if (selectedPlace.source !== 'map_click' && selectedPlace.source !== 'basemap_label') { - // Only fly IN if below z14. At z14+ do nothing. + // Search result - fly to center without changing zoom + // Note: if this place has a boundary, the boundary fitBounds will zoom appropriately const currentZoom = map.getZoom() - if (currentZoom < 14) { - map.flyTo({ center: [selectedPlace.lon, selectedPlace.lat], zoom: 14, duration: 800 }) - } + map.flyTo({ center: [selectedPlace.lon, selectedPlace.lat], zoom: currentZoom, duration: 800 }) } + // For map_click and basemap_label: do nothing to camera + // The boundary fitBounds will handle zooming if a boundary is fetched } // Different visual feedback based on mode