From aafcf8c81a0d2bac9dd9f72cebd3837d18070f64 Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 28 Jan 2026 09:00:56 +0100 Subject: [PATCH 01/13] fix: update visibility logic for WMS layers in MlWmsLoader --- .../src/components/MlWmsLoader/MlWmsLoader.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx index 8b3b5ef1..1af5d858 100644 --- a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx +++ b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx @@ -421,10 +421,9 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { if (idx === 0) { _LatLonBoundingBox = layer.EX_GeographicBoundingBox || layer?.LatLonBoundingBox || []; } - const isVisible = - props.visibleLayersAtStart && layer.Name - ? props.visibleLayersAtStart.includes(layer.Name) - : false; + const isVisible = props.visibleLayersAtStart + ? props.visibleLayersAtStart.includes(layer.Name || '') + : true; return { visible: isVisible, Attribution: { Title: '' }, From 17a02e833c1785a35ad4d23a65d8486d9bd0f0ae Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 28 Jan 2026 10:34:48 +0100 Subject: [PATCH 02/13] fix: normalize URL parameters to avoid duplicates in useWms hook --- packages/react-maplibre/src/hooks/useWms.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/react-maplibre/src/hooks/useWms.ts b/packages/react-maplibre/src/hooks/useWms.ts index 91d56e61..21574c1f 100644 --- a/packages/react-maplibre/src/hooks/useWms.ts +++ b/packages/react-maplibre/src/hooks/useWms.ts @@ -44,9 +44,17 @@ function useWms(props: useWmsProps): useWmsReturnType { } const _urlParamsFromUrl = new URLSearchParams(_propsUrlParams?.[1]); + // Normalize all parameter keys to uppercase to avoid duplicates + const normalizedUrlParams = Object.fromEntries( + Array.from(_urlParamsFromUrl.entries()).map(([key, value]) => [key.toUpperCase(), value]) + ); + const normalizedPropsParams = Object.fromEntries( + Object.entries(props.urlParameters || {}).map(([key, value]) => [key.toUpperCase(), value]) + ); + const urlParamsObj = { - ...Object.fromEntries(_urlParamsFromUrl), - ...props.urlParameters, + ...normalizedUrlParams, + ...normalizedPropsParams, }; // create URLSearchParams object to assemble the URL Parameters const urlParams = new URLSearchParams(urlParamsObj); @@ -54,6 +62,7 @@ function useWms(props: useWmsProps): useWmsReturnType { const urlParamsStr = decodeURIComponent(urlParams.toString()) + ''.replace(/%2F/g, '/').replace(/%3A/g, ':'); + console.log(_wmsUrl + '?' + urlParamsStr); fetch(_wmsUrl + '?' + urlParamsStr) .then((res) => { if (!res.ok) { From d4daab754656567fead392a21eb71d90ab69cde5 Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 28 Jan 2026 10:41:36 +0100 Subject: [PATCH 03/13] fix: normalize WMS URL parameter keys to uppercase to avoid duplicates --- .../src/components/MlWmsLayer/MlWmsLayer.tsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/react-maplibre/src/components/MlWmsLayer/MlWmsLayer.tsx b/packages/react-maplibre/src/components/MlWmsLayer/MlWmsLayer.tsx index 5ce229a5..6767c0fc 100644 --- a/packages/react-maplibre/src/components/MlWmsLayer/MlWmsLayer.tsx +++ b/packages/react-maplibre/src/components/MlWmsLayer/MlWmsLayer.tsx @@ -62,11 +62,26 @@ const MlWmsLayer = (props: MlWmsLayerProps) => { _wmsUrl = _propsUrlParams[0]; } const _urlParamsFromUrl = new URLSearchParams(_propsUrlParams?.[1]); + + // Normalize all parameter keys to uppercase to avoid duplicates + const normalizedDefaultParams = Object.fromEntries( + Object.entries(defaultProps.urlParameters || {}).map(([key, value]) => [ + key.toUpperCase(), + value, + ]) + ); + const normalizedUrlParams = Object.fromEntries( + Array.from(_urlParamsFromUrl.entries()).map(([key, value]) => [key.toUpperCase(), value]) + ); + const normalizedPropsParams = Object.fromEntries( + Object.entries(props.urlParameters || {}).map(([key, value]) => [key.toUpperCase(), value]) + ); + // first spread in default props manually to enable overriding a single parameter without replacing the whole default urlParameters object const urlParamsObj = { - ...defaultProps.urlParameters, - ...Object.fromEntries(_urlParamsFromUrl), - ...props.urlParameters, + ...normalizedDefaultParams, + ...normalizedUrlParams, + ...normalizedPropsParams, }; const urlParams = new URLSearchParams(urlParamsObj as unknown as Record); const urlParamsStr = From ea1e71503e2d7697e7672fe21a8a73b740eea3f9 Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 28 Jan 2026 10:56:50 +0100 Subject: [PATCH 04/13] fix: refactor WMS URL parameters for clarity and normalization --- .../components/MlWmsLoader/MlWmsLoader.tsx | 103 +++++++++++++----- 1 file changed, 78 insertions(+), 25 deletions(-) diff --git a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx index 1af5d858..0683c45b 100644 --- a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx +++ b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx @@ -73,13 +73,21 @@ export interface MlWmsLoaderProps { layerId?: string; insertBeforeLayer?: string; /** - * URL parameters that will be used in the getCapabilities request + * Base URL parameters that will be used for all WMS requests (GetCapabilities, GetMap, GetFeatureInfo) */ - urlParameters?: useWmsProps['urlParameters']; + baseUrlParameters?: { [key: string]: string }; /** - * URL parameters that will be added when requesting WMS capabilities + * URL parameters specific to GetCapabilities requests */ - wmsUrlParameters?: { [key: string]: string }; + getCapabilitiesUrlParameters?: { [key: string]: string }; + /** + * URL parameters specific to GetMap requests + */ + getMapUrlParameters?: { [key: string]: string }; + /** + * URL parameters specific to GetFeatureInfo requests + */ + getFeatureInfoUrlParameters?: { [key: string]: string }; /** * If true, zooms to the extent of the WMS layer after loading the getCapabilities response */ @@ -177,14 +185,26 @@ export type LayerType = { const defaultProps = { mapId: undefined, url: '', - urlParameters: { + baseUrlParameters: { SERVICE: 'WMS', VERSION: '1.3.0', - REQUEST: 'GetCapabilities', }, - wmsUrlParameters: { + 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, @@ -199,6 +219,16 @@ const defaultProps = { */ const MlWmsLoader = (props: MlWmsLoaderProps) => { props = { ...defaultProps, ...props }; + + const capabilitiesUrlParameters = useMemo( + () => ({ + ...props.baseUrlParameters, + ...props.getCapabilitiesUrlParameters, + REQUEST: 'GetCapabilities', + }), + [props.baseUrlParameters, props.getCapabilitiesUrlParameters] + ); + const { capabilities: _capabilities, error, @@ -206,7 +236,7 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { getFeatureInfoUrl: _getFeatureInfoUrl, wmsUrl: _wmsUrl, }: useWmsReturnType = useWms({ - urlParameters: props.urlParameters, + urlParameters: capabilitiesUrlParameters, }); const [open, setOpen] = useState(props?.config?.open || false); @@ -291,33 +321,20 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { 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]]; + const _getFeatureInfoUrlParams = { REQUEST: 'GetFeatureInfo', - BBOX: bbox?.join(','), - SERVICE: 'WMS', INFO_FORMAT: capabilities?.Capability?.Request?.GetFeatureInfo.Format.indexOf('text/html') !== -1 ? 'text/html' : 'text/plain', - FEATURE_COUNT: '10', LAYERS: layers .map((layer: LayerType) => (layer.visible && layer.queryable ? layer.Name : undefined)) .filter((n) => n), QUERY_LAYERS: layers .map((layer: LayerType) => (layer.visible && layer.queryable ? layer.Name : undefined)) .filter((n) => n), - STYLES: '', - WIDTH: 100, - HEIGHT: 100, - srs: 'EPSG:3857', - CRS: 'EPSG:3857', - version: '1.3.0', - X: 50, - Y: 50, - I: 50, - J: 50, - buffer: '50', }; let _gfiUrl: string | undefined = getFeatureInfoUrl; @@ -328,15 +345,50 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { } const _urlParamsFromUrl = new URLSearchParams(_gfiUrlParts?.[1]); + // Normalize all parameter keys to uppercase to avoid duplicates + const normalizedDefaultParams = Object.fromEntries( + Object.entries(defaultProps.baseUrlParameters).map(([key, value]) => [ + key.toUpperCase(), + value, + ]) + ); + const normalizedDefaultFeatureInfoParams = Object.fromEntries( + Object.entries(defaultProps.getFeatureInfoUrlParameters).map(([key, value]) => [ + key.toUpperCase(), + value, + ]) + ); + const normalizedUrlParams = Object.fromEntries( + Array.from(_urlParamsFromUrl.entries()) + .filter(([key]) => key.toUpperCase() !== 'REQUEST' || !key.match(/GetCapabilities/i)) + .map(([key, value]) => [key.toUpperCase(), value]) + ); + const normalizedBaseParams = Object.fromEntries( + Object.entries(props.baseUrlParameters || {}).map(([key, value]) => [ + key.toUpperCase(), + value, + ]) + ); + const normalizedFeatureInfoParams = Object.fromEntries( + Object.entries(props.getFeatureInfoUrlParameters || {}).map(([key, value]) => [ + key.toUpperCase(), + value, + ]) + ); + const urlParamsObj = { - ...Object.fromEntries(_urlParamsFromUrl), + ...normalizedDefaultParams, + ...normalizedDefaultFeatureInfoParams, + ...normalizedUrlParams, + ...normalizedBaseParams, + ...normalizedFeatureInfoParams, ..._getFeatureInfoUrlParams, }; // create URLSearchParams object to assemble the URL Parameters // "as any" can be removed once the URLSearchParams ts spec is fixed const urlParams = new URLSearchParams(urlParamsObj as unknown as Record); - fetch(props.url + '?' + urlParams.toString()) + fetch(_gfiUrl + '?' + urlParams.toString()) .then((res) => { if (!res.ok) { throw new Error('FeatureInfo could not be fetched'); @@ -595,7 +647,8 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { attribution={attribution} visible={visible} urlParameters={{ - ...props.wmsUrlParameters, + ...props.baseUrlParameters, + ...props.getMapUrlParameters, layers: layers ?.filter?.((layer) => layer.visible) .map((el) => el.Name) From 1fa510f810c14c5f74cb2c4580f7b7f062cb6303 Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 28 Jan 2026 11:02:44 +0100 Subject: [PATCH 05/13] fix: implement normalizeWmsParams utility to standardize WMS URL parameters --- .../src/components/MlWmsLayer/MlWmsLayer.tsx | 21 ++------- .../components/MlWmsLoader/MlWmsLoader.tsx | 45 ++++--------------- packages/react-maplibre/src/hooks/useWms.ts | 13 ++---- packages/react-maplibre/src/utils/wmsUtils.ts | 21 +++++++++ 4 files changed, 37 insertions(+), 63 deletions(-) create mode 100644 packages/react-maplibre/src/utils/wmsUtils.ts diff --git a/packages/react-maplibre/src/components/MlWmsLayer/MlWmsLayer.tsx b/packages/react-maplibre/src/components/MlWmsLayer/MlWmsLayer.tsx index 6767c0fc..abf66f33 100644 --- a/packages/react-maplibre/src/components/MlWmsLayer/MlWmsLayer.tsx +++ b/packages/react-maplibre/src/components/MlWmsLayer/MlWmsLayer.tsx @@ -1,6 +1,7 @@ import { useMemo, useRef, useEffect, useCallback } from 'react'; import useMap from '../../hooks/useMap'; import { RasterLayerSpecification, RasterSourceSpecification } from 'maplibre-gl'; +import { normalizeWmsParams } from '../../utils/wmsUtils'; const defaultProps: MlWmsLayerProps = { url: '', @@ -63,25 +64,11 @@ const MlWmsLayer = (props: MlWmsLayerProps) => { } const _urlParamsFromUrl = new URLSearchParams(_propsUrlParams?.[1]); - // Normalize all parameter keys to uppercase to avoid duplicates - const normalizedDefaultParams = Object.fromEntries( - Object.entries(defaultProps.urlParameters || {}).map(([key, value]) => [ - key.toUpperCase(), - value, - ]) - ); - const normalizedUrlParams = Object.fromEntries( - Array.from(_urlParamsFromUrl.entries()).map(([key, value]) => [key.toUpperCase(), value]) - ); - const normalizedPropsParams = Object.fromEntries( - Object.entries(props.urlParameters || {}).map(([key, value]) => [key.toUpperCase(), value]) - ); - // first spread in default props manually to enable overriding a single parameter without replacing the whole default urlParameters object const urlParamsObj = { - ...normalizedDefaultParams, - ...normalizedUrlParams, - ...normalizedPropsParams, + ...normalizeWmsParams(defaultProps.urlParameters), + ...normalizeWmsParams(_urlParamsFromUrl), + ...normalizeWmsParams(props.urlParameters), }; const urlParams = new URLSearchParams(urlParamsObj as unknown as Record); const urlParamsStr = diff --git a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx index 0683c45b..1d7956b3 100644 --- a/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx +++ b/packages/react-maplibre/src/components/MlWmsLoader/MlWmsLoader.tsx @@ -19,6 +19,7 @@ import ConfirmDialog from '../../ui_components/ConfirmDialog'; 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 }) { @@ -345,43 +346,15 @@ const MlWmsLoader = (props: MlWmsLoaderProps) => { } const _urlParamsFromUrl = new URLSearchParams(_gfiUrlParts?.[1]); - // Normalize all parameter keys to uppercase to avoid duplicates - const normalizedDefaultParams = Object.fromEntries( - Object.entries(defaultProps.baseUrlParameters).map(([key, value]) => [ - key.toUpperCase(), - value, - ]) - ); - const normalizedDefaultFeatureInfoParams = Object.fromEntries( - Object.entries(defaultProps.getFeatureInfoUrlParameters).map(([key, value]) => [ - key.toUpperCase(), - value, - ]) - ); - const normalizedUrlParams = Object.fromEntries( - Array.from(_urlParamsFromUrl.entries()) - .filter(([key]) => key.toUpperCase() !== 'REQUEST' || !key.match(/GetCapabilities/i)) - .map(([key, value]) => [key.toUpperCase(), value]) - ); - const normalizedBaseParams = Object.fromEntries( - Object.entries(props.baseUrlParameters || {}).map(([key, value]) => [ - key.toUpperCase(), - value, - ]) - ); - const normalizedFeatureInfoParams = Object.fromEntries( - Object.entries(props.getFeatureInfoUrlParameters || {}).map(([key, value]) => [ - key.toUpperCase(), - value, - ]) - ); - const urlParamsObj = { - ...normalizedDefaultParams, - ...normalizedDefaultFeatureInfoParams, - ...normalizedUrlParams, - ...normalizedBaseParams, - ...normalizedFeatureInfoParams, + ...normalizeWmsParams(defaultProps.baseUrlParameters), + ...normalizeWmsParams(defaultProps.getFeatureInfoUrlParameters), + ...normalizeWmsParams( + _urlParamsFromUrl, + (key) => key.toUpperCase() !== 'REQUEST' || !key.match(/GetCapabilities/i) + ), + ...normalizeWmsParams(props.baseUrlParameters), + ...normalizeWmsParams(props.getFeatureInfoUrlParameters), ..._getFeatureInfoUrlParams, }; // create URLSearchParams object to assemble the URL Parameters diff --git a/packages/react-maplibre/src/hooks/useWms.ts b/packages/react-maplibre/src/hooks/useWms.ts index 21574c1f..9f73bc65 100644 --- a/packages/react-maplibre/src/hooks/useWms.ts +++ b/packages/react-maplibre/src/hooks/useWms.ts @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import WMSCapabilities, { WMSCapabilitiesJSON } from 'wms-capabilities'; +import { normalizeWmsParams } from '../utils/wmsUtils'; export interface useWmsProps { url?: string; @@ -44,17 +45,9 @@ function useWms(props: useWmsProps): useWmsReturnType { } const _urlParamsFromUrl = new URLSearchParams(_propsUrlParams?.[1]); - // Normalize all parameter keys to uppercase to avoid duplicates - const normalizedUrlParams = Object.fromEntries( - Array.from(_urlParamsFromUrl.entries()).map(([key, value]) => [key.toUpperCase(), value]) - ); - const normalizedPropsParams = Object.fromEntries( - Object.entries(props.urlParameters || {}).map(([key, value]) => [key.toUpperCase(), value]) - ); - const urlParamsObj = { - ...normalizedUrlParams, - ...normalizedPropsParams, + ...normalizeWmsParams(_urlParamsFromUrl), + ...normalizeWmsParams(props.urlParameters), }; // create URLSearchParams object to assemble the URL Parameters const urlParams = new URLSearchParams(urlParamsObj); diff --git a/packages/react-maplibre/src/utils/wmsUtils.ts b/packages/react-maplibre/src/utils/wmsUtils.ts new file mode 100644 index 00000000..9f666960 --- /dev/null +++ b/packages/react-maplibre/src/utils/wmsUtils.ts @@ -0,0 +1,21 @@ +/** + * Normalizes URL parameter keys to uppercase to avoid duplicates when merging WMS parameters + * @param params - Object or URLSearchParams to normalize + * @param filterFn - Optional filter function to exclude certain key-value pairs + * @returns Object with uppercase keys + */ +export function normalizeWmsParams( + params: { [key: string]: string | number } | URLSearchParams | undefined, + filterFn?: (key: string, value: string) => boolean +): { [key: string]: string | number } { + if (!params) { + return {}; + } + + const entries = + params instanceof URLSearchParams ? Array.from(params.entries()) : Object.entries(params); + + const filtered = filterFn ? entries.filter(([key, value]) => filterFn(key, value)) : entries; + + return Object.fromEntries(filtered.map(([key, value]) => [key.toUpperCase(), value])); +} From 29a12b4d2191715b4fac517c05d8a58cb7241618 Mon Sep 17 00:00:00 2001 From: Max Tobias Weber Date: Wed, 28 Jan 2026 11:19:13 +0100 Subject: [PATCH 06/13] fix: add close button functionality to MlMarker component and improve appearance --- .../src/components/MlMarker/MlMarker.tsx | 151 +++++++++++++++--- 1 file changed, 127 insertions(+), 24 deletions(-) diff --git a/packages/react-maplibre/src/components/MlMarker/MlMarker.tsx b/packages/react-maplibre/src/components/MlMarker/MlMarker.tsx index 0202192e..1eaa9fe8 100644 --- a/packages/react-maplibre/src/components/MlMarker/MlMarker.tsx +++ b/packages/react-maplibre/src/components/MlMarker/MlMarker.tsx @@ -2,7 +2,8 @@ import React, { useRef, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import useMap from '../../hooks/useMap'; import maplibregl from 'maplibre-gl'; -import { Box } from '@mui/material'; +import { Box, Paper, IconButton } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; export interface MlMarkerProps { /** ID of the map to add the marker to */ @@ -27,6 +28,10 @@ export interface MlMarkerProps { contentOffset?: number; /** Whether mouse events pass through the marker content */ passEventsThrough?: boolean; + /** Whether to show a close button to remove the marker */ + showCloseButton?: boolean; + /** Callback function when the close button is clicked */ + onClose?: () => void; /** Anchor position of the marker relative to its coordinates */ anchor?: | 'top' @@ -103,16 +108,33 @@ function getBoxMargins( return m; } -const MlMarker = ({ passEventsThrough = true, contentOffset = 5, ...props }: MlMarkerProps) => { +const MlMarker = ({ + passEventsThrough = true, + contentOffset = 5, + showCloseButton = true, + ...props +}: MlMarkerProps) => { const mapHook = useMap({ mapId: props.mapId, waitForLayer: props.insertBeforeLayer, }); const [marker, setMarker] = useState(null); + const [contentWidth, setContentWidth] = useState(300); const container = useRef(null); const iframeRef = useRef(null); + const handleClose = (event: React.MouseEvent) => { + event.stopPropagation(); + if (props.onClose) { + props.onClose(); + } else { + // Default behavior: remove the marker + marker?.remove(); + container.current?.remove(); + } + }; + useEffect(() => { if (!mapHook.map) return; @@ -161,9 +183,14 @@ const MlMarker = ({ passEventsThrough = true, contentOffset = 5, ...props }: MlM function handleIframeLoad() { const iframeDoc = iframeRef.current?.contentWindow?.document; - if (iframeDoc && iframeRef.current?.parentElement) { + if (iframeDoc && iframeRef.current) { const scrollHeight = iframeDoc.documentElement.scrollHeight; - iframeRef.current.parentElement.style.height = `${scrollHeight}px`; + const scrollWidth = iframeDoc.documentElement.scrollWidth; + iframeRef.current.style.height = `${scrollHeight}px`; + + // Set width based on content, with min of 200px and max of 600px + const calculatedWidth = Math.max(200, Math.min(scrollWidth + 32, 600)); + setContentWidth(calculatedWidth); } } @@ -173,41 +200,117 @@ const MlMarker = ({ passEventsThrough = true, contentOffset = 5, ...props }: MlM -