From a1cb1ecc25208ce0e00c8e981869bffd67860a4b Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 4 Feb 2026 14:23:28 +0100 Subject: [PATCH 1/4] fix MlWmsLoader reinitialization on prop change bug --- .../MlWmsLoader/MlWmsLoader.stories.tsx | 97 ++++++++++ .../components/MlWmsLoader/MlWmsLoader.tsx | 179 +++++++++++------- 2 files changed, 211 insertions(+), 65 deletions(-) diff --git a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.stories.tsx b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.stories.tsx index c6be954f..19eb0a5e 100644 --- a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.stories.tsx +++ b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.stories.tsx @@ -166,3 +166,100 @@ const FixedConfigTemplate: any = () => { export const ExampleFixedConfig = FixedConfigTemplate.bind({}); ExampleFixedConfig.parameters = {}; ExampleFixedConfig.args = {}; + +const CustomFeatureInfoTemplate: any = () => { + const [url] = useState('https://www.wms.nrw.de/geobasis/wms_nw_vdop'); + const [openSidebar, setOpenSidebar] = useState(true); + const [featureInfoActive, setFeatureInfoActive] = useState(true); + const [featureInfoData, setFeatureInfoData] = useState<{ + content: string; + lngLat: { lng: number; lat: number }; + } | null>(null); + const mapHook = useMap({ + mapId: undefined, + }); + + const initializedRef = useRef(false); + + useEffect(() => { + if (!mapHook.map || initializedRef.current) return; + + initializedRef.current = true; + mapHook.map.map.flyTo({ center: [2.3522, 48.8566], zoom: 12 }); + }, [mapHook.map]); + + const handleFeatureInfoSuccess = (content: string, lngLat: { lng: number; lat: number }) => { + setFeatureInfoData({ content, lngLat }); + }; + + return ( + <> + + + + } + /> + + + + console.log(config)} + zoomToExtent={true} + layerId="WMS-layer" + featureInfoActive={featureInfoActive} + setFeatureInfoActive={setFeatureInfoActive} + featureInfoMarkerEnabled={false} + featureInfoSuccess={handleFeatureInfoSuccess} + /> + + + {featureInfoData && ( + <> + + + + + + + + + )} + + + ); +}; + +export const CustomFeatureInfoDisplay = CustomFeatureInfoTemplate.bind({}); +CustomFeatureInfoDisplay.parameters = {}; +CustomFeatureInfoDisplay.args = {}; diff --git a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx index 232c3142..004e71bc 100644 --- a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx +++ b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx @@ -9,7 +9,7 @@ import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import IconButton from '@mui/material/IconButton'; -import { LngLat, MapMouseEvent } from 'maplibre-gl'; +import { MapMouseEvent } from 'maplibre-gl'; import useMap from '../../hooks/useMap'; import { Box, Checkbox, ListItemIcon, Snackbar } from '@mui/material'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; @@ -21,20 +21,6 @@ import * as turf from '@turf/turf'; import SortableContainer from '../../ui_components/LayerList/util/SortableContainer'; import { normalizeWmsParams } from '../../utils/wmsUtils'; -const originShift = (2 * Math.PI * 6378137) / 2.0; -const lngLatToMeters = function (lnglat: LngLat, accuracy = { enable: true, decimal: 1 }) { - const lng = lnglat.lng; - const lat = lnglat.lat; - let x = (lng * originShift) / 180.0; - let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360.0)) / (Math.PI / 180.0); - y = (y * originShift) / 180.0; - if (accuracy.enable) { - x = Number(x.toFixed(accuracy.decimal)); - y = Number(y.toFixed(accuracy.decimal)); - } - return [x, y]; -}; - export interface WmsConfig { /** * The URL to use for the getFeatureInfo request @@ -186,32 +172,37 @@ export type LayerType = { const defaultProps = { mapId: undefined, url: '', - baseUrlParameters: { - SERVICE: 'WMS', - VERSION: '1.3.0', - }, - getCapabilitiesUrlParameters: {}, - getMapUrlParameters: { - TRANSPARENT: 'TRUE', - }, - getFeatureInfoUrlParameters: { - FEATURE_COUNT: '10', - STYLES: '', - WIDTH: '100', - HEIGHT: '100', - SRS: 'EPSG:3857', - CRS: 'EPSG:3857', - X: '50', - Y: '50', - I: '50', - J: '50', - }, featureInfoEnabled: true, featureInfoMarkerEnabled: true, zoomToExtent: false, showDeleteButton: false, showLayerList: true, }; + +const defaultBaseUrlParameters = { + SERVICE: 'WMS', + VERSION: '1.3.0', +}; + +const defaultGetCapabilitiesUrlParameters = {}; + +const defaultGetMapUrlParameters = { + TRANSPARENT: 'TRUE', +}; + +const defaultGetFeatureInfoUrlParameters = { + FEATURE_COUNT: '10', + STYLES: '', + WIDTH: '100', + HEIGHT: '100', + SRS: 'EPSG:3857', + CRS: 'EPSG:3857', + X: '50', + Y: '50', + I: '50', + J: '50', +}; + /** * Loads a WMS getCapabilities xml document and adds a MlWmsLayer component for each layer that is * offered by the WMS. @@ -219,15 +210,44 @@ const defaultProps = { * @component */ const MlWmsLoader = (props: MlWmsLoaderProps) => { - props = { ...defaultProps, ...props }; + // Merge defaults with props using useMemo for stable references + // The dependencies are the prop objects - if consumers pass inline objects, + // they should memoize them. This follows standard React patterns. + const baseUrlParameters = useMemo( + () => ({ ...defaultBaseUrlParameters, ...props.baseUrlParameters }), + [props.baseUrlParameters] + ); + const getCapabilitiesUrlParameters = useMemo( + () => ({ ...defaultGetCapabilitiesUrlParameters, ...props.getCapabilitiesUrlParameters }), + [props.getCapabilitiesUrlParameters] + ); + const getMapUrlParameters = useMemo( + () => ({ ...defaultGetMapUrlParameters, ...props.getMapUrlParameters }), + [props.getMapUrlParameters] + ); + const getFeatureInfoUrlParameters = useMemo( + () => ({ ...defaultGetFeatureInfoUrlParameters, ...props.getFeatureInfoUrlParameters }), + [props.getFeatureInfoUrlParameters] + ); + + // Apply simple defaults via destructuring + const { + mapId = defaultProps.mapId, + url = defaultProps.url, + featureInfoEnabled = defaultProps.featureInfoEnabled, + featureInfoMarkerEnabled = defaultProps.featureInfoMarkerEnabled, + zoomToExtent = defaultProps.zoomToExtent, + showDeleteButton = defaultProps.showDeleteButton, + showLayerList = defaultProps.showLayerList, + } = props; const capabilitiesUrlParameters = useMemo( () => ({ - ...props.baseUrlParameters, - ...props.getCapabilitiesUrlParameters, + ...baseUrlParameters, + ...getCapabilitiesUrlParameters, REQUEST: 'GetCapabilities', }), - [props.baseUrlParameters, props.getCapabilitiesUrlParameters] + [baseUrlParameters, getCapabilitiesUrlParameters] ); const { @@ -244,7 +264,7 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { const [visible, setVisible] = useState(props?.config?.visible || true); const [showDeletionConfirmationDialog, setShowDeletionConfirmationDialog] = useState(false); - const mapHook = useMap({ mapId: props?.mapId }); + const mapHook = useMap({ mapId }); const [_layers, _setLayers] = useState>(props?.config?.layers || []); const [featureInfoEventsEnabled, setFeatureInfoEventsEnabled] = useState(false); @@ -315,13 +335,50 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { (ev: MapMouseEvent & unknown) => { if (!mapHook.map) return; resetFeatureInfo(); + + // First, determine what CRS/SRS will be used in the final request + let _gfiUrl: string | undefined = getFeatureInfoUrl; + let _gfiUrlParts; + if (_gfiUrl?.indexOf?.('?') !== -1) { + _gfiUrlParts = props?.url?.split('?') || props?.config?.wmsUrl?.split('?'); + _gfiUrl = _gfiUrlParts?.[0]; + } + const _urlParamsFromUrl = new URLSearchParams(_gfiUrlParts?.[1]); + + // Build a temporary params object to determine the effective CRS + const tempParams = { + ...normalizeWmsParams(defaultBaseUrlParameters), + ...normalizeWmsParams(defaultGetFeatureInfoUrlParameters), + ...normalizeWmsParams(_urlParamsFromUrl, (key) => key.toUpperCase() !== 'REQUEST'), + ...normalizeWmsParams(baseUrlParameters), + ...normalizeWmsParams(getFeatureInfoUrlParameters), + }; + + // Get the effective CRS (prefer CRS over SRS for WMS 1.3.0) + const crsValue = tempParams.CRS || tempParams.SRS || 'EPSG:3857'; + const effectiveCrs = String(crsValue).toUpperCase(); + + // Calculate the bbox in the appropriate coordinate system const unprojected = mapHook.map.unproject([ev.point.x, ev.point.y]); const point = turf.point([unprojected.lng, unprojected.lat]); const buffered = turf.buffer(point, 50, { units: 'meters' }); const _bbox = buffered && turf.bbox(buffered); - const _sw = _bbox && lngLatToMeters({ lng: _bbox[0], lat: _bbox[1] } as LngLat); - const _ne = _bbox && lngLatToMeters({ lng: _bbox[2], lat: _bbox[3] } as LngLat); - const bbox = _sw && _ne && [_sw[0], _sw[1], _ne[0], _ne[1]]; + + let bbox: number[] | undefined; + if (effectiveCrs === 'EPSG:4326' || effectiveCrs === 'CRS:84') { + // Use lat/lng coordinates directly for EPSG:4326/CRS:84 + bbox = _bbox ? [..._bbox] : undefined; + } else { + // Convert to Web Mercator meters (EPSG:3857) + if (effectiveCrs !== 'EPSG:3857' && effectiveCrs !== 'EPSG:900913') { + console.warn(`CRS "${effectiveCrs}" not supported for GetFeatureInfo BBOX conversion, using EPSG:3857`); + } + if (_bbox) { + const swMercator = turf.toMercator(turf.point([_bbox[0], _bbox[1]])); + const neMercator = turf.toMercator(turf.point([_bbox[2], _bbox[3]])); + bbox = [...swMercator.geometry.coordinates, ...neMercator.geometry.coordinates]; + } + } const _getFeatureInfoUrlParams = { REQUEST: 'GetFeatureInfo', @@ -338,21 +395,13 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { .filter((n) => n), }; - let _gfiUrl: string | undefined = getFeatureInfoUrl; - let _gfiUrlParts; - if (_gfiUrl?.indexOf?.('?') !== -1) { - _gfiUrlParts = props?.url?.split('?') || props?.config?.wmsUrl?.split('?'); - _gfiUrl = _gfiUrlParts?.[0]; - } - const _urlParamsFromUrl = new URLSearchParams(_gfiUrlParts?.[1]); - const urlParamsObj = { - ...normalizeWmsParams(defaultProps.baseUrlParameters), - ...normalizeWmsParams(defaultProps.getFeatureInfoUrlParameters), + ...normalizeWmsParams(defaultBaseUrlParameters), + ...normalizeWmsParams(defaultGetFeatureInfoUrlParameters), ...normalizeWmsParams(_urlParamsFromUrl, (key) => key.toUpperCase() !== 'REQUEST'), - ...normalizeWmsParams(props.baseUrlParameters), + ...normalizeWmsParams(baseUrlParameters), ..._getFeatureInfoUrlParams, - ...normalizeWmsParams(props.getFeatureInfoUrlParameters), + ...normalizeWmsParams(getFeatureInfoUrlParameters), }; // create URLSearchParams object to assemble the URL Parameters // "as any" can be removed once the URLSearchParams ts spec is fixed @@ -372,7 +421,7 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { }) .catch((error) => console.log(error)); }, - [capabilities, getFeatureInfoUrl, props, mapHook, layers] + [capabilities, getFeatureInfoUrl, props?.url, props?.config?.wmsUrl, props.featureInfoSuccess, mapHook, layers, baseUrlParameters, getFeatureInfoUrlParameters] ); const _featureInfoEventsEnabled = useMemo(() => { @@ -463,7 +512,7 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { setLayers(_layers); // zoom to extent of first layer - if (props.zoomToExtent && mapHook?.map && _LatLonBoundingBox.length > 3) { + if (zoomToExtent && mapHook?.map && _LatLonBoundingBox.length > 3) { mapHook?.map.fitBounds([ [_LatLonBoundingBox[0], _LatLonBoundingBox[1]], [_LatLonBoundingBox[2], _LatLonBoundingBox[3]], @@ -476,7 +525,7 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { secondaryAction={ <> {props.buttons} - {props.featureInfoEnabled && ( + {featureInfoEnabled && ( { )} setOpen((current) => !current)} > {open ? : } - {props.showDeleteButton && ( + {showDeleteButton && ( <> { )} {wmsUrl && ( <> - {props.showLayerList && ( + {showLayerList && ( <> {props.sortable && {listContent}} {!props.sortable && listContent} @@ -617,8 +666,8 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { attribution={attribution} visible={visible} urlParameters={{ - ...props.baseUrlParameters, - ...props.getMapUrlParameters, + ...baseUrlParameters, + ...getMapUrlParameters, layers: layers ?.filter?.((layer) => layer.visible) .map((el) => el.Name) @@ -629,7 +678,7 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { /> )} - {props.featureInfoEnabled && props.featureInfoMarkerEnabled && featureInfoLngLat && ( + {featureInfoEnabled && featureInfoMarkerEnabled && featureInfoLngLat && ( )} From b4c39b807a743375bed7a0bc3919a828db2b44cb Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 4 Feb 2026 14:25:37 +0100 Subject: [PATCH 2/4] fix formatting --- .../src/components/MlWmsLoader/MlWmsLoader.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx index 004e71bc..00cf063c 100644 --- a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx +++ b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx @@ -371,7 +371,9 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { } else { // Convert to Web Mercator meters (EPSG:3857) if (effectiveCrs !== 'EPSG:3857' && effectiveCrs !== 'EPSG:900913') { - console.warn(`CRS "${effectiveCrs}" not supported for GetFeatureInfo BBOX conversion, using EPSG:3857`); + console.warn( + `CRS "${effectiveCrs}" not supported for GetFeatureInfo BBOX conversion, using EPSG:3857` + ); } if (_bbox) { const swMercator = turf.toMercator(turf.point([_bbox[0], _bbox[1]])); @@ -421,7 +423,17 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { }) .catch((error) => console.log(error)); }, - [capabilities, getFeatureInfoUrl, props?.url, props?.config?.wmsUrl, props.featureInfoSuccess, mapHook, layers, baseUrlParameters, getFeatureInfoUrlParameters] + [ + capabilities, + getFeatureInfoUrl, + props?.url, + props?.config?.wmsUrl, + props.featureInfoSuccess, + mapHook, + layers, + baseUrlParameters, + getFeatureInfoUrlParameters, + ] ); const _featureInfoEventsEnabled = useMemo(() => { From be15ec707b6bc44213ae7c3b44e4bca4b803a785 Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 4 Feb 2026 14:30:21 +0100 Subject: [PATCH 3/4] fix ts error --- .../react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx index 00cf063c..52807024 100644 --- a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx +++ b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx @@ -233,7 +233,6 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { // Apply simple defaults via destructuring const { mapId = defaultProps.mapId, - url = defaultProps.url, featureInfoEnabled = defaultProps.featureInfoEnabled, featureInfoMarkerEnabled = defaultProps.featureInfoMarkerEnabled, zoomToExtent = defaultProps.zoomToExtent, From 5cbf4d6a6cabe9c66ef4c6c959a917d389a411e1 Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 4 Feb 2026 14:35:29 +0100 Subject: [PATCH 4/4] implement review suggestions --- .../src/components/MlWmsLoader/MlWmsLoader.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx index 52807024..bd59ff26 100644 --- a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx +++ b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import MlWmsLayer from '../MlWmsLayer/MlWmsLayer'; import MlMarker from '../MlMarker/MlMarker'; @@ -230,6 +230,9 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { [props.getFeatureInfoUrlParameters] ); + const featureInfoSuccessRef = useRef(props.featureInfoSuccess); + featureInfoSuccessRef.current = props.featureInfoSuccess; + // Apply simple defaults via destructuring const { mapId = defaultProps.mapId, @@ -371,7 +374,8 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { // Convert to Web Mercator meters (EPSG:3857) if (effectiveCrs !== 'EPSG:3857' && effectiveCrs !== 'EPSG:900913') { console.warn( - `CRS "${effectiveCrs}" not supported for GetFeatureInfo BBOX conversion, using EPSG:3857` + `CRS "${effectiveCrs}" is not explicitly supported for GetFeatureInfo BBOX conversion; ` + + 'BBOX will be computed in Web Mercator (EPSG:3857), which may be inaccurate for this CRS.' ); } if (_bbox) { @@ -418,7 +422,7 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { .then((text) => { setFeatureInfoLngLat(ev.lngLat); setFeatureInfoContent(text); - props.featureInfoSuccess?.(text, ev.lngLat); + featureInfoSuccessRef.current?.(text, ev.lngLat); }) .catch((error) => console.log(error)); }, @@ -427,7 +431,6 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { getFeatureInfoUrl, props?.url, props?.config?.wmsUrl, - props.featureInfoSuccess, mapHook, layers, baseUrlParameters,