From e0ea993fa4776a568b44f460b353fd48eb4c353d Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Mon, 11 May 2026 15:54:48 -0400 Subject: [PATCH 1/9] CLIM-1322 (4): BEGIN From 94b2f940d995b730a59c9e2b75686db7c0d1672d Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Thu, 30 Apr 2026 22:24:42 -0400 Subject: [PATCH 2/9] CLIM-1322 (2): Atom1 Alias province_fr as province MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function selected "province_fr" (or "province") via $term_append but the result row was always read as $result['province']. In FR this produced "Undefined array key" warnings and a null province_short, which in turn yielded titles like "Sainte-Adèle, " (trailing comma). Aliases the column back to the "province" key, matching the pattern already used by cdc_get_location_by_id() at the same file. --- fw-child/resources/functions/rest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fw-child/resources/functions/rest.php b/fw-child/resources/functions/rest.php index afd6ca522..991c95dfa 100644 --- a/fw-child/resources/functions/rest.php +++ b/fw-child/resources/functions/rest.php @@ -367,7 +367,7 @@ function cdc_get_location_by_coords () { "geo_name", "gen_term" . $term_append . " as generic_term", "location", - "province" . $term_append, + "province" . $term_append . " as province", "lat", "lon" ); From 9790fffb66460450fa5117b7db6446ca2cfe5df9 Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Wed, 15 Apr 2026 11:32:40 -0400 Subject: [PATCH 3/9] CLIM-1322 (2): Atom3 .title for Popup not .geo_id In the locationNotFound branch (raw lat,lng paste), showLocation()'s title argument was the opaque 5-char geo_id (e.g. "EPYTF", "EJK8O"), producing a popup title users could not read. The fetched response already carries a human-readable title field built server-side; pass that instead. --- apps/src/components/map-layers/search-control.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/src/components/map-layers/search-control.tsx b/apps/src/components/map-layers/search-control.tsx index 087262d48..7949993d1 100644 --- a/apps/src/components/map-layers/search-control.tsx +++ b/apps/src/components/map-layers/search-control.tsx @@ -235,7 +235,7 @@ const SearchControl = ({ // Fetch location data const locationByCoords = await fetchLocationByCoords({ lat: latLng.lat, lng: latLng.lon }); // Trigger show location. - this.showLocation(locationByCoords, locationByCoords.geo_id); + this.showLocation(locationByCoords, locationByCoords.title); } else { // Show error alert. From 851abd9928eb84e9672d7ab1cf7cf4f9a71713c3 Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Tue, 5 May 2026 20:22:18 -0400 Subject: [PATCH 4/9] CLIM-1322 (3): Atom14 Use autocomplete row for gridded popup title --- apps/src/components/map-container.tsx | 10 ++- .../components/map-layers/search-control.tsx | 68 ++++++++++++++++++- apps/src/components/map.tsx | 4 +- apps/src/hooks/use-map-interactions.tsx | 20 ++++++ 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/apps/src/components/map-container.tsx b/apps/src/components/map-container.tsx index f454e3deb..24fd4c26e 100644 --- a/apps/src/components/map-container.tsx +++ b/apps/src/components/map-container.tsx @@ -60,6 +60,9 @@ interface MapContainerProps { onClick: (e: { latlng: L.LatLng; layer: { properties: unknown } }) => void; selectedLocation: SelectedLocationInfo | null; clearSelectedLocation: () => void; + selectGriddedLocation?: ( + input: { latlng: L.LatLng; title: string }, + ) => void; // @ts-expect-error: L.VectorGrid is a valid type layerRef?: React.MutableRefObject; } @@ -76,6 +79,7 @@ export default function MapContainer({ onClick, selectedLocation, clearSelectedLocation, + selectGriddedLocation, layerRef, }: MapContainerProps): React.ReactElement { const [locationModalContent, setLocationModalContent] = useState(null); @@ -245,7 +249,11 @@ export default function MapContainer({ {/* Show search control if not a comparison map. */} - { !isComparisonMap && } + { !isComparisonMap && ( + + ) } void; } /** @@ -71,7 +82,16 @@ export interface SearchControlProps { */ const SearchControl = ({ className, + onSelectGriddedLocation, }: SearchControlProps): ReactElement => { + const { climateVariable } = useClimateVariable(); + // Captured at the point formatData() builds entries, keyed by the same + // title string leaflet-search later passes back to moveToLocation(). + // A closure-scoped Map keeps lookup local to the search lifecycle + // and avoids any cross-render leakage. + const formattedItemsRef = useRef>( + new Map(), + ); const [isGeolocationEnabled, setIsGeolocationEnabled] = useState(false); const [isTracking, setIsTracking] = useState(false); @@ -95,7 +115,10 @@ const SearchControl = ({ const map = useMap(); const handleLocationChange = useCallback( - async (inputLatlng: SearchLatLng) => { + async ( + inputLatlng: SearchLatLng, + autocompleteItem?: { item: SearchControlLocationItem; title: string }, + ) => { const latlng = convertSearchLatLng(inputLatlng); // clear all existing markers from the map map.eachLayer(layer => { @@ -104,10 +127,33 @@ const SearchControl = ({ } }); map.setView(latlng, SEARCH_DEFAULT_ZOOM); + + const interactiveRegion = climateVariable?.getInteractiveRegion() + ?? InteractiveRegionOption.GRIDDED_DATA; + + // UC-Search in GRIDDED_DATA: the autocomplete row already has a + // usable title — set the selected location directly. Skip the + // synthetic-click reverse-lookup path that the other modes need. + if ( + autocompleteItem + && interactiveRegion === InteractiveRegionOption.GRIDDED_DATA + && onSelectGriddedLocation + ) { + onSelectGriddedLocation({ + latlng, + title: autocompleteItem.title, + }); + return; + } + + // UC-LocateMe, UC-RawCoordPaste, and all polygon modes: + // fall through to existing behaviour from main. await dispatchMapClick(map, latlng); }, [ + climateVariable, map, + onSelectGriddedLocation, ], ); @@ -205,6 +251,11 @@ const SearchControl = ({ SearchControlLocationItem & { loc: number[]; lng: string; } > = {}; + // Reset and rebuild the title→item index for the current + // suggestion set, so moveToLocation can look the row up by + // title without round-tripping through the server. + formattedItemsRef.current.clear(); + response.items.forEach((item: SearchControlLocationItem) => { const title = buildLocationTitle(item); const loc = [parseFloat(item.lat), parseFloat(item.lon)]; @@ -213,6 +264,7 @@ const SearchControl = ({ lng: item.lon, loc, }; + formattedItemsRef.current.set(title, item); }); return formattedData; @@ -250,8 +302,18 @@ const SearchControl = ({ popupAnchor: [0, -41], // Popup position relative to the icon }), }), - moveToLocation: (latlng: SearchLatLng) => { - handleLocationChange(latlng); + moveToLocation: (latlng: SearchLatLng, title: string) => { + // leaflet-search invokes this with (latlng, title, map); + // `title` is the same key formatData() used in formattedData, + // so we can recover the original autocomplete row from it. + const item = formattedItemsRef.current.get(title); + if (item) { + handleLocationChange(latlng, { item, title }); + } else { + // No matching row (e.g. raw lat/lon paste branch from + // locationNotFound) — fall through to existing behaviour. + handleLocationChange(latlng); + } }, }); diff --git a/apps/src/components/map.tsx b/apps/src/components/map.tsx index ba37a4515..378b3d75c 100644 --- a/apps/src/components/map.tsx +++ b/apps/src/components/map.tsx @@ -33,7 +33,8 @@ export default function Map(): React.ReactElement { handleOver, handleOut, handleClick, - handleClearSelectedLocation + handleClearSelectedLocation, + selectGriddedLocation, } = useMapInteractions({ primaryLayerRef, comparisonLayerRef, @@ -95,6 +96,7 @@ export default function Map(): React.ReactElement { onClick={handleClick} selectedLocation={selectedLocation} clearSelectedLocation={handleClearSelectedLocation} + selectGriddedLocation={selectGriddedLocation} layerRef={primaryLayerRef} /> {showComparisonMap && ( diff --git a/apps/src/hooks/use-map-interactions.tsx b/apps/src/hooks/use-map-interactions.tsx index 34e9d819c..54dbd57ca 100644 --- a/apps/src/hooks/use-map-interactions.tsx +++ b/apps/src/hooks/use-map-interactions.tsx @@ -103,6 +103,25 @@ export function useMapInteractions({ primaryLayerRef, comparisonLayerRef }: UseM clearMarkers(); }, [clearMarkers]); + // Used by the search control's autocomplete branch in GRIDDED_DATA mode: + // the autocomplete row already carries a usable display title, so we set + // the selected location directly without a reverse-coords lookup. + const selectGriddedLocation = useCallback(( + { latlng, title }: { latlng: L.LatLng; title: string }, + ) => { + clearMarkers(); + addMarker(latlng, title); + + dispatch(addRecentLocation({ + id: `${latlng.lat}|${latlng.lng}`, + title, + lat: latlng.lat, + lng: latlng.lng, + })); + + setSelectedLocation({ featureId: 0, title, latlng }); + }, [clearMarkers, addMarker, dispatch]); + // Effect to handle location updates when climate variables change useEffect(() => { const interactiveRegion = climateVariable?.getInteractiveRegion() ?? null; @@ -140,5 +159,6 @@ export function useMapInteractions({ primaryLayerRef, comparisonLayerRef }: UseM handleOut, handleClick, handleClearSelectedLocation, + selectGriddedLocation, }; } From d50d085b31107063167930e876846b1efb96de8b Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Wed, 6 May 2026 03:02:48 -0400 Subject: [PATCH 5/9] CLIM-1322 (3): Atom12 Use short-form title for UC-Search popup; expose province_short in autocomplete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Atom 9-equivalent's buildLocationTitle() produced the verbose autocomplete dropdown display ("Montréal, (Ville), Montréal; Montréal, Québec") in the popup title; make UC-Search's popup match the server-resolved short shape ("Montréal, QC") that cdc_get_location_by_coords already returns. Server-side cdc_location_search() now adds province_short to each item, derived via the existing short_province() helper from fw-child/functions.php (already used by cdc_get_location_by_coords and cdc_get_location_by_id), so the client doesn't need its own province-name mapping. buildLocationTitle() is unchanged — leaflet-search still uses it as the dropdown display key. --- apps/src/components/map-layers/search-control.tsx | 10 ++++++++-- apps/src/types/types.ts | 6 ++++++ fw-child/resources/functions/rest.php | 6 ++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/src/components/map-layers/search-control.tsx b/apps/src/components/map-layers/search-control.tsx index f368adbd1..a31c72337 100644 --- a/apps/src/components/map-layers/search-control.tsx +++ b/apps/src/components/map-layers/search-control.tsx @@ -305,10 +305,16 @@ const SearchControl = ({ moveToLocation: (latlng: SearchLatLng, title: string) => { // leaflet-search invokes this with (latlng, title, map); // `title` is the same key formatData() used in formattedData, - // so we can recover the original autocomplete row from it. + // (the verbose buildLocationTitle output, kept for the + // dropdown display), so we can recover the original + // autocomplete row from it. const item = formattedItemsRef.current.get(title); if (item) { - handleLocationChange(latlng, { item, title }); + // UC-Search popup title uses the short server-resolved + // shape ("Montréal, QC"), matching cdc_get_location_by_coords, + // not the verbose dropdown display. + const popupTitle = `${item.text}, ${item.province_short}`; + handleLocationChange(latlng, { item, title: popupTitle }); } else { // No matching row (e.g. raw lat/lon paste branch from // locationNotFound) — fall through to existing behaviour. diff --git a/apps/src/types/types.ts b/apps/src/types/types.ts index 60b4c7155..4c06a2c47 100644 --- a/apps/src/types/types.ts +++ b/apps/src/types/types.ts @@ -519,6 +519,12 @@ export interface SearchControlLocationItem { term: string; location: string; province: string; + /** + * 2-letter province code derived server-side via short_province(). + * @example 'QC', 'NS', 'BC' — pairs with `text` to form the popup + * title shape used by cdc_get_location_by_coords ("Montréal, QC"). + */ + province_short: string; lat: string; lon: string; } diff --git a/fw-child/resources/functions/rest.php b/fw-child/resources/functions/rest.php index 991c95dfa..e8c146479 100644 --- a/fw-child/resources/functions/rest.php +++ b/fw-child/resources/functions/rest.php @@ -589,6 +589,11 @@ function cdc_location_search() { $response['items'] = array(); // finish getting rows from the main query + // preserve existing `{id, text, term, location, province, lat, lon} response shape; + // `province_short` is additive, derived via the same `short_province()` + // helper used by cdc_get_location_by_coords/_by_id so the client can + // render the same "geo_name, XX" shape as those endpoints without + // duplicating the province-name → 2-letter mapping in JS. while ( $row = mysqli_fetch_row ( $main_query ) ) { $row = array ( "id" => $row[0], @@ -596,6 +601,7 @@ function cdc_location_search() { "term" => $row[2], "location" => $row[3], "province" => $row[4], + "province_short" => short_province ( $row[4] ), "lat" => $row[5], "lon" => $row[6] ); From 1a716dd16c5e8c22458f98d1d5dc57bc4d54335b Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Tue, 12 May 2026 02:11:38 -0400 Subject: [PATCH 6/9] CLIM-1322 (4): Atom15 dispatchMapClick is only about dispatching DOM PointerEvent AFTER setView --- apps/src/components/map-layers/search-control.tsx | 7 +++---- .../sidebar-footer-links/recent-locations.tsx | 2 +- apps/src/lib/dispatch-map-click.ts | 12 +++++------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/src/components/map-layers/search-control.tsx b/apps/src/components/map-layers/search-control.tsx index a31c72337..fcb695731 100644 --- a/apps/src/components/map-layers/search-control.tsx +++ b/apps/src/components/map-layers/search-control.tsx @@ -131,9 +131,7 @@ const SearchControl = ({ const interactiveRegion = climateVariable?.getInteractiveRegion() ?? InteractiveRegionOption.GRIDDED_DATA; - // UC-Search in GRIDDED_DATA: the autocomplete row already has a - // usable title — set the selected location directly. Skip the - // synthetic-click reverse-lookup path that the other modes need. + // The autocomplete row already has a usable title — set the selected location directly. if ( autocompleteItem && interactiveRegion === InteractiveRegionOption.GRIDDED_DATA @@ -143,12 +141,13 @@ const SearchControl = ({ latlng, title: autocompleteItem.title, }); + // Skip the synthetic-click reverse-lookup path that the other modes need. return; } // UC-LocateMe, UC-RawCoordPaste, and all polygon modes: // fall through to existing behaviour from main. - await dispatchMapClick(map, latlng); + await dispatchMapClick(map); }, [ climateVariable, diff --git a/apps/src/components/sidebar-footer-links/recent-locations.tsx b/apps/src/components/sidebar-footer-links/recent-locations.tsx index 966501c5b..aebe1f1ee 100644 --- a/apps/src/components/sidebar-footer-links/recent-locations.tsx +++ b/apps/src/components/sidebar-footer-links/recent-locations.tsx @@ -62,7 +62,7 @@ const RecentLocationsPanel: React.FC = () => { const moveToLocation = async (location: MapLocation) => { map.setView(location, SEARCH_DEFAULT_ZOOM); - await dispatchMapClick(map, location); + await dispatchMapClick(map); }; return ( diff --git a/apps/src/lib/dispatch-map-click.ts b/apps/src/lib/dispatch-map-click.ts index 090247f97..b260efc45 100644 --- a/apps/src/lib/dispatch-map-click.ts +++ b/apps/src/lib/dispatch-map-click.ts @@ -4,20 +4,13 @@ import L from 'leaflet'; * Dispatches a real DOM PointerEvent on the VectorGrid canvas tile at the * map container center after the view settles. * - * `pointer-events: none` on Leaflet's pane wrapper divs causes - * `document.elementFromPoint` to return the outermost container — canvas - * tiles are queried via `gridPane.querySelectorAll('canvas')` and filtered - * by bounding rect instead. - * * `Promise.race` with a 1,000 ms timeout guards the case where `setView` * fires `moveend` synchronously (short pan, no animation) before the * listener is attached. */ export const dispatchMapClick = async ( map: L.Map, - latlng: { lat: number; lng: number }, ): Promise => { - void latlng; await Promise.race([ new Promise((resolve) => map.once('moveend', () => resolve())), new Promise((resolve) => setTimeout(resolve, 1_000)), @@ -31,6 +24,11 @@ export const dispatchMapClick = async ( if (!gridPane) { return; } + // `pointer-events: none` on Leaflet's pane wrapper divs causes + // `document.elementFromPoint` to return the outermost container — the + // canvas tiles themselves are not hit-testable through that API. We + // enumerate the grid pane's canvases and filter by their bounding rect + // to find the one under (clientX, clientY) instead. const canvases = Array.from(gridPane.querySelectorAll('canvas')); const target = canvases.find((canvas) => { const r = canvas.getBoundingClientRect(); From f9bcbaffa2a16f97e451d579582c94571dd48bd9 Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Tue, 12 May 2026 02:51:50 -0400 Subject: [PATCH 7/9] CLIM-1322 (4): Comments to add clarity --- .../components/map-layers/search-control.tsx | 22 ++++++++++++++----- apps/src/services/services.ts | 2 ++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/src/components/map-layers/search-control.tsx b/apps/src/components/map-layers/search-control.tsx index fcb695731..dfca5b766 100644 --- a/apps/src/components/map-layers/search-control.tsx +++ b/apps/src/components/map-layers/search-control.tsx @@ -280,13 +280,23 @@ const SearchControl = ({ return; } // Check if the coordinates are valid if the location is empty. - const latLng = parseLatLon(this._input.value); + const latLon = parseLatLon(this._input.value); // If the coordinates are valid, move to that location. - if (latLng && !latLng.isPartial) { - // Fetch location data - const locationByCoords = await fetchLocationByCoords({ lat: latLng.lat, lng: latLng.lon }); - // Trigger show location. - this.showLocation(locationByCoords, locationByCoords.title); + if (latLon && !latLon.isPartial) { + const latLng = { + lat: latLon.lat, + lng: latLon.lon, + } + const locationByCoords = await fetchLocationByCoords(latLng); + const locationAtTypedCoords = { + ...locationByCoords, + lat: latLng.lat, + lng: latLng.lng, + }; + this.showLocation( + locationAtTypedCoords, + locationByCoords.title, + ); } else { // Show error alert. diff --git a/apps/src/services/services.ts b/apps/src/services/services.ts index 71f14f607..3adae3dd8 100644 --- a/apps/src/services/services.ts +++ b/apps/src/services/services.ts @@ -455,6 +455,8 @@ export const fetchPostsData = async ( /** * Fetches location data from the API * + * Call to WordPress wp-api endpoint `/wp-json/cdc/v2/get_location_by_coords` + * * @param latlng Latitude and Longitude of the location * @param fetchOptions Any other options to pass to fetch (ex: `signal`) */ From 3b6bc0b7daf7a988ef23474d634fe7786f2d1c83 Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Tue, 12 May 2026 16:20:11 -0400 Subject: [PATCH 8/9] CLIM-1322 (4): Atom16 UC-RawCoordPaste GRIDDED pin precision (DoD-2) Empirical drift in UC-RawCoordPaste was traced to leaflet-search's showLocation synchronously invoking moveToLocation -> handleLocationChange -> dispatchMapClick (container-center synthetic click) -> drifted ~80m re-projection in handleClick's addMarker. This commit bypasses that chain in GRIDDED mode by calling setView + onSelectGriddedLocation directly with the typed lat/lng, mirroring the same-intent-same-pattern path UC-Search GRIDDED already uses. Polygon modes (CENSUS/HEALTH/WATERSHED) preserve the existing showLocation path because their popup title comes from layer.properties.label_* via handleClick's override and the ~80m drift is benign at polygon scale. --- .../components/map-layers/search-control.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/src/components/map-layers/search-control.tsx b/apps/src/components/map-layers/search-control.tsx index dfca5b766..7638613e8 100644 --- a/apps/src/components/map-layers/search-control.tsx +++ b/apps/src/components/map-layers/search-control.tsx @@ -288,6 +288,28 @@ const SearchControl = ({ lng: latLon.lon, } const locationByCoords = await fetchLocationByCoords(latLng); + + // Bypass leaflet-search's showLocation pipeline for GRIDDED mode so the + // marker lands at the typed coordinates rather than the synthetic-click + // container-center reprojection. Polygon modes (CENSUS/HEALTH/WATERSHED) + // fall through to the existing showLocation path because their popup + // title comes from layer.properties.label_* and the small drift is + // benign at polygon scale. + const interactiveRegion = climateVariable?.getInteractiveRegion() + ?? InteractiveRegionOption.GRIDDED_DATA; + + if ( + interactiveRegion === InteractiveRegionOption.GRIDDED_DATA + && onSelectGriddedLocation + ) { + map.setView(latLng, SEARCH_DEFAULT_ZOOM); + onSelectGriddedLocation({ + latlng: L.latLng(latLng.lat, latLng.lng), + title: locationByCoords.title, + }); + return; + } + const locationAtTypedCoords = { ...locationByCoords, lat: latLng.lat, From d5f0fd35cba4eb79345f50b10ef600a0a7258391 Mon Sep 17 00:00:00 2001 From: Renoir Boulanger Date: Wed, 13 May 2026 08:19:51 -0400 Subject: [PATCH 9/9] CLIM-1322 (4): Comments to add clarity --- .../components/map-layers/search-control.tsx | 42 +++++++++-------- apps/src/hooks/use-map-interactions.tsx | 46 +++++++++++++------ 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/apps/src/components/map-layers/search-control.tsx b/apps/src/components/map-layers/search-control.tsx index 7638613e8..db17e9dc6 100644 --- a/apps/src/components/map-layers/search-control.tsx +++ b/apps/src/components/map-layers/search-control.tsx @@ -272,6 +272,13 @@ const SearchControl = ({ void _; // intentionally ignore to suppress typescript error return `
${buildLocationTitle(item)}
`; }, + + /** + * Search by raw "lat,lon" coordinates or anything else entry point. + * + * The autocomplete-hit path (UC-Search) never reaches here; it lands + * in `moveToLocation` above with a matching row in formattedItemsRef. + */ locationNotFound: async function() { // If the list of suggestions is still shown, no error message is shown. // See #86862 @@ -279,21 +286,21 @@ const SearchControl = ({ this.showTooltip(this._recordsCache) return; } - // Check if the coordinates are valid if the location is empty. - const latLon = parseLatLon(this._input.value); + + // parseLatLon is both PARSER and DETECTOR: a non-partial result means + // the input was a valid "lat, lon" string. + const parsedSearchControlInput = parseLatLon(this._input.value); // If the coordinates are valid, move to that location. - if (latLon && !latLon.isPartial) { - const latLng = { - lat: latLon.lat, - lng: latLon.lon, - } + if (parsedSearchControlInput && !parsedSearchControlInput.isPartial) { + const latLng = new L.LatLng(parsedSearchControlInput.lat, parsedSearchControlInput.lon); const locationByCoords = await fetchLocationByCoords(latLng); // Bypass leaflet-search's showLocation pipeline for GRIDDED mode so the - // marker lands at the typed coordinates rather than the synthetic-click - // container-center reprojection. Polygon modes (CENSUS/HEALTH/WATERSHED) - // fall through to the existing showLocation path because their popup - // title comes from layer.properties.label_* and the small drift is + // marker lands at the typed coordinates rather than dispatchMapClick + // calculated center (container-center reprojection). + // Other `InteractiveRegionOption.` modes (CENSUS/HEALTH/WATERSHED) + // fall through because their popup title comes from + // `layer.properties.label_*` and the small drift is // benign at polygon scale. const interactiveRegion = climateVariable?.getInteractiveRegion() ?? InteractiveRegionOption.GRIDDED_DATA; @@ -304,19 +311,18 @@ const SearchControl = ({ ) { map.setView(latLng, SEARCH_DEFAULT_ZOOM); onSelectGriddedLocation({ - latlng: L.latLng(latLng.lat, latLng.lng), + latlng: latLng, title: locationByCoords.title, }); return; } - const locationAtTypedCoords = { - ...locationByCoords, - lat: latLng.lat, - lng: latLng.lng, - }; this.showLocation( - locationAtTypedCoords, + { + ...locationByCoords, + lat: latLng.lat, + lng: latLng.lng, + }, locationByCoords.title, ); } diff --git a/apps/src/hooks/use-map-interactions.tsx b/apps/src/hooks/use-map-interactions.tsx index 54dbd57ca..35471ea95 100644 --- a/apps/src/hooks/use-map-interactions.tsx +++ b/apps/src/hooks/use-map-interactions.tsx @@ -95,17 +95,24 @@ export function useMapInteractions({ primaryLayerRef, comparisonLayerRef }: UseM ...latlng, })); - setSelectedLocation({ featureId: featureId ?? 0, title: locationTitle, latlng }); - }, [clearMarkers, addMarker, dispatch, climateVariable, locale]); - - const handleClearSelectedLocation = useCallback(() => { - setSelectedLocation(null); - clearMarkers(); - }, [clearMarkers]); - - // Used by the search control's autocomplete branch in GRIDDED_DATA mode: - // the autocomplete row already carries a usable display title, so we set - // the selected location directly without a reverse-coords lookup. + setSelectedLocation({ + featureId: featureId ?? 0, + latlng, + title: locationTitle, + }); + }, [ + addMarker, + clearMarkers, + climateVariable, + dispatch, + locale, + ]); + + /** + * Used by the `search-control.tsx`'s autocomplete branch in GRIDDED_DATA mode: + * the autocomplete row already carries a usable display title, so we set + * the selected location directly without a reverse-coords lookup. + */ const selectGriddedLocation = useCallback(( { latlng, title }: { latlng: L.LatLng; title: string }, ) => { @@ -119,8 +126,21 @@ export function useMapInteractions({ primaryLayerRef, comparisonLayerRef }: UseM lng: latlng.lng, })); - setSelectedLocation({ featureId: 0, title, latlng }); - }, [clearMarkers, addMarker, dispatch]); + setSelectedLocation({ + featureId: 0, + latlng, + title, + }); + }, [ + addMarker, + clearMarkers, + dispatch, + ]); + + const handleClearSelectedLocation = useCallback(() => { + setSelectedLocation(null); + clearMarkers(); + }, [clearMarkers]); // Effect to handle location updates when climate variables change useEffect(() => {