From 1e8f8bab86ab1edbaf7a12078a9063abfc20646f Mon Sep 17 00:00:00 2001 From: meyerlor Date: Tue, 30 Sep 2025 14:31:14 +0200 Subject: [PATCH 1/6] Fix: Display legend for external WMS layers External WMS layers were showing empty legend icons because QGIS Server returns empty icon fields in JSON GetLegendGraphic responses. - Added getLegendGraphicPNG() method to WMS class - Modified updateLayerTreeLayersSymbology() to detect external WMS layers - External WMS layers now fetch PNG legend and convert to base64 - Normal layers continue using JSON format (no breaking changes) --- assets/src/modules/WMS.js | 25 +++++++++++++ assets/src/modules/action/Symbology.js | 51 +++++++++++++++++++++----- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/assets/src/modules/WMS.js b/assets/src/modules/WMS.js index 6177869a03..f81228b2f4 100644 --- a/assets/src/modules/WMS.js +++ b/assets/src/modules/WMS.js @@ -86,4 +86,29 @@ export default class WMS { body: params, }); } + + /** + * Get legend graphic as PNG image URL from WMS + * @param {object} options - optional parameters which can override this._defaultGetLegendGraphicsParameters + * @returns {string} PNG image URL + * @memberof WMS + */ + getLegendGraphicPNG(options) { + const layers = options['LAYERS'] ?? options['LAYER']; + // Check if layer is specified + if (!layers) { + throw new RequestError( + 'LAYERS or LAYER parameter is required for getLegendGraphic request', + options, + ); + } + + const params = new URLSearchParams({ + ...this._defaultGetLegendGraphicParameters, + ...options, + FORMAT: 'image/png' // Force PNG format for external WMS layers + }); + + return `${globalThis['lizUrls'].wms}?${params}`; + } } diff --git a/assets/src/modules/action/Symbology.js b/assets/src/modules/action/Symbology.js index bb4596440f..baa6866a98 100644 --- a/assets/src/modules/action/Symbology.js +++ b/assets/src/modules/action/Symbology.js @@ -32,21 +32,54 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ if (method.toUpperCase() == HttpRequestMethods.GET) { for (const treeLayer of treeLayers) { + // Check if this is an external WMS layer + const isExternalWMS = treeLayer.itemState?.externalWmsToggle === true; + const wmsParams = { LAYER: treeLayer.wmsName, STYLES: treeLayer.wmsSelectedStyleName, }; - await wms.getLegendGraphic(wmsParams).then((response) => { - for (const node of response.nodes) { - // If the layer has no symbology, there is no type property - if (node.hasOwnProperty('type')) { - treeLayer.symbology = node; - } + if (isExternalWMS) { + // For external WMS layers, get PNG legend directly + try { + const pngUrl = wms.getLegendGraphicPNG(wmsParams); + // Fetch the PNG and convert to base64 + const response = await fetch(pngUrl); + const blob = await response.blob(); + const reader = new FileReader(); + + await new Promise((resolve, reject) => { + reader.onloadend = () => { + const base64data = reader.result.split(',')[1]; // Remove data:image/png;base64, prefix + treeLayer.symbology = { + type: 'layer', + name: treeLayer.wmsName, + title: treeLayer.name, + icon: base64data + }; + resolve(); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } catch (error) { + console.error('Error loading external WMS legend:', error); + // Fallback to default icon will be handled by symbology state } - }).catch((error) => { - console.error(error); - }); + } else { + // For normal layers, use JSON format + await wms.getLegendGraphic(wmsParams).then((response) => { + for (const node of response.nodes) { + // If the layer has no symbology, there is no type property + if (node.hasOwnProperty('type')) { + treeLayer.symbology = node; + } + } + }).catch((error) => { + console.error(error); + }); + } } return treeLayers; } From 2e8873bc545476bcfcbb150818d103958eff1559 Mon Sep 17 00:00:00 2001 From: meyerlor Date: Fri, 14 Nov 2025 21:09:04 +0100 Subject: [PATCH 2/6] Improve external WMS layer detection in symbology Fixed the external WMS layer detection to use the correct property path and handle both boolean and string values from the backend. Changes: - Updated path from itemState.externalWmsToggle to itemState.layerConfig.externalWmsToggle - Added support for string value 'True' in addition to boolean true - Cleaned up unnecessary comments for better code clarity This ensures external WMS layers are properly detected and their legends are fetched in PNG format instead of JSON. --- assets/src/modules/action/Symbology.js | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/assets/src/modules/action/Symbology.js b/assets/src/modules/action/Symbology.js index baa6866a98..cb412fc3d0 100644 --- a/assets/src/modules/action/Symbology.js +++ b/assets/src/modules/action/Symbology.js @@ -24,17 +24,16 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ const wms = new WMS(); - // If the tree layers is empty - // nothing to do if (treeLayers.length == 0) { return treeLayers; } if (method.toUpperCase() == HttpRequestMethods.GET) { for (const treeLayer of treeLayers) { - // Check if this is an external WMS layer - const isExternalWMS = treeLayer.itemState?.externalWmsToggle === true; - + // Check if this is an external WMS layer using the backend flag + const isExternalWMS = treeLayer.itemState?.layerConfig?.externalWmsToggle === true + || treeLayer.itemState?.layerConfig?.externalWmsToggle === 'True'; + const wmsParams = { LAYER: treeLayer.wmsName, STYLES: treeLayer.wmsSelectedStyleName, @@ -44,14 +43,13 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ // For external WMS layers, get PNG legend directly try { const pngUrl = wms.getLegendGraphicPNG(wmsParams); - // Fetch the PNG and convert to base64 const response = await fetch(pngUrl); const blob = await response.blob(); const reader = new FileReader(); - + await new Promise((resolve, reject) => { reader.onloadend = () => { - const base64data = reader.result.split(',')[1]; // Remove data:image/png;base64, prefix + const base64data = reader.result.split(',')[1]; treeLayer.symbology = { type: 'layer', name: treeLayer.wmsName, @@ -65,13 +63,11 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ }); } catch (error) { console.error('Error loading external WMS legend:', error); - // Fallback to default icon will be handled by symbology state } } else { // For normal layers, use JSON format await wms.getLegendGraphic(wmsParams).then((response) => { for (const node of response.nodes) { - // If the layer has no symbology, there is no type property if (node.hasOwnProperty('type')) { treeLayer.symbology = node; } @@ -84,6 +80,7 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ return treeLayers; } + // POST method code bleibt unverändert... const wmsNames = treeLayers.map(layer => layer.wmsName); const wmsStyles = treeLayers.map(layer => layer.wmsSelectedStyleName); let treeLayersByName = {}; @@ -98,7 +95,6 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ await wms.getLegendGraphic(wmsParams).then((response) => { for (const node of response.nodes) { - // If the layer has no symbology, there is no type property if (node.hasOwnProperty('type')) { treeLayersByName[node.name].symbology = node; } @@ -106,20 +102,12 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ return treeLayers; }).catch(async (error) => { console.error(error); - // If the request failed, try to get the legend graphic for each layer separately - // This is a workaround for the issue when QGIS server timed out when requesting - // the legend graphic for multiple layers at once (LAYER parameter with multiple values) if (treeLayers.length == 1) { - // If there is only one layer, there is no need to try to get the legend graphic - // for each layer separately return treeLayers; } if (!(error instanceof HttpError) || error.statusCode != 504) { - // If the error is not a timeout, there is no need to try to get the legend graphic - // for each layer separately return treeLayers; } - // Try to get the legend graphic for each layer separately for (const treeLayer of treeLayers) { await updateLayerTreeLayerSymbology(treeLayer); } From 7f3807ea691756cce98e97052a8f59760826b03c Mon Sep 17 00:00:00 2001 From: meyerlor Date: Fri, 14 Nov 2025 22:03:13 +0100 Subject: [PATCH 3/6] Enable externalWmsToggle for all external WMS layers Previously, externalWmsToggle was only set for layers with EPSG:3857. This change enables it for ALL external WMS layers regardless of CRS. Changes: - Set externalWmsToggle = 'True' for all external WMS layers - Keep EPSG:3857 specific handling for future use - Allows legend display for external WMS in any projection This works together with the JavaScript improvements to properly display legends for all external WMS layers. --- lizmap/modules/lizmap/lib/Project/Project.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lizmap/modules/lizmap/lib/Project/Project.php b/lizmap/modules/lizmap/lib/Project/Project.php index d12b3132a2..963da7d76a 100644 --- a/lizmap/modules/lizmap/lib/Project/Project.php +++ b/lizmap/modules/lizmap/lib/Project/Project.php @@ -2045,12 +2045,15 @@ public function getUpdatedConfig() && !array_key_exists('crs', $layerDatasource)) { $layerDatasource['crs'] = 'EPSG:3857'; } + // Set externalWmsToggle for ALL external WMS layers (not just EPSG:3857) + $obj->externalWmsToggle = 'True'; + $obj->externalAccess = $layerDatasource; + // if the layer datasource contains type and crs EPSG:3857 - // external access can be provided + // additional external access configuration can be provided if (array_key_exists('type', $layerDatasource) && $layerDatasource['crs'] == 'EPSG:3857') { - $obj->externalWmsToggle = 'True'; - $obj->externalAccess = $layerDatasource; + // Additional external access for EPSG:3857 layers } } } From 95f774b23bb4c4da0d33f6ac030c9e56052ccc8a Mon Sep 17 00:00:00 2001 From: meyerlor Date: Fri, 14 Nov 2025 22:11:35 +0100 Subject: [PATCH 4/6] Fix external WMS detection using correct internal property path Updated the property path to access externalWmsToggle correctly through the internal object structure. Changes: - Access via _mapItemState._layerItemState._layerTreeItemCfg._layerCfg - Check _externalWmsToggle (private property with underscore) - Maintains support for both boolean true and string 'True' This ensures external WMS layers are properly detected and their legends are displayed correctly. --- assets/src/modules/action/Symbology.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/assets/src/modules/action/Symbology.js b/assets/src/modules/action/Symbology.js index cb412fc3d0..f418bd266d 100644 --- a/assets/src/modules/action/Symbology.js +++ b/assets/src/modules/action/Symbology.js @@ -31,8 +31,9 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ if (method.toUpperCase() == HttpRequestMethods.GET) { for (const treeLayer of treeLayers) { // Check if this is an external WMS layer using the backend flag - const isExternalWMS = treeLayer.itemState?.layerConfig?.externalWmsToggle === true - || treeLayer.itemState?.layerConfig?.externalWmsToggle === 'True'; + const layerCfg = treeLayer._mapItemState?._layerItemState?._layerTreeItemCfg?._layerCfg; + const isExternalWMS = layerCfg?._externalWmsToggle === true + || layerCfg?._externalWmsToggle === 'True'; const wmsParams = { LAYER: treeLayer.wmsName, From 848cd04bf504f2e8d767159491d50b42e64b9e91 Mon Sep 17 00:00:00 2001 From: meyerlor Date: Fri, 14 Nov 2025 22:28:21 +0100 Subject: [PATCH 5/6] Add LAYERTITLE parameter to suppress layer title in external WMS legends Added LAYERTITLE: 'FALSE' parameter for external WMS layers to suppress the layer title in the legend graphic response. Changes: - Moved wmsParams definition into separate if/else blocks - External WMS: wmsParams includes LAYERTITLE: 'FALSE' - Normal layers: wmsParams without LAYERTITLE (unchanged behavior) This improves the legend display by removing unnecessary titles from external WMS layer legends. --- assets/src/modules/action/Symbology.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/assets/src/modules/action/Symbology.js b/assets/src/modules/action/Symbology.js index f418bd266d..f9aa740c01 100644 --- a/assets/src/modules/action/Symbology.js +++ b/assets/src/modules/action/Symbology.js @@ -35,13 +35,13 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ const isExternalWMS = layerCfg?._externalWmsToggle === true || layerCfg?._externalWmsToggle === 'True'; - const wmsParams = { - LAYER: treeLayer.wmsName, - STYLES: treeLayer.wmsSelectedStyleName, - }; - if (isExternalWMS) { // For external WMS layers, get PNG legend directly + const wmsParams = { + LAYER: treeLayer.wmsName, + STYLES: treeLayer.wmsSelectedStyleName, + LAYERTITLE: 'FALSE', + }; try { const pngUrl = wms.getLegendGraphicPNG(wmsParams); const response = await fetch(pngUrl); @@ -67,6 +67,10 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ } } else { // For normal layers, use JSON format + const wmsParams = { + LAYER: treeLayer.wmsName, + STYLES: treeLayer.wmsSelectedStyleName, + }; await wms.getLegendGraphic(wmsParams).then((response) => { for (const node of response.nodes) { if (node.hasOwnProperty('type')) { From 27c532c657a127c4cfe85cdb833c1c599a753026 Mon Sep 17 00:00:00 2001 From: meyerlor Date: Sat, 15 Nov 2025 07:21:28 +0100 Subject: [PATCH 6/6] Restore original comments and translate German comment to English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restored all original comments that were removed in previous commits: - Added back comment about empty tree layers check - Restored comment about fetching PNG and converting to base64 - Restored inline comment about removing data:image/png;base64 prefix - Restored comment about fallback to default icon - Restored comments about symbology type property checks - Restored detailed error handling comments for POST method timeout Also removed German comment and replaced with original structure: - Removed: 'POST method code bleibt unverändert...' This improves code readability and maintains original documentation. --- assets/src/modules/action/Symbology.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/assets/src/modules/action/Symbology.js b/assets/src/modules/action/Symbology.js index f9aa740c01..fdfa3c724a 100644 --- a/assets/src/modules/action/Symbology.js +++ b/assets/src/modules/action/Symbology.js @@ -24,6 +24,8 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ const wms = new WMS(); + // If the tree layers is empty + // nothing to do if (treeLayers.length == 0) { return treeLayers; } @@ -44,13 +46,14 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ }; try { const pngUrl = wms.getLegendGraphicPNG(wmsParams); + // Fetch the PNG and convert to base64 const response = await fetch(pngUrl); const blob = await response.blob(); const reader = new FileReader(); await new Promise((resolve, reject) => { reader.onloadend = () => { - const base64data = reader.result.split(',')[1]; + const base64data = reader.result.split(',')[1]; // Remove data:image/png;base64, prefix treeLayer.symbology = { type: 'layer', name: treeLayer.wmsName, @@ -64,6 +67,7 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ }); } catch (error) { console.error('Error loading external WMS legend:', error); + // Fallback to default icon will be handled by symbology state } } else { // For normal layers, use JSON format @@ -73,6 +77,7 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ }; await wms.getLegendGraphic(wmsParams).then((response) => { for (const node of response.nodes) { + // If the layer has no symbology, there is no type property if (node.hasOwnProperty('type')) { treeLayer.symbology = node; } @@ -85,7 +90,6 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ return treeLayers; } - // POST method code bleibt unverändert... const wmsNames = treeLayers.map(layer => layer.wmsName); const wmsStyles = treeLayers.map(layer => layer.wmsSelectedStyleName); let treeLayersByName = {}; @@ -100,6 +104,7 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ await wms.getLegendGraphic(wmsParams).then((response) => { for (const node of response.nodes) { + // If the layer has no symbology, there is no type property if (node.hasOwnProperty('type')) { treeLayersByName[node.name].symbology = node; } @@ -107,12 +112,20 @@ export async function updateLayerTreeLayersSymbology(treeLayers, method=HttpRequ return treeLayers; }).catch(async (error) => { console.error(error); + // If the request failed, try to get the legend graphic for each layer separately + // This is a workaround for the issue when QGIS server timed out when requesting + // the legend graphic for multiple layers at once (LAYER parameter with multiple values) if (treeLayers.length == 1) { + // If there is only one layer, there is no need to try to get the legend graphic + // for each layer separately return treeLayers; } if (!(error instanceof HttpError) || error.statusCode != 504) { + // If the error is not a timeout, there is no need to try to get the legend graphic + // for each layer separately return treeLayers; } + // Try to get the legend graphic for each layer separately for (const treeLayer of treeLayers) { await updateLayerTreeLayerSymbology(treeLayer); }