From 698d0314fc7fa7fd8082483653e7a54f64568000 Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 16 May 2025 13:08:18 +0200 Subject: [PATCH 01/49] Add legend count --- web/client/utils/LegendUtils.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js index cd9a0bb8d6..c9bdec4ce9 100644 --- a/web/client/utils/LegendUtils.js +++ b/web/client/utils/LegendUtils.js @@ -61,8 +61,8 @@ export const getWMSLegendConfig = ({ return { ...baseParams, ...(addContentDependantParams && { - // hideEmptyRules is applied for all layers except background layers - LEGEND_OPTIONS: `hideEmptyRules:${layer.group !== "background"};${legendOptions}`, + // hideEmptyRules and countMatched is applied for all layers except background layers + LEGEND_OPTIONS: `hideEmptyRules:${layer.group !== "background"};countMatched:${layer.group !== "background"};${legendOptions}`, SRCWIDTH: mapSize?.width ?? 512, SRCHEIGHT: mapSize?.height ?? 512, SRS: projection, @@ -121,4 +121,3 @@ export default { getWMSLegendConfig, updateLayerWithLegendFilters }; - From 8e18c13059d9056743f7cadb72887b390e33f34f Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 16 May 2025 13:12:25 +0200 Subject: [PATCH 02/49] ArcGISLegend able to manage dynamicLegend --- .../plugins/TOC/components/ArcGISLegend.jsx | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/web/client/plugins/TOC/components/ArcGISLegend.jsx b/web/client/plugins/TOC/components/ArcGISLegend.jsx index f6978c60a0..d232d2977b 100644 --- a/web/client/plugins/TOC/components/ArcGISLegend.jsx +++ b/web/client/plugins/TOC/components/ArcGISLegend.jsx @@ -9,6 +9,7 @@ import React, { useState, useEffect } from 'react'; import trimEnd from 'lodash/trimEnd'; import max from 'lodash/max'; +import assign from 'object-assign'; import axios from '../../../libs/ajax'; import Message from '../../../components/I18N/Message'; import Loader from '../../../components/misc/Loader'; @@ -17,24 +18,48 @@ import { getLayerIds } from '../../../utils/ArcGISUtils'; /** * ArcGISLegend renders legend from a MapServer or ImageServer service * @prop {object} node layer node options + * @prop {number} legendWidth width of the legend symbols + * @prop {number} legendHeight height of the legend symbols + * @prop {object} mapBbox map bounding box + * @prop {boolean} enableDynamicLegend if the displayed legends should be dynamic */ function ArcGISLegend({ - node = {} + node = {}, + onUpdateNode = null, + legendWidth = 12, + legendHeight = 12, + mapBbox, + enableDynamicLegend = false }) { const [legendData, setLegendData] = useState(null); const [error, setError] = useState(false); - const legendUrl = node.url ? `${trimEnd(node.url, '/')}/legend` : ''; + const legendUrl = node.url ? `${trimEnd(node.url, '/')}/${enableDynamicLegend && node.enableDynamicLegend ? 'queryLegends' : 'legend'}` : ''; useEffect(() => { if (legendUrl) { axios.get(legendUrl, { - params: { + params: assign({ f: 'json' - } + }, enableDynamicLegend && node.enableDynamicLegend ? { + bbox: Object.values(mapBbox.bounds ?? {}).join(',') || '', + bboxSR: mapBbox?.crs?.split(':')[1] ?? '', + // layers: 'show:' + node.options.layers.map(layer => layer.id).join(','), + size: `${(node.legendOptions?.legendWidth ?? legendWidth)},${(node.legendOptions?.legendHeight ?? legendHeight)}`, + format: 'png', + transparent: false, + timeRelation: 'esriTimeRelationOverlaps', + returnVisibleOnly: true + } : {}) }) - .then(({ data }) => setLegendData(data)) + .then(({ data }) => { + const dynamicLegendIsEmpty = data.layers.every(layer => layer.legend.length === 0); + if ((node.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { + onUpdateNode({ dynamicLegendIsEmpty }); + } + setLegendData(data); + }) .catch(() => setError(true)); } - }, [legendUrl]); + }, [legendUrl, mapBbox]); const supportedLayerIds = node.name !== undefined ? getLayerIds(node.name, node?.options?.layers || []) : []; From 6e93840f8bfef4090b81fec34b924dadd7832b8a Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 16 May 2025 13:45:01 +0200 Subject: [PATCH 03/49] Unused paramter deletion --- web/client/plugins/TOC/components/ArcGISLegend.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/client/plugins/TOC/components/ArcGISLegend.jsx b/web/client/plugins/TOC/components/ArcGISLegend.jsx index d232d2977b..c5c99da974 100644 --- a/web/client/plugins/TOC/components/ArcGISLegend.jsx +++ b/web/client/plugins/TOC/components/ArcGISLegend.jsx @@ -21,25 +21,23 @@ import { getLayerIds } from '../../../utils/ArcGISUtils'; * @prop {number} legendWidth width of the legend symbols * @prop {number} legendHeight height of the legend symbols * @prop {object} mapBbox map bounding box - * @prop {boolean} enableDynamicLegend if the displayed legends should be dynamic */ function ArcGISLegend({ node = {}, onUpdateNode = null, legendWidth = 12, legendHeight = 12, - mapBbox, - enableDynamicLegend = false + mapBbox }) { const [legendData, setLegendData] = useState(null); const [error, setError] = useState(false); - const legendUrl = node.url ? `${trimEnd(node.url, '/')}/${enableDynamicLegend && node.enableDynamicLegend ? 'queryLegends' : 'legend'}` : ''; + const legendUrl = node.url ? `${trimEnd(node.url, '/')}/${node.enableDynamicLegend ? 'queryLegends' : 'legend'}` : ''; useEffect(() => { if (legendUrl) { axios.get(legendUrl, { params: assign({ f: 'json' - }, enableDynamicLegend && node.enableDynamicLegend ? { + }, node.enableDynamicLegend ? { bbox: Object.values(mapBbox.bounds ?? {}).join(',') || '', bboxSR: mapBbox?.crs?.split(':')[1] ?? '', // layers: 'show:' + node.options.layers.map(layer => layer.id).join(','), From f325d89234045abec2b81d5c0c607487a4008de7 Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 16 May 2025 13:45:45 +0200 Subject: [PATCH 04/49] Trasmit --- web/client/plugins/TOC/components/DefaultLayer.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx index 673660267d..1f266ea587 100644 --- a/web/client/plugins/TOC/components/DefaultLayer.jsx +++ b/web/client/plugins/TOC/components/DefaultLayer.jsx @@ -96,6 +96,8 @@ const NodeLegend = ({
  • {visible ? : null}
  • From 0537fbb3d4f3b20a47c4269e2cd73a7081d85f76 Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 16 May 2025 13:46:04 +0200 Subject: [PATCH 05/49] Add dynamic legend check --- .../TOC/fragments/settings/Display.jsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/web/client/components/TOC/fragments/settings/Display.jsx b/web/client/components/TOC/fragments/settings/Display.jsx index a4b617188a..50e0f35062 100644 --- a/web/client/components/TOC/fragments/settings/Display.jsx +++ b/web/client/components/TOC/fragments/settings/Display.jsx @@ -401,6 +401,30 @@ export default class extends React.Component { } } + {this.props.element.type === "arcgis" && + +
    + + + + + + {!hideDynamicLegend && { + this.props.onChange("enableDynamicLegend", e.target.checked); + }} + checked={enableDynamicLegend || enableInteractiveLegend} > + +  } /> + } + + +
    +
    } ); } From 4446e1c3e8ed69b785fd9eb8ed5a071ab340b61e Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 16 May 2025 13:48:50 +0200 Subject: [PATCH 06/49] Remove unused parameters --- web/client/plugins/TOC/components/ArcGISLegend.jsx | 9 +-------- web/client/plugins/TOC/components/DefaultLayer.jsx | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/web/client/plugins/TOC/components/ArcGISLegend.jsx b/web/client/plugins/TOC/components/ArcGISLegend.jsx index c5c99da974..171dfc59c4 100644 --- a/web/client/plugins/TOC/components/ArcGISLegend.jsx +++ b/web/client/plugins/TOC/components/ArcGISLegend.jsx @@ -24,7 +24,6 @@ import { getLayerIds } from '../../../utils/ArcGISUtils'; */ function ArcGISLegend({ node = {}, - onUpdateNode = null, legendWidth = 12, legendHeight = 12, mapBbox @@ -48,13 +47,7 @@ function ArcGISLegend({ returnVisibleOnly: true } : {}) }) - .then(({ data }) => { - const dynamicLegendIsEmpty = data.layers.every(layer => layer.legend.length === 0); - if ((node.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { - onUpdateNode({ dynamicLegendIsEmpty }); - } - setLegendData(data); - }) + .then(({ data }) => setLegendData(data)) .catch(() => setError(true)); } }, [legendUrl, mapBbox]); diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx index 1f266ea587..2d49448536 100644 --- a/web/client/plugins/TOC/components/DefaultLayer.jsx +++ b/web/client/plugins/TOC/components/DefaultLayer.jsx @@ -96,7 +96,6 @@ const NodeLegend = ({
  • {visible ? : null}
  • From f662957c2c9a7baa908a91ffa4c45206d1da07db Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 09:44:32 +0200 Subject: [PATCH 07/49] Add Configs --- project/standard/templates/configs/pluginsConfig.json | 11 +++++++++++ web/client/configs/localConfig.json | 2 +- web/client/configs/pluginsConfig.json | 11 +++++++++++ web/client/configs/simple.json | 3 +++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/project/standard/templates/configs/pluginsConfig.json b/project/standard/templates/configs/pluginsConfig.json index 60be44cc5b..b9caa68ce3 100644 --- a/project/standard/templates/configs/pluginsConfig.json +++ b/project/standard/templates/configs/pluginsConfig.json @@ -139,6 +139,17 @@ "children": ["Permalink"], "autoEnableChildren": ["Permalink"] }, + { + "name": "DynamicLegend", + "glyph": "align-left", + "title": "plugins.DynamicLegend.title", + "description": "plugins.DynamicLegend.description", + "dependencies": [ + "Toolbar", + "BurgerMenu", + "SidebarMenu" + ] + }, { "name": "Permalink", "glyph": "link", diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index c861e0e98d..a09e55ad0e 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -412,7 +412,7 @@ { "name": "WidgetsTray" } ], "desktop": [ - "Details", + "Details","DynamicLegend", { "name": "BrandNavbar", "cfg": { diff --git a/web/client/configs/pluginsConfig.json b/web/client/configs/pluginsConfig.json index a0f2a18dde..010cce3cb6 100644 --- a/web/client/configs/pluginsConfig.json +++ b/web/client/configs/pluginsConfig.json @@ -139,6 +139,17 @@ "children": ["Permalink"], "autoEnableChildren": ["Permalink"] }, + { + "name": "DynamicLegend", + "glyph": "align-left", + "title": "plugins.DynamicLegend.title", + "description": "plugins.DynamicLegend.description", + "dependencies": [ + "Toolbar", + "BurgerMenu", + "SidebarMenu" + ] + }, { "name": "Permalink", "glyph": "link", diff --git a/web/client/configs/simple.json b/web/client/configs/simple.json index 28eff31633..20dc7ed2c2 100644 --- a/web/client/configs/simple.json +++ b/web/client/configs/simple.json @@ -76,6 +76,9 @@ } ], "desktop": [ + { + "name": "DynamicLegend" + }, { "name": "Map", "cfg": { From c2cd086df46608b9e9c9f5da931454e3c8512456 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 09:45:06 +0200 Subject: [PATCH 08/49] Add dynamic legend to plugin --- web/client/product/plugins.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index cbc5c76e6c..f65f458392 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -70,6 +70,7 @@ export const plugins = { DashboardImport: toModulePlugin('DashboardImport', () => import( /* webpackChunkName: 'plugins/dashboardImport' */'../plugins/DashboardImport')), DetailsPlugin: toModulePlugin('Details', () => import(/* webpackChunkName: 'plugins/details' */ '../plugins/Details')), DrawerMenuPlugin: toModulePlugin('DrawerMenu', () => import(/* webpackChunkName: 'plugins/drawerMenu' */ '../plugins/DrawerMenu')), + DynamicLegendPlugin: toModulePlugin('DynamicLegend', () => import(/* webpackChunkName: 'plugins/dynamiclegend' */ '../plugins/DynamicLegend')), ExpanderPlugin: toModulePlugin('Expander', () => import(/* webpackChunkName: 'plugins/expander' */ '../plugins/Expander')), FilterLayerPlugin: toModulePlugin('FilterLayer', () => import(/* webpackChunkName: 'plugins/filterLayer' */ '../plugins/FilterLayer')), FullScreenPlugin: toModulePlugin('FullScreen', () => import(/* webpackChunkName: 'plugins/fullScreen' */ '../plugins/FullScreen')), From 4d299a2f76732ba68f13e231a487f63d640e5ee1 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 09:46:24 +0200 Subject: [PATCH 09/49] Add enableDynamicLegend in TOC --- web/client/plugins/TOC/components/TOC.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/client/plugins/TOC/components/TOC.jsx b/web/client/plugins/TOC/components/TOC.jsx index c53bc8d795..825d0516b6 100644 --- a/web/client/plugins/TOC/components/TOC.jsx +++ b/web/client/plugins/TOC/components/TOC.jsx @@ -54,6 +54,7 @@ import { * @prop {object} config.groupOptions.tooltipOptions options for group title tooltip * @prop {object} config.layerOptions specific options for layer nodes * @prop {object} config.layerOptions.tooltipOptions options for layer title tooltip + * @prop {boolean} config.layerOptions.enableDynamicLegend make the legend dynamic * @prop {boolean} config.layerOptions.hideLegend hide the legend of the layer * @prop {object} config.layerOptions.legendOptions additional options for WMS legend * @prop {boolean} config.layerOptions.hideFilter hide the filter button in the layer nodes @@ -137,6 +138,7 @@ export function ControlledTOC({ * @prop {object} config.groupOptions.tooltipOptions options for group title tooltip * @prop {object} config.layerOptions specific options for layer nodes * @prop {object} config.layerOptions.tooltipOptions options for layer title tooltip + * @prop {boolean} config.layerOptions.enableDynamicLegend make the legend dynamic * @prop {boolean} config.layerOptions.hideLegend hide the legend of the layer * @prop {object} config.layerOptions.legendOptions additional options for WMS legend * @prop {boolean} config.layerOptions.hideFilter hide the filter button in the layer nodes From c563d7688d632e7fe0187b106131f85ced1c2ffc Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 09:56:13 +0200 Subject: [PATCH 10/49] Add dynamic legend capabilities --- web/client/plugins/TOC/components/ArcGISLegend.jsx | 12 ++++++++++-- web/client/plugins/TOC/components/Legend.jsx | 13 ++++++++++--- .../TOC/components/StyleBasedWMSJsonLegend.jsx | 10 ++++++++-- web/client/plugins/TOC/components/WMSLegend.jsx | 3 ++- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/web/client/plugins/TOC/components/ArcGISLegend.jsx b/web/client/plugins/TOC/components/ArcGISLegend.jsx index f6978c60a0..9274713121 100644 --- a/web/client/plugins/TOC/components/ArcGISLegend.jsx +++ b/web/client/plugins/TOC/components/ArcGISLegend.jsx @@ -17,9 +17,11 @@ import { getLayerIds } from '../../../utils/ArcGISUtils'; /** * ArcGISLegend renders legend from a MapServer or ImageServer service * @prop {object} node layer node options + * @prop {function} onUpdateNode return the changes of a specific node */ function ArcGISLegend({ - node = {} + node = {}, + onUpdateNode = () => {} }) { const [legendData, setLegendData] = useState(null); const [error, setError] = useState(false); @@ -31,7 +33,13 @@ function ArcGISLegend({ f: 'json' } }) - .then(({ data }) => setLegendData(data)) + .then(({ data }) => { + const dynamicLegendIsEmpty = data.layers.every(layer => layer.legend.length === 0); + if ((node.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { + onUpdateNode(node.id, 'layers', { dynamicLegendIsEmpty }); + } + setLegendData(data); + }) .catch(() => setError(true)); } }, [legendUrl]); diff --git a/web/client/plugins/TOC/components/Legend.jsx b/web/client/plugins/TOC/components/Legend.jsx index 7f2e3bada7..e5deb9cc1b 100644 --- a/web/client/plugins/TOC/components/Legend.jsx +++ b/web/client/plugins/TOC/components/Legend.jsx @@ -35,6 +35,7 @@ import { getWMSLegendConfig, LEGEND_FORMAT } from '../../../utils/LegendUtils'; * @prop {string} language current language code * @prop {number} legendWidth width of the legend symbols * @prop {number} legendHeight height of the legend symbols + * @prop {function} onUpdateNode return the changes of a specific node */ class Legend extends React.Component { static propTypes = { @@ -49,7 +50,8 @@ class Legend extends React.Component { language: PropTypes.string, projection: PropTypes.string, mapSize: PropTypes.object, - bbox: PropTypes.object + bbox: PropTypes.object, + onUpdateNode: PropTypes.func }; static defaultProps = { @@ -57,7 +59,8 @@ class Legend extends React.Component { legendWidth: 12, legendOptions: "forceLabels:on", style: {maxWidth: "100%"}, - scaleDependent: true + scaleDependent: true, + onUpdateNode: () => {} }; state = { error: false @@ -132,9 +135,13 @@ class Legend extends React.Component { validateImg = (img) => { // GeoServer response is a 1x2 px size when legend is not available. // In this case we need to show the "Legend Not available" message - if (img.height <= 1 && img.width <= 2) { + const imgError = img.height <= 1 && img.width <= 2; + if (imgError) { this.onImgError(); } + if ((this.props.layer.dynamicLegendIsEmpty ?? null) !== imgError) { + this.props.onUpdateNode(this.props.layer.id, 'layers', { dynamicLegendIsEmpty: imgError }); + } } } diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index a99f542437..f3e9be763a 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -46,7 +46,8 @@ class StyleBasedWMSJsonLegend extends React.Component { interactive: PropTypes.bool, // the indicator flag that refers if this legend is interactive or not projection: PropTypes.string, mapSize: PropTypes.object, - mapBbox: PropTypes.object + mapBbox: PropTypes.object, + onUpdateNode: PropTypes.func }; static defaultProps = { @@ -56,7 +57,8 @@ class StyleBasedWMSJsonLegend extends React.Component { style: {maxWidth: "100%"}, scaleDependent: true, onChange: () => {}, - interactive: false + interactive: false, + onUpdateNode: () => {} }; state = { error: false, @@ -104,6 +106,10 @@ class StyleBasedWMSJsonLegend extends React.Component { } this.setState({ loading: true }); getJsonWMSLegend(jsonLegendUrl).then(data => { + const dynamicLegendIsEmpty = data.length === 0 || data[0].rules.length === 0; + if ((this.props.layer.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { + this.props.onUpdateNode(this.props.layer.id, 'layers', { dynamicLegendIsEmpty }); + } this.setState({ jsonLegend: data[0], loading: false }); }).catch(() => { this.setState({ error: true, loading: false }); diff --git a/web/client/plugins/TOC/components/WMSLegend.jsx b/web/client/plugins/TOC/components/WMSLegend.jsx index bad721cc0f..ffd1422f33 100644 --- a/web/client/plugins/TOC/components/WMSLegend.jsx +++ b/web/client/plugins/TOC/components/WMSLegend.jsx @@ -68,7 +68,7 @@ class WMSLegend extends React.Component { this.setState({ containerWidth, ...this.state }); // eslint-disable-line -- TODO: need to be fixed } getLegendProps = () => { - return pick(this.props, ['currentZoomLvl', 'scales', 'scaleDependent', 'language', 'projection', 'mapSize', 'mapBbox']); + return pick(this.props, ['currentZoomLvl', 'scales', 'scaleDependent', 'language', 'projection', 'mapSize', 'mapBbox', 'enableDynamicLegend']); } render() { let node = this.props.node || {}; @@ -97,6 +97,7 @@ class WMSLegend extends React.Component { } legendOptions={this.props.WMSLegendOptions} {...this.getLegendProps()} + onUpdateNode={this.props.onChange} /> ); From 2eab47ce4ff47ea7a1fef9d641bbfce912b5b94c Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:06:34 +0200 Subject: [PATCH 11/49] Add fontAntiAliasing --- web/client/utils/LegendUtils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js index c9bdec4ce9..e328b9acda 100644 --- a/web/client/utils/LegendUtils.js +++ b/web/client/utils/LegendUtils.js @@ -58,11 +58,12 @@ export const getWMSLegendConfig = ({ }; if (layer.serverType !== ServerTypes.NO_VENDOR) { const addContentDependantParams = layer.enableDynamicLegend || layer.enableInteractiveLegend; + const layerisNotBackground = layer.group !== "background"; return { ...baseParams, ...(addContentDependantParams && { - // hideEmptyRules and countMatched is applied for all layers except background layers - LEGEND_OPTIONS: `hideEmptyRules:${layer.group !== "background"};countMatched:${layer.group !== "background"};${legendOptions}`, + // hideEmptyRules, countMatched and fontAntiAliasing are applied for all layers except background layers + LEGEND_OPTIONS: `hideEmptyRules:${layerisNotBackground};countMatched:${layerisNotBackground};fontAntiAliasing:${layerisNotBackground};${legendOptions}`, SRCWIDTH: mapSize?.width ?? 512, SRCHEIGHT: mapSize?.height ?? 512, SRS: projection, From ad6aca66a60ae17acc61f2e9a08b2ce9c44384f5 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:16:21 +0200 Subject: [PATCH 12/49] Add plugin code --- web/client/plugins/DynamicLegend.jsx | 78 +++++++++++++ .../dynamicLegend/assets/dynamicLegend.css | 8 ++ .../components/DynamicLegend.jsx | 104 ++++++++++++++++++ web/client/selectors/dynamiclegend.js | 12 ++ 4 files changed, 202 insertions(+) create mode 100644 web/client/plugins/DynamicLegend.jsx create mode 100644 web/client/plugins/dynamicLegend/assets/dynamicLegend.css create mode 100644 web/client/plugins/dynamicLegend/components/DynamicLegend.jsx create mode 100644 web/client/selectors/dynamiclegend.js diff --git a/web/client/plugins/DynamicLegend.jsx b/web/client/plugins/DynamicLegend.jsx new file mode 100644 index 0000000000..0a361f2c31 --- /dev/null +++ b/web/client/plugins/DynamicLegend.jsx @@ -0,0 +1,78 @@ +/* eslint-disable react/jsx-boolean-value */ +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { get } from 'lodash'; +import { Glyphicon } from 'react-bootstrap'; + +import { createPlugin } from '../utils/PluginsUtils'; +import { groupsSelector, layersSelector } from '../selectors/layers'; +import { keepLayer } from '../selectors/dynamiclegend'; +import { mapSelector } from '../selectors/map'; +import { updateNode } from '../actions/layers'; +import controls from '../reducers/controls'; +import { toggleControl } from '../actions/controls'; +import Message from '../components/I18N/Message'; + +import DynamicLegend from './dynamicLegend/components/DynamicLegend'; + +export default createPlugin('DynamicLegend', { + component: connect( + createSelector([ + (state) => get(state, 'controls.dynamic-legend.enabled'), + groupsSelector, + layersSelector, + mapSelector + ], (isVisible, groups, layers, map) => ({ + isVisible, + groups, + layers: layers.filter(keepLayer), + currentZoomLvl: map?.zoom, + mapBbox: map?.bbox + })), + { + onClose: toggleControl.bind(null, 'dynamic-legend', null), + onUpdateNode: updateNode + } + )(DynamicLegend), + options: { + disablePluginIf: "{state('router') && (state('router').endsWith('new') || state('router').includes('newgeostory') || state('router').endsWith('dashboard'))}" + }, + reducers: { controls }, + epics: {}, + containers: { + BurgerMenu: { + name: 'dynamic-legend', + position: 1000, + priority: 2, + doNotHide: true, + text: , + tooltip: , + icon: , + action: toggleControl.bind(null, 'dynamic-legend', null), + toggle: true + }, + SidebarMenu: { + name: 'dynamic-legend', + position: 1000, + priority: 1, + doNotHide: true, + text: , + tooltip: , + icon: , + action: toggleControl.bind(null, 'dynamic-legend', null), + toggle: true + }, + Toolbar: { + name: 'dynamic-legend', + alwaysVisible: true, + position: 2, + priority: 0, + doNotHide: true, + tooltip: , + icon: , + action: toggleControl.bind(null, 'dynamic-legend', null), + toggle: true + } + } +}); diff --git a/web/client/plugins/dynamicLegend/assets/dynamicLegend.css b/web/client/plugins/dynamicLegend/assets/dynamicLegend.css new file mode 100644 index 0000000000..8eb750ba04 --- /dev/null +++ b/web/client/plugins/dynamicLegend/assets/dynamicLegend.css @@ -0,0 +1,8 @@ +.ms-resizable-modal > .modal-content.legend-dialog { + top: 0vh; + right: -100vw; +} + +.legend-content * .ms-node-title { + font-weight: bold !important +} diff --git a/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx b/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx new file mode 100644 index 0000000000..5b71d4a35b --- /dev/null +++ b/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { keepNode, isLayerVisible } from '../../../selectors/dynamiclegend'; +import { getResolutions } from '../../../utils/MapUtils'; +import Message from '../../../components/I18N/Message'; +import ResizableModal from '../../../components/misc/ResizableModal'; +import { ControlledTOC } from '../../TOC/components/TOC'; +import DefaultGroup from '../../TOC/components/DefaultGroup'; +import DefaultLayer from '../../TOC/components/DefaultLayer'; + +import '../assets/dynamicLegend.css'; + +function applyVersionParamToLegend(layer) { + // we need to pass a parameter that invalidate the cache for GetLegendGraphic + // all layer inside the dataset viewer apply a new _v_ param each time we switch page + return { ...layer, legendParams: { ...layer?.legendParams, _v_: layer?._v_ } }; +} + +function filterNode(node, currentResolution) { + const nodes = Array.isArray(node.nodes) ? node.nodes.filter(keepNode).map(applyVersionParamToLegend).map(n => filterNode(n, currentResolution)) : undefined; + + return { + ...node, + isVisible: (node.visibility ?? true) && (nodes ? nodes.length > 0 && nodes.some(n => n.isVisible) : isLayerVisible(node, currentResolution)), + ...(nodes && { nodes }) + }; +} + +export default ({ + layers, + onUpdateNode, + currentZoomLvl, + onClose, + isVisible, + groups, + mapBbox +}) => { + const layerDict = layers.reduce((acc, layer) => ({ + ...acc, + ...{ + [layer.id]: { + ...layer, + ...{enableDynamicLegend: true, enableInteractiveLegend: false} + } + } + }), {}); + const getVisibilityStyle = nodeVisibility => ({ + opacity: nodeVisibility ? 1 : 0, + height: nodeVisibility ? "auto" : "0" + }); + + const customGroupNodeComponent = props => ( +
    + +
    + ); + const customLayerNodeComponent = props => { + const layer = layerDict[props.node.id]; + if (!layer) { + return null; + } + + return ( +
    + +
    + ); + }; + + return ( + } + dialogClassName=" legend-dialog" + show={isVisible} + draggable + style={{zIndex: 1993}}> + + + ); +}; diff --git a/web/client/selectors/dynamiclegend.js b/web/client/selectors/dynamiclegend.js new file mode 100644 index 0000000000..7a331e4da3 --- /dev/null +++ b/web/client/selectors/dynamiclegend.js @@ -0,0 +1,12 @@ +const isLayer = node => node.nodeType === "layers"; + +export const keepLayer = layer => (layer.group !== 'background' && ['wms', 'arcgis'].includes(layer.type)); + +export const keepNode = node => !isLayer(node) || keepLayer(node); + +export const isLayerVisible = (node, currentResolution) => keepLayer(node) + && (!node.hasOwnProperty('dynamicLegendIsEmpty') || !node.dynamicLegendIsEmpty) + && ( + (!node.minResolution || node.minResolution <= currentResolution) && + (!node.maxResolution || node.maxResolution > currentResolution) + ); From 7379eb17d752cdee9847bbda8564f628e1bcabed Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:29:04 +0200 Subject: [PATCH 13/49] Merge Correction --- web/client/plugins/TOC/components/ArcGISLegend.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/client/plugins/TOC/components/ArcGISLegend.jsx b/web/client/plugins/TOC/components/ArcGISLegend.jsx index c053d1d818..767073bacc 100644 --- a/web/client/plugins/TOC/components/ArcGISLegend.jsx +++ b/web/client/plugins/TOC/components/ArcGISLegend.jsx @@ -27,8 +27,7 @@ function ArcGISLegend({ node = {}, legendWidth = 12, legendHeight = 12, - mapBbox, - node = {}, + mapBbox = {}, onUpdateNode = () => {} }) { const [legendData, setLegendData] = useState(null); From 014dbaea22c896e036ba387cfb40c38cbe09b128 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:39:14 +0200 Subject: [PATCH 14/49] Add Translations --- web/client/translations/data.de-DE.json | 41 +++++++++++++++++++++++++ web/client/translations/data.en-US.json | 41 +++++++++++++++++++++++++ web/client/translations/data.es-ES.json | 41 +++++++++++++++++++++++++ web/client/translations/data.fr-FR.json | 41 +++++++++++++++++++++++++ web/client/translations/data.it-IT.json | 41 +++++++++++++++++++++++++ 5 files changed, 205 insertions(+) diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 313ee87606..c4440423fa 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -1567,6 +1567,43 @@ } } }, + "select": { + "title": "Auswählen", + "tooltip": "Auswahlwerkzeug anzeigen", + "description": "Auswahlwerkzeug anzeigen", + "hasReachMaxCount": "Maximale Anzahl von Elementen erreicht", + "selection": "Auswahl", + "allLayers": "Alle Schichten", + "featuresCount": "Featuresanzahl", + "button": { + "select": "Auswahlmodus", + "chooseGeometry": "Wählen", + "selectByPoint": "Punkt", + "selectByLine": "Linie", + "selectByCircle": "Kreis", + "selectByRectangle": "Rechteck", + "selectByPolygon": "Polygon", + "clear": "Löschen", + "zoomTo": "Zoomen auf", + "statistics": "Statistiken", + "createLayer": "Ebene erstellen", + "filterData": "Daten filtern", + "export": "Exportieren", + "exportToCsv": "Exportieren nach CSV", + "exportToJson": "Exportieren nach JSON", + "exportToGeoJson": "Exportieren nach GeoJSON" + }, + "statistics": { + "title": "Statistiken", + "field": "Feld", + "count": "Anzahl der Werte", + "sum": "Summe der Werte", + "min": "Minimum", + "max": "Maximum", + "avg": "Durchschnitt", + "std": "Standardabweichung" + } + }, "snapshot": { "title": "Snapshot Vorschau", "save": "Speichern", @@ -3470,6 +3507,10 @@ "description": "Ermöglicht das Erstellen eines Permalinks der aktuell angezeigten Ressource", "title": "Permalink" }, + "Select": { + "title": "Auswählen", + "tooltip": "Auswahlwerkzeug anzeigen" + }, "StreetView": { "title": "Street-View", "description": "Tool zum Durchsuchen von Google Street View-Bildern auf der Karte" diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 7f4af13a91..220f35a704 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -1528,6 +1528,43 @@ } } }, + "select": { + "title": "Select", + "tooltip": "Display the selection tool", + "description": "Display the selection tool", + "hasReachMaxCount": "Maximum number of elements reached", + "selection": "Selection", + "allLayers": "All layers", + "featuresCount": "Features number", + "button": { + "select": "Selection mode", + "chooseGeometry": "Choose", + "selectByPoint": "Point", + "selectByLine": "Line", + "selectByCircle": "Circle", + "selectByRectangle": "Rectangle", + "selectByPolygon": "Polygon", + "clear": "Clear", + "zoomTo": "Zoom to", + "statistics": "Statistics", + "createLayer": "Create layer", + "filterData": "Filter data", + "export": "Export", + "exportToCsv": "Export to CSV", + "exportToJson": "Export to JSON", + "exportToGeoJson": "Export to GeoJSON" + }, + "statistics": { + "title": "Statistics", + "field": "Field", + "count": "Number of values", + "sum": "Sum of values", + "min": "Minimum", + "max": "Maximum", + "avg": "Average", + "std": "Standard deviation" + } + }, "snapshot": { "title": "Snapshot Preview", "save": "Save", @@ -3441,6 +3478,10 @@ "description": "Allows to create a permalink of the current resource in view", "title": "Permalink" }, + "Select": { + "title": "Select", + "tooltip": "Kartenlegende anzeigen" + }, "StreetView": { "title": "Street View", "description": "Street view tool for browsing Google street view images from the map" diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 2ae860ae11..644b2cb660 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -1528,6 +1528,43 @@ } } }, + "select": { + "title": "Seleccionar", + "tooltip": "Mostrar la herramienta de selección", + "description": "Mostrar la herramienta de selección", + "hasReachMaxCount": "Número máximo de elementos alcanzado", + "selection": "Selección", + "allLayers": "Todas las capas", + "featuresCount": "Número de entidades", + "button": { + "select": "Modo de selección", + "chooseGeometry": "Elegir", + "selectByPoint": "Punto", + "selectByLine": "Línea", + "selectByCircle": "Círculo", + "selectByRectangle": "Rectángulo", + "selectByPolygon": "Polígono", + "clear": "Borrar", + "zoomTo": "Hacer zoom en", + "statistics": "Estadísticas", + "createLayer": "Crear capa", + "filterData": "Filtrar datos", + "export": "Exportar", + "exportToCsv": "Exportar a CSV", + "exportToJson": "Exportar a JSON", + "exportToGeoJson": "Exportar a GeoJSON" + }, + "statistics": { + "title": "Estadísticas", + "field": "Campo", + "count": "Número de valores", + "sum": "Suma de los valores", + "min": "Mínimo", + "max": "Máximo", + "avg": "Promedio", + "std": "Desviación estándar" + } + }, "snapshot": { "title": "Previsualización de la captura del mapa", "save": "Guardar", @@ -3431,6 +3468,10 @@ "description": "Permite crear un enlace permanente del recurso actual a la vista", "title": "Enlace permanente" }, + "Select": { + "title": "Seleccionar", + "tooltip": "Mostrar la herramienta de selección" + }, "StreetView": { "title": "Street View", "description": "Herramienta para buscar imágenes de Google Street View desde el mapa" diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 890f97c50a..834c6a3787 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -1529,6 +1529,43 @@ } } }, + "select": { + "title": "Sélectionner", + "tooltip": "Afficher l'outil de sélection", + "description": "Afficher l'outil de sélection", + "hasReachMaxCount": "Nombre d'éléments maximum atteints", + "selection": "Sélection", + "allLayers": "Toutes les couches", + "featuresCount": "Nombre d'entités", + "button": { + "select": "Mode de sélection", + "chooseGeometry": "Choisir", + "selectByPoint": "Point", + "selectByLine": "Ligne", + "selectByCircle": "Cercle", + "selectByRectangle": "Rectangle", + "selectByPolygon": "Polygone", + "clear": "Effacer", + "zoomTo": "Zoomer sur", + "statistics": "Statistiques", + "createLayer": "Créer une couche", + "filterData": "Filtrer les données", + "export": "Exporter", + "exportToCsv": "Exporter en CSV", + "exportToJson": "Exporter en JSON", + "exportToGeoJson": "Exporter en GeoJSON" + }, + "statistics": { + "title": "Statistiques", + "field": "Champs", + "count": "Nombre de valeurs", + "sum": "Somme des valeurs", + "min": "Minimum", + "max": "Maximum", + "avg": "Moyenne", + "std": "Écart type" + } + }, "snapshot": { "title": "Prévisualisation de la capture de la carte", "save": "Sauver", @@ -3432,6 +3469,10 @@ "description": "Permet de créer un permalien de la ressource courante en vue", "title": "Lien permanent" }, + "Select": { + "title": "Sélectionner", + "tooltip": "Afficher l'outil de sélection" + }, "StreetView": { "title": "Street View", "description": "Outil pour parcourir les images Google Street View à partir de la carte" diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index e78b24aaad..c19033473e 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -1528,6 +1528,43 @@ } } }, + "select": { + "title": "Selezionare", + "tooltip": "Mostra lo strumento di selezione", + "description": "Mostra lo strumento di selezione", + "hasReachMaxCount": "Numero massimo di elementi raggiunto", + "selection": "Selezione", + "allLayers": "Tutti i livelli", + "featuresCount": "Numero di entità", + "button": { + "select": "Modalità di selezione", + "chooseGeometry": "Scegliere", + "selectByPoint": "Punto", + "selectByLine": "Linea", + "selectByCircle": "Cerchio", + "selectByRectangle": "Rettangolo", + "selectByPolygon": "Poligono", + "clear": "Cancella", + "zoomTo": "Zoom su", + "statistics": "Statistiche", + "createLayer": "Crea livello", + "filterData": "Filtra dati", + "export": "Esporta", + "exportToCsv": "Esporta in CSV", + "exportToJson": "Esporta in JSON", + "exportToGeoJson": "Esporta in GeoJSON" + }, + "statistics": { + "title": "Statistiche", + "field": "Campo", + "count": "Numero di valori", + "sum": "Somma dei valori", + "min": "Minimo", + "max": "Massimo", + "avg": "Media", + "std": "Deviazione standard" + } + }, "snapshot": { "title": "Istantanea", "save": "Salva", @@ -3433,6 +3470,10 @@ "description": "Permette di creare un permalink della risorsa attualmente in vista", "title": "Permalink" }, + "Select": { + "title": "Selezionare", + "tooltip": "Mostra lo strumento di selezione" + }, "StreetView": { "title": "Street View", "description": "Strumento Street view, per visualizzare le immagini di Google Street View dalla mappa" From 8977b5b976b0bd91fade2f68550fae02c445b788 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:41:20 +0200 Subject: [PATCH 15/49] Add configs --- .../templates/configs/pluginsConfig.json | 11 +++++++++ web/client/configs/localConfig.json | 23 +++++++++++++++++++ web/client/configs/pluginsConfig.json | 11 +++++++++ web/client/configs/simple.json | 23 +++++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/project/standard/templates/configs/pluginsConfig.json b/project/standard/templates/configs/pluginsConfig.json index 60be44cc5b..d330878a0e 100644 --- a/project/standard/templates/configs/pluginsConfig.json +++ b/project/standard/templates/configs/pluginsConfig.json @@ -146,6 +146,17 @@ "description": "plugins.Permalink.description", "denyUserSelection": true }, + { + "name": "Select", + "glyph": "hand-down", + "title": "plugins.Select.title", + "description": "plugins.Select.description", + "dependencies": [ + "Toolbar", + "BurgerMenu", + "SidebarMenu" + ] + }, { "name": "BackgroundSelector", "title": "plugins.BackgroundSelector.title", diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index c861e0e98d..e1b4558aa9 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -419,6 +419,29 @@ "containerPosition": "header" } }, + { + "name": "Select", + "cfg": { + "highlightOptions": { + "color": "#3388ff", + "dashArray": "", + "fillColor": "#3388ff", + "fillOpacity": 0.2, + "radius": 4, + "weight": 4 + }, + "queryOptions": { + "maxCount": -1 + }, + "selectTools": [ + "Point", + "Line", + "Circle", + "Rectangle", + "Polygon" + ] + } + }, { "name": "SecurityPopup" }, { "name": "Map", diff --git a/web/client/configs/pluginsConfig.json b/web/client/configs/pluginsConfig.json index a0f2a18dde..2eebde5412 100644 --- a/web/client/configs/pluginsConfig.json +++ b/web/client/configs/pluginsConfig.json @@ -145,6 +145,17 @@ "title": "plugins.Permalink.title", "description": "plugins.Permalink.description" }, + { + "name": "Select", + "glyph": "hand-down", + "title": "plugins.Select.title", + "description": "plugins.Select.description", + "dependencies": [ + "Toolbar", + "BurgerMenu", + "SidebarMenu" + ] + }, { "name": "BackgroundSelector", "title": "plugins.BackgroundSelector.title", diff --git a/web/client/configs/simple.json b/web/client/configs/simple.json index 28eff31633..c69cfcbd81 100644 --- a/web/client/configs/simple.json +++ b/web/client/configs/simple.json @@ -89,6 +89,29 @@ "zoomControl": false } }, + { + "name": "SelectExtension", + "cfg": { + "highlightOptions": { + "color": "#3388ff", + "dashArray": "", + "fillColor": "#3388ff", + "fillOpacity": 0.2, + "radius": 4, + "weight": 4 + }, + "queryOptions": { + "maxCount": -1 + }, + "selectTools": [ + "Point", + "Line", + "Circle", + "Rectangle", + "Polygon" + ] + } + }, { "name": "Help" }, From dc8f867c5559c75311fff736e8b643953cdae564 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:41:52 +0200 Subject: [PATCH 16/49] Add Select to plugins --- web/client/product/plugins.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index cbc5c76e6c..d80259deff 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -118,6 +118,7 @@ export const plugins = { SidebarMenuPlugin: toModulePlugin('SidebarMenu', () => import(/* webpackChunkName: 'plugins/sidebarMenu' */ '../plugins/SidebarMenu')), SharePlugin: toModulePlugin('Share', () => import(/* webpackChunkName: 'plugins/share' */ '../plugins/Share')), PermalinkPlugin: toModulePlugin('Permalink', () => import(/* webpackChunkName: 'plugins/permalink' */ '../plugins/Permalink')), + Select: toModulePlugin('Select', () => import(/* webpackChunkName: 'plugins/snapshot' */ '../plugins/Select')), SnapshotPlugin: toModulePlugin('Snapshot', () => import(/* webpackChunkName: 'plugins/snapshot' */ '../plugins/Snapshot')), StreetView: toModulePlugin('StreetView', () => import(/* webpackChunkName: 'plugins/streetView' */ '../plugins/StreetView')), StyleEditor: toModulePlugin('StyleEditor', () => import(/* webpackChunkName: 'plugins/styleEditor' */ '../plugins/StyleEditor')), From 21d1a3be4706c7ae8427cdb076a324637599f619 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:42:55 +0200 Subject: [PATCH 17/49] Correction on getCQLGeometryElement --- web/client/utils/FilterUtils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/client/utils/FilterUtils.js b/web/client/utils/FilterUtils.js index d6ff0bfe83..22ecb8fe97 100644 --- a/web/client/utils/FilterUtils.js +++ b/web/client/utils/FilterUtils.js @@ -781,6 +781,7 @@ export const getCQLGeometryElement = function(coordinates, type) { geometry += coordinates.join(" "); break; case "MultiPoint": + case "LineString": coordinates.forEach((position, index) => { geometry += position.join(" "); geometry += index < coordinates.length - 1 ? ", " : ""; From be98cad2fb95d7eb321fb420c045c208e250012e Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:46:35 +0200 Subject: [PATCH 18/49] Add treeHeadr to TOC and LayerTree --- web/client/plugins/TOC/components/LayersTree.jsx | 5 ++++- web/client/plugins/TOC/components/TOC.jsx | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/web/client/plugins/TOC/components/LayersTree.jsx b/web/client/plugins/TOC/components/LayersTree.jsx index 94dd104080..d0a6b677cf 100644 --- a/web/client/plugins/TOC/components/LayersTree.jsx +++ b/web/client/plugins/TOC/components/LayersTree.jsx @@ -68,6 +68,7 @@ const loopGroupCondition = (groupNode, condition) => { * @prop {string} noFilteredResultsMsgId message id for no result on filter * @prop {object} config optional configuration available for the nodes * @prop {boolean} config.sortable activate the possibility to sort nodes + * @prop {component} treeHeader display a header on top of the layer tree */ const LayersTree = ({ tree, @@ -90,7 +91,8 @@ const LayersTree = ({ nodeToolItems, nodeContentItems, singleDefaultGroup = isSingleDefaultGroup(tree), - theme + theme, + treeHeader }) => { const containerNode = useRef(); @@ -151,6 +153,7 @@ const LayersTree = ({ event.preventDefault(); }} > + {treeHeader ?? null} {(root || []).map((node, index) => { return ( ); } @@ -140,6 +143,7 @@ export function ControlledTOC({ * @prop {boolean} config.layerOptions.hideLegend hide the legend of the layer * @prop {object} config.layerOptions.legendOptions additional options for WMS legend * @prop {boolean} config.layerOptions.hideFilter hide the filter button in the layer nodes + * @prop {component} treeHeader display a header on top of the layer tree */ function TOC({ map = { layers: [], groups: [] }, @@ -154,7 +158,8 @@ function TOC({ singleDefaultGroup, nodeItems, theme, - filterText + filterText, + treeHeader }) { const { layers } = splitMapAndLayers(map) || {}; const tree = denormalizeGroups(layers.flat || [], layers.groups || []).groups; @@ -218,6 +223,7 @@ function TOC({ nodeToolItems={nodeToolItems} nodeContentItems={nodeContentItems} singleDefaultGroup={singleDefaultGroup} + treeHeader={treeHeader} /> ); } From ca0a49445a3298f0fef8db2f99ab27c32378ef47 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:47:34 +0200 Subject: [PATCH 19/49] =?UTF-8?q?tooltip=20=E2=86=92=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/client/translations/data.de-DE.json | 2 +- web/client/translations/data.en-US.json | 2 +- web/client/translations/data.es-ES.json | 2 +- web/client/translations/data.fr-FR.json | 2 +- web/client/translations/data.it-IT.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index c4440423fa..5328975c8f 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -3509,7 +3509,7 @@ }, "Select": { "title": "Auswählen", - "tooltip": "Auswahlwerkzeug anzeigen" + "description": "Auswahlwerkzeug anzeigen" }, "StreetView": { "title": "Street-View", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 220f35a704..8a7dd9c67b 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -3480,7 +3480,7 @@ }, "Select": { "title": "Select", - "tooltip": "Kartenlegende anzeigen" + "description": "Kartenlegende anzeigen" }, "StreetView": { "title": "Street View", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 644b2cb660..f8464a49ea 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -3470,7 +3470,7 @@ }, "Select": { "title": "Seleccionar", - "tooltip": "Mostrar la herramienta de selección" + "description": "Mostrar la herramienta de selección" }, "StreetView": { "title": "Street View", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 834c6a3787..64aa029be6 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -3471,7 +3471,7 @@ }, "Select": { "title": "Sélectionner", - "tooltip": "Afficher l'outil de sélection" + "description": "Afficher l'outil de sélection" }, "StreetView": { "title": "Street View", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index c19033473e..a89082f8fd 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -3472,7 +3472,7 @@ }, "Select": { "title": "Selezionare", - "tooltip": "Mostra lo strumento di selezione" + "description": "Mostra lo strumento di selezione" }, "StreetView": { "title": "Street View", From cb7625bb31b485491497e86191a6ddf867d912d2 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:48:55 +0200 Subject: [PATCH 20/49] =?UTF-8?q?Salect=20=E2=86=92=20SelectPlugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/client/product/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index d80259deff..e97b2f0b19 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -118,7 +118,7 @@ export const plugins = { SidebarMenuPlugin: toModulePlugin('SidebarMenu', () => import(/* webpackChunkName: 'plugins/sidebarMenu' */ '../plugins/SidebarMenu')), SharePlugin: toModulePlugin('Share', () => import(/* webpackChunkName: 'plugins/share' */ '../plugins/Share')), PermalinkPlugin: toModulePlugin('Permalink', () => import(/* webpackChunkName: 'plugins/permalink' */ '../plugins/Permalink')), - Select: toModulePlugin('Select', () => import(/* webpackChunkName: 'plugins/snapshot' */ '../plugins/Select')), + SelectPlugin: toModulePlugin('Select', () => import(/* webpackChunkName: 'plugins/snapshot' */ '../plugins/Select')), SnapshotPlugin: toModulePlugin('Snapshot', () => import(/* webpackChunkName: 'plugins/snapshot' */ '../plugins/Snapshot')), StreetView: toModulePlugin('StreetView', () => import(/* webpackChunkName: 'plugins/streetView' */ '../plugins/StreetView')), StyleEditor: toModulePlugin('StyleEditor', () => import(/* webpackChunkName: 'plugins/styleEditor' */ '../plugins/StyleEditor')), From 1ea363c808cb8871f7bd89b7211884d199bfe6c3 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 10:59:06 +0200 Subject: [PATCH 21/49] Add select Code --- web/client/actions/select.js | 25 ++ web/client/epics/select.js | 232 ++++++++++++++++++ web/client/plugins/Select.jsx | 88 +++++++ web/client/plugins/select/assets/select.css | 40 +++ .../EllipsisButton/EllipsisButton.css | 57 +++++ .../EllipsisButton/EllipsisButton.jsx | 226 +++++++++++++++++ .../EllipsisButton/Statistics/Statistics.css | 36 +++ .../EllipsisButton/Statistics/Statistics.jsx | 75 ++++++ .../plugins/select/components/Select.jsx | 144 +++++++++++ .../components/SelectHeader/SelectHeader.css | 91 +++++++ .../components/SelectHeader/SelectHeader.jsx | 84 +++++++ web/client/reducers/select.js | 20 ++ web/client/selectors/select.js | 19 ++ web/client/utils/Select.js | 63 +++++ 14 files changed, 1200 insertions(+) create mode 100644 web/client/actions/select.js create mode 100644 web/client/epics/select.js create mode 100644 web/client/plugins/Select.jsx create mode 100644 web/client/plugins/select/assets/select.css create mode 100644 web/client/plugins/select/components/EllipsisButton/EllipsisButton.css create mode 100644 web/client/plugins/select/components/EllipsisButton/EllipsisButton.jsx create mode 100644 web/client/plugins/select/components/EllipsisButton/Statistics/Statistics.css create mode 100644 web/client/plugins/select/components/EllipsisButton/Statistics/Statistics.jsx create mode 100644 web/client/plugins/select/components/Select.jsx create mode 100644 web/client/plugins/select/components/SelectHeader/SelectHeader.css create mode 100644 web/client/plugins/select/components/SelectHeader/SelectHeader.jsx create mode 100644 web/client/reducers/select.js create mode 100644 web/client/selectors/select.js create mode 100644 web/client/utils/Select.js diff --git a/web/client/actions/select.js b/web/client/actions/select.js new file mode 100644 index 0000000000..5da21da1eb --- /dev/null +++ b/web/client/actions/select.js @@ -0,0 +1,25 @@ +export const SELECT_CLEAN_SELECTION = "SELECT:CLEAN_SELECTION"; +export const SELECT_STORE_CFG = "SELECT:STORE_CFG"; +export const ADD_OR_UPDATE_SELECTION = "SELECT:ADD_OR_UPDATE_SELECTION"; + +export function cleanSelection(geomType) { + return { + type: SELECT_CLEAN_SELECTION, + geomType + }; +} + +export function storeConfiguration(cfg) { + return { + type: SELECT_STORE_CFG, + cfg + }; +} + +export function addOrUpdateSelection(layer, geoJsonData) { + return { + type: ADD_OR_UPDATE_SELECTION, + layer, + geoJsonData + }; +} diff --git a/web/client/epics/select.js b/web/client/epics/select.js new file mode 100644 index 0000000000..132b9ae17b --- /dev/null +++ b/web/client/epics/select.js @@ -0,0 +1,232 @@ +import { Observable } from 'rxjs'; +import axios from 'axios'; +import assign from 'object-assign'; + +import { SET_CONTROL_PROPERTY, TOGGLE_CONTROL } from '../actions/controls'; +import { UPDATE_NODE, REMOVE_NODE } from '../actions/layers'; +import { changeDrawingStatus, END_DRAWING } from '../actions/draw'; +import { registerEventListener, unRegisterEventListener} from '../actions/map'; +import { shutdownToolOnAnotherToolDrawing } from "../utils/ControlUtils"; +import { describeFeatureType, getFeatureURL } from '../api/WFS'; +import { extractGeometryAttributeName } from '../utils/WFSLayerUtils'; +import { mergeOptionsByOwner, removeAdditionalLayer } from '../actions/additionallayers'; +import { highlightStyleSelector } from '../selectors/mapInfo'; +import { layersSelector, groupsSelector } from '../selectors/layers'; +import { flattenArrayOfObjects, getInactiveNode } from '../utils/LayersUtils'; + +import { optionsToVendorParams } from '../utils/VendorParamsUtils'; +import { selectLayersSelector, isSelectEnabled, filterLayerForSelect, isSelectQueriable, getSelectQueryMaxFeatureCount, getSelectHighlightOptions } from '../selectors/select'; +import { SELECT_CLEAN_SELECTION, ADD_OR_UPDATE_SELECTION, addOrUpdateSelection } from '../actions/select'; +import { buildAdditionalLayerId, buildAdditionalLayerOwnerName, arcgisToGeoJSON, makeCrsValid, customUpdateAdditionalLayer } from '../utils/Select'; + +const queryLayer = (layer, geometry, selectQueryMaxCount) => { + switch (layer.type) { + case 'arcgis': { + const parsedGeometry = JSON.stringify({ + spatialReference: { wkid: geometry.projection.split(':')[1] }, + ...(geometry.type === 'Point' + ? { x: geometry.coordinates[0], y: geometry.coordinates[1] } + : (geometry.type === 'LineString' ? + { 'paths': [geometry.coordinates] } : + { 'rings': geometry.coordinates } + ) + ) + }); + const geometryType = geometry.type === 'Point' ? "esriGeometryPoint" : (geometry.type === 'LineString' ? 'esriGeometryPolyline' : 'esriGeometryPolygon'); + const singleLayerId = parseInt(layer.name ?? '', 10); + return Promise.all((Number.isInteger(singleLayerId) ? layer.options.layers.filter(l => l.id === singleLayerId) : layer.options.layers).map(l => axios.get(`${layer.url}/${l.id}`, { params: { f: 'json'} }) + .then(describe => + axios.get(`${layer.url}/${l.id}/query`, { + params: assign({ + f: "json", + geometry: parsedGeometry, + geometryType: geometryType, + spatialRel: "esriSpatialRelIntersects", + where: '1=1', + outFields: '*' + }, describe.data.advancedQueryCapabilities.supportsPagination && selectQueryMaxCount > -1 ? { resultRecordCount: selectQueryMaxCount } : {} + )}) + .then(response => ({ features: arcgisToGeoJSON(response.data.features, describe.data.name, response.data.fields.find(field => field.type === 'esriFieldTypeOID')?.name ?? response.data.objectIdFieldName ?? 'objectid'), crs: makeCrsValid(describe.data.sourceSpatialReference.wkid.toString()) })) + .catch(err => { throw new Error(`Error while querying layer: ${err.message}`); }) + ))) + .then(responses => responses.reduce((acc, response) => { + const features = [...acc.features, ...response.features]; + return {...acc, ...{ + features: selectQueryMaxCount > -1 && features.length > selectQueryMaxCount ? features.slice(0, selectQueryMaxCount) : features, + totalFeatures: acc.totalFeatures + response.features.length, + numberMatched: acc.numberMatched + response.features.length, + numberReturned: acc.numberReturned + response.features.length + }}; + }, { + type: "FeatureCollection", + features: [], + totalFeatures: 0, + numberMatched: 0, + numberReturned: 0, + timeStamp: new Date().toISOString(), + crs: { + type: "name", + properties: { + name: makeCrsValid(responses[0].crs.toString()) // All layer crs in a MapServer/FeatureServer are the same + } + } + })) + .catch(err => { + throw new Error(`Error while querying layer: ${err.message}`); + }) + ; + } + case 'wms': + case 'wfs': { + return describeFeatureType(layer.url, layer.name) + .then(describe => axios + .get(getFeatureURL(layer.url, layer.name, + optionsToVendorParams({ + filterObj: { + spatialField: { + operation: "INTERSECTS", + attribute: extractGeometryAttributeName(describe), + geometry: geometry + } + } + }) + ), { params: assign({ outputFormat: 'application/json' }, + selectQueryMaxCount > -1 ? { + maxFeatures: selectQueryMaxCount, // WFS v1.1.0 + count: selectQueryMaxCount + } : {} + )}) + .then(response => assign(response.data, response.data.crs === null ? {} : + { + crs: { + type: response.data.crs.type, + properties: {...response.data.crs.properties, [response.data.crs.type]: makeCrsValid(response.data.crs.properties[response.data.crs.type])} + } + }) + ) + .catch(err => { throw new Error(`Error ${err.status}: ${err.statusText}`); }) + ).catch(err => { throw new Error(`Error ${err.status}: ${err.statusText}`); }); + } + default: + return new Promise((_, reject) => reject(new Error(`Unsupported layer type: ${layer.type}`))); + } +}; + +export const openSelectEpic = (action$, store) => action$ + .ofType(SET_CONTROL_PROPERTY, TOGGLE_CONTROL) + .filter(action => action.control === "select" && isSelectEnabled(store.getState())) + .switchMap(() => Observable.merge( + Observable.of(registerEventListener('click', 'select')), + ...selectLayersSelector(store.getState()).map(layer => Observable.of(mergeOptionsByOwner(buildAdditionalLayerOwnerName(layer.id), { visibility: layer.visibility }))) + )); + +export const closeSelectEpics = (action$, store) => action$ + .ofType(SET_CONTROL_PROPERTY, TOGGLE_CONTROL) + .filter(action => action.control === "select" && !isSelectEnabled(store.getState())) + .switchMap(() => Observable.merge( + Observable.of(unRegisterEventListener('click', 'select')), + Observable.of(changeDrawingStatus("clean", "", "select", [], {})), + ...selectLayersSelector(store.getState()).map(layer => Observable.of(mergeOptionsByOwner(buildAdditionalLayerOwnerName(layer.id), { visibility: false })))) + ); + +export const tearDownSelectOnDrawToolActive = (action$, store) => shutdownToolOnAnotherToolDrawing(action$, store, 'select'); + +export const queryLayers = (action$, store) => action$ + .ofType(END_DRAWING) + .filter(action => + action.owner === 'select' && + isSelectEnabled(store.getState()) && + action.geometry + ) + .switchMap(action => { + const state = store.getState(); + const selectQueryMaxCount = getSelectQueryMaxFeatureCount(state); + return Observable.from(selectLayersSelector(state)) + .mergeMap(layer => Observable.concat( + Observable.of(addOrUpdateSelection(layer, {})), + isSelectQueriable(layer) + ? Observable.concat( + Observable.of(addOrUpdateSelection(layer, { loading: true })), + Observable.fromPromise(queryLayer(layer, action.geometry, selectQueryMaxCount)) + .map(geoJsonData => addOrUpdateSelection(layer, geoJsonData)) + .catch(error => Observable.of(addOrUpdateSelection(layer, { error }))) + ) + : Observable.empty() + )); + }); + +export const cleanSelection = (action$, store) => action$ + .ofType(SELECT_CLEAN_SELECTION) + .filter(() => isSelectEnabled(store.getState())) + .switchMap(action => Observable.merge( + Observable.of( + changeDrawingStatus( + action.geomType ? "start" : "clean", + action.geomType || "", + "select", + [], + action.geomType ? { + stopAfterDrawing: true, + editEnabled: false, + drawEnabled: false + } : {} + ) + ), + Observable.from(selectLayersSelector(store.getState())).flatMap(layer => + Observable.merge( + Observable.of(addOrUpdateSelection(layer, {})), + Observable.of(mergeOptionsByOwner(buildAdditionalLayerOwnerName(layer.id), { + features: [], + visibility: false + })) + ) + ) + )); + +export const synchroniseLayersAndAdditionalLayers = (action$, store) => action$ + .filter(action => action.type === UPDATE_NODE + && isSelectEnabled(store.getState()) + && Object.hasOwn(action.options || {}, 'visibility') + ) + .concatMap(action => { + const state = store.getState(); + const layersForSelect = layersSelector(state).filter(filterLayerForSelect); + + if (layersForSelect?.find(layer => layer.id === action.node)) { + return Observable.of(mergeOptionsByOwner(buildAdditionalLayerOwnerName(action.node), { visibility: action.options.visibility })); + } + + const groups = flattenArrayOfObjects(groupsSelector(state)); + return Observable.from(layersForSelect.filter(layer => layer.group?.startsWith(action.node))) + .mergeMap(layer => Observable.of(mergeOptionsByOwner(buildAdditionalLayerOwnerName(layer.id), { visibility: !getInactiveNode(layer.group, groups) })) + ); + }); + +export const onRemoveLayer = (action$, store) => action$ + .ofType(REMOVE_NODE) + .filter(action => isSelectEnabled(store.getState()) + && action.nodeType === 'layers' + ) + .mergeMap(action => Observable.of(removeAdditionalLayer({ id: buildAdditionalLayerId(action.node), owner: buildAdditionalLayerOwnerName(action.node) }))); + + +export const onSelectionUpdate = (action$, store) => action$ + .ofType(ADD_OR_UPDATE_SELECTION) + .filter(action => isSelectEnabled(store.getState()) && action.layer) + .mergeMap(action => Observable.of(customUpdateAdditionalLayer( + action.layer.id, + action.geoJsonData.features ?? [], + action.layer.visibility && action.geoJsonData.error && !action.geoJsonData.loading, + { ...highlightStyleSelector(store.getState()), ...getSelectHighlightOptions(store.getState())} + ))); + +export default { + openSelectEpic, + closeSelectEpics, + tearDownSelectOnDrawToolActive, + queryLayers, + cleanSelection, + synchroniseLayersAndAdditionalLayers, + onRemoveLayer, + onSelectionUpdate +}; diff --git a/web/client/plugins/Select.jsx b/web/client/plugins/Select.jsx new file mode 100644 index 0000000000..956dd03e65 --- /dev/null +++ b/web/client/plugins/Select.jsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { get } from 'lodash'; +import { Glyphicon } from 'react-bootstrap'; + +import { createPlugin } from '../utils/PluginsUtils'; +import { layersSelector } from '../selectors/layers'; +import { updateNode, addLayer, changeLayerProperties } from '../actions/layers'; +import { zoomToExtent } from '../actions/map'; +import controls from '../reducers/controls'; +import { toggleControl } from '../actions/controls'; +import Message from '../components/I18N/Message'; + +import SelectComponent from './select/components/Select'; +import epics from '../epics/select'; +import select from '../reducers/select'; +import { storeConfiguration, cleanSelection, addOrUpdateSelection } from '../actions/select'; +import { getSelectSelections, getSelectQueryMaxFeatureCount } from '../selectors/select'; + +export default createPlugin('Select', { + component: connect( + createSelector([ + (state) => get(state, 'controls.select.enabled'), + layersSelector, + getSelectSelections, + getSelectQueryMaxFeatureCount + ], (isVisible, layers, selections, maxFeatureCount) => ({ + isVisible, + layers, + selections, + maxFeatureCount + })), + { + onClose: toggleControl.bind(null, 'select', null), + onUpdateNode: updateNode, + storeConfiguration, + cleanSelection, + addOrUpdateSelection, + zoomToExtent, + addLayer, + changeLayerProperties + } + )(SelectComponent), + options: { + disablePluginIf: "{state('router') && (state('router').endsWith('new') || state('router').includes('newgeostory') || state('router').endsWith('dashboard'))}" + }, + reducers: { + ...controls, + select + }, + epics: epics, + containers: { + BurgerMenu: { + name: 'select', + position: 1000, + priority: 2, + doNotHide: true, + text: , + tooltip: , + icon: , + action: toggleControl.bind(null, 'select', null), + toggle: true + }, + SidebarMenu: { + name: 'select', + position: 1000, + priority: 1, + doNotHide: true, + text: , + tooltip: , + icon: , + action: toggleControl.bind(null, 'select', null), + toggle: true + }, + Toolbar: { + name: 'select', + alwaysVisible: true, + position: 2, + priority: 0, + doNotHide: true, + tooltip: , + icon: , + action: toggleControl.bind(null, 'select', null), + toggle: true + } + } +}); diff --git a/web/client/plugins/select/assets/select.css b/web/client/plugins/select/assets/select.css new file mode 100644 index 0000000000..a59e6e5abd --- /dev/null +++ b/web/client/plugins/select/assets/select.css @@ -0,0 +1,40 @@ +.ms-resizable-modal > .modal-content.select-dialog { + top: 0vh; + right: -100vw; +} + +.select-content * .ms-node-title { + font-weight: bold; +} + +.select-content * .ms-node-header-info > .ms-node-header-addons:nth-child(3) { + flex: 1 ; + justify-content: space-between; +} + +.features-count-displayer{ + display: flex; +} + +.title-container { + display: flex; +} + +.title-icon { + height: 100%; + width: auto; + margin-right: 0.5em; +} + +.title-title { + flex-grow: 1; + text-align: center; +} + +.tree-header { + background-color: #E9EDF4; +} + +.features-count { + font-weight: bold; +} diff --git a/web/client/plugins/select/components/EllipsisButton/EllipsisButton.css b/web/client/plugins/select/components/EllipsisButton/EllipsisButton.css new file mode 100644 index 0000000000..66025df79a --- /dev/null +++ b/web/client/plugins/select/components/EllipsisButton/EllipsisButton.css @@ -0,0 +1,57 @@ +.ellipsis-container { + position: relative; + display: inline-block; + opacity: 1; +} + +.ellipsis-button { + padding: 2%; + background-color: lightgray; + /* border: none; */ + border: 1px solid #ccc; + border-radius: 50%; + /* font-size: 16px; */ + font-weight: bold; + cursor: pointer; + text-align: center; + line-height: 1; +} + +.ellipsis-button:hover { + background-color: #e0e0e0; +} + +.ellipsis-menu { + position: absolute; + top: 100%; + right: 0; + background-color: white; + border: 1px solid #ccc; + border-radius: 5px; + /* margin-top: 5px; */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + z-index: 1; + width: 10vw; +} + +.ellipsis-menu p { + margin: 0; + padding: 5%; + cursor: pointer; +} + +.ellipsis-menu p:hover { + background-color: #f0f0f0; +} + +.export-toggle { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + padding: 5px 10px; +} + +.export-toggle span:nth-of-type(2) { + font-weight: bold; +} diff --git a/web/client/plugins/select/components/EllipsisButton/EllipsisButton.jsx b/web/client/plugins/select/components/EllipsisButton/EllipsisButton.jsx new file mode 100644 index 0000000000..79d67c611d --- /dev/null +++ b/web/client/plugins/select/components/EllipsisButton/EllipsisButton.jsx @@ -0,0 +1,226 @@ +import React, { useState, useEffect, useContext } from 'react'; +import ReactDOM from "react-dom"; +import bbox from '@turf/bbox'; +import { saveAs } from 'file-saver'; +import axios from 'axios'; + +import Message from '../../../../components/I18N/Message'; +import { describeFeatureType } from '../../../../api/WFS'; + +import { SelectRefContext } from '../Select'; +import Statistics from './Statistics/Statistics'; +import './EllipsisButton.css'; + +export default ({ + node = {}, + layers = [], + selectionData = {}, + onAddOrUpdateSelection = () => {}, + onZoomToExtent = () => {}, + onAddLayer = () => {}, + onChangeLayerProperties = () => {} +}) => { + const [menuOpen, setMenuOpen] = useState(false); + const [exportOpen, setExportOpen] = useState(false); + const [statisticsOpen, setStatisticsOpen] = useState(false); + const [numericFields, setNumericFields] = useState([]); + + const SelectRef = useContext(SelectRefContext); + const ellipsisContainerClass = 'ellipsis-container'; + useEffect(() => { + const selectElement = SelectRef.current?.addEventListener ? SelectRef.current : ReactDOM.findDOMNode(SelectRef.current); + if (!selectElement || !selectElement.addEventListener) { return null; } + const handleClick = e => { + if (menuOpen) { + let parentElement = e.target; + let foundThis = false; + while (!foundThis && parentElement !== e.currentTarget) { + foundThis = parentElement.className === ellipsisContainerClass; + parentElement = parentElement.parentElement; + } + if (!foundThis) { setMenuOpen(false); } + } + }; + selectElement.addEventListener("click", handleClick); + return () => selectElement.removeEventListener("click", handleClick); + }); + + const toggleMenu = () => setMenuOpen(!menuOpen); + const toggleExport = () => setExportOpen(!exportOpen); + + const triggerAction = (action) => { + switch (action) { + case 'clear': { + onAddOrUpdateSelection(node, {}); + break; + } + case 'zoomTo': { + if (selectionData.features?.length > 0) { + const extent = bbox(selectionData); + if (extent) { onZoomToExtent(extent, selectionData.crs.properties[selectionData.crs.type]); } + } + break; + } + case 'createLayer': { + if (selectionData.features?.length > 0) { + const nodeName = node.title + '_Select _'; + let index = 0; + let notFound = false; + while (!notFound) { + index++; + // eslint-disable-next-line no-loop-func + notFound = layers.findIndex(layer => layer.name === (nodeName + index.toString())) === -1; + } + onAddLayer({ + type: 'vector', + visibility: true, + name: nodeName + index.toString(), + hideLoading: true, + // bbox: { + // bounds: bbox({ + // type: "FeatureCollection", + // features: selectionData.features + // }), + // crs: node.bbox.crs + // }, + features: selectionData.features + }); + } + break; + } + case 'exportToGeoJson': { + if (selectionData.features?.length > 0) { saveAs(new Blob([JSON.stringify(selectionData)], { type: 'application/json' }), node.title + '.json'); } + break; + } + case 'exportToJson': { + if (selectionData.features?.length > 0) { saveAs(new Blob([JSON.stringify(selectionData.features.map(feature => feature.properties))], { type: 'application/json' }), node.title + '.json'); } + break; + } + case 'exportToCsv': { + if (selectionData.features?.length > 0) { saveAs(new Blob([Object.keys(selectionData.features[0].properties).join(',') + '\n' + selectionData.features.map(feature => Object.values(feature.properties).join(',')).join('\n')], { type: 'text/csv' }), node.title + '.csv'); } + break; + } + case 'filterData': { + const customOnChangeLayerProperties = fieldIdName => onChangeLayerProperties(node.id, { + layerFilter: { + // searchUrl: null, + // featureTypeConfigUrl: null, + // showGeneratedFilter: false, + // attributePanelExpanded: true, + // spatialPanelExpanded: false, + // crossLayerExpanded: false, + // showDetailsPanel: false, + // groupLevels: 5, + // useMapProjection: false, + // toolbarEnabled: true, + groupFields: [ + { + id: 1, + logic: 'OR', + index: 0 + } + ], + // maxFeaturesWPS: 5, + filterFields: selectionData.features.map(feature => ({ + rowId: new Date().getDate(), + groupId: 1, + attribute: fieldIdName, + operator: '=', + value: feature.properties[fieldIdName], + type: 'number', + fieldOptions: { + valuesCount: 0, + currentPage: 1 + }, + exception: null + })) + // spatialField: null, + // simpleFilterFields: [], + // map: null, + // filters: [], + // crossLayerFilter: null, + // autocompleteEnabled: true + } + }); + switch (node.type) { + case 'arcgis': { + // TODO : implement here when MapStore supports filtering for arcgis services + throw new Error(`Unsupported layer type: ${node.type}`); + // break; + } + case 'wms': + case 'wfs': { + describeFeatureType(node.url, node.name) + .then(describe => customOnChangeLayerProperties(describe.featureTypes.find(featureType => node.name.endsWith(featureType.typeName)).properties.find(property => ['xsd:string', 'xsd:int'].find(type => type === property.type) && !property.nillable && property.maxOccurs === 1 && property.minOccurs === 1).name)) + .catch(err => { throw new Error(`Error while querying layer: ${err.message}`); }); + break; + } + default: + throw new Error(`Unsupported layer type: ${node.type}`); + } + break; + } + default: + } + toggleMenu(); + }; + + useEffect(() => { + switch (node.type) { + case 'arcgis': { + const arcgisNumericFields = new Set([ 'esriFieldTypeSmallInteger', 'esriFieldTypeInteger', 'esriFieldTypeSingle', 'esriFieldTypeDouble' ]); + const singleLayerId = parseInt(node.name ?? '', 10); + Promise.all((Number.isInteger(singleLayerId) ? node.options.layers.filter(l => l.id === singleLayerId) : node.options.layers).map(l => axios.get(`${node.url}/${l.id}`, { params: { f: 'json'} }) + .then(describe => describe.data.fields.filter(field => field.domain === null && arcgisNumericFields.has(field.type)).map(field => field.name)) + .catch(() => []) + )) + .then(responses => setNumericFields(responses.map(response => response ?? []).flat())) + .catch(() => setNumericFields([])); + break; + } + case 'wms': + case 'wfs': { + describeFeatureType(node.url, node.name) + .then(describe => setNumericFields(describe.featureTypes[0].properties.filter(property => property.localType === 'number').map(property => property.name))) + .catch(() => setNumericFields([])); + break; + } + default: + } + }, []); + + return ( +
    + + {menuOpen && ( +
    +

    triggerAction('zoomTo')}>

    +

    { toggleMenu(); selectionData.features?.length > 0 ? setStatisticsOpen(true) : null;}}>

    +

    triggerAction('createLayer')}>

    + {node.type !== 'arcgis' &&

    triggerAction('filterData')}>

    } +
    +

    + + {exportOpen ? "−" : "+"} +

    + {exportOpen && ( +
    +

    triggerAction('exportToGeoJson')}> -

    +

    triggerAction('exportToJson')}> -

    +

    triggerAction('exportToCsv')}> -

    +
    + )} +
    +

    triggerAction('clear')}>

    +
    + )} + {statisticsOpen && } +
    + ); +}; diff --git a/web/client/plugins/select/components/EllipsisButton/Statistics/Statistics.css b/web/client/plugins/select/components/EllipsisButton/Statistics/Statistics.css new file mode 100644 index 0000000000..ff8abea2a2 --- /dev/null +++ b/web/client/plugins/select/components/EllipsisButton/Statistics/Statistics.css @@ -0,0 +1,36 @@ +.feature-statistics { + display: flex; + flex-direction: column; + padding: 1rem; + width: 100%; + } + + .select-container { + display: flex; + width: 100%; + align-items: center; + } + + .select-container label { + font-weight: bold; + margin-right: 0.5rem; + } + + .select-container select { + flex-grow: 1; + padding: 0.5rem; + border: 1px solid #ccc; + } + + .statistics-table { + width: 100%; + margin-top: 1rem; + } + + .statistics-table td { + padding: 0.5rem; + } + + .statistics-table td:first-child { + font-weight: bold; + } diff --git a/web/client/plugins/select/components/EllipsisButton/Statistics/Statistics.jsx b/web/client/plugins/select/components/EllipsisButton/Statistics/Statistics.jsx new file mode 100644 index 0000000000..0b04a81a03 --- /dev/null +++ b/web/client/plugins/select/components/EllipsisButton/Statistics/Statistics.jsx @@ -0,0 +1,75 @@ +import React, { useState, useMemo } from 'react'; + +import Message from '../../../../../components/I18N/Message'; +import Portal from '../../../../../components/misc/Portal'; +import ResizableModal from '../../../../../components/misc/ResizableModal'; + +import './Statistics.css'; + +export default ({ + fields = [], + features = [], + setStatisticsOpen = () => {} +}) => { + const [selectedField, setSelectedField] = useState(fields.length > 0 ? fields[0] : null); + + const statistics = useMemo(() => { + if (!selectedField) return null; + + const values = features.map(f => f.properties[selectedField]).filter(v => typeof v === "number"); + if (values.length === 0) return null; + + const sum = values.reduce((acc, val) => acc + val, 0); + const min = Math.min(...values); + const max = Math.max(...values); + const mean = sum / values.length; + const variance = values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / values.length; + const stdDev = Math.sqrt(variance); + + return { count: values.length, sum, min, max, mean, stdDev }; + }, [features, selectedField]); + + return ( + + } + size="sm" + // eslint-disable-next-line react/jsx-boolean-value + show={true} + onClose={() => setStatisticsOpen(false)} + // draggable={true} + buttons={[{ + text: , + onClick: () => setStatisticsOpen(false), + bsStyle: 'primary' + }]}> +
    +
    + + +
    + + {statistics && ( + + + + + + + + + +
    {statistics.count}
    {statistics.sum.toFixed(6)}
    {statistics.min.toFixed(6)}
    {statistics.max.toFixed(6)}
    {statistics.mean.toFixed(6)}
    {statistics.stdDev.toFixed(6)}
    + )} +
    +
    +
    + ); +}; diff --git a/web/client/plugins/select/components/Select.jsx b/web/client/plugins/select/components/Select.jsx new file mode 100644 index 0000000000..9bcf12dea9 --- /dev/null +++ b/web/client/plugins/select/components/Select.jsx @@ -0,0 +1,144 @@ +import React, { useEffect, createContext, useRef } from 'react'; +import { injectIntl } from 'react-intl'; +import { Glyphicon } from 'react-bootstrap'; + +import { ControlledTOC } from '../../TOC/components/TOC'; +import ResizableModal from '../../../components/misc/ResizableModal'; +import Message from '../../../components/I18N/Message'; +import VisibilityCheck from '../../TOC/components/VisibilityCheck'; +import NodeHeader from '../../TOC/components/NodeHeader'; +import { getLayerTypeGlyph } from '../../../utils/LayersUtils'; +import NodeTool from '../../TOC/components/NodeTool'; +import InlineLoader from '../../TOC/components/InlineLoader'; + +import SelectHeader from './SelectHeader/SelectHeader'; +import EllipsisButton from './EllipsisButton/EllipsisButton'; +import { isSelectQueriable, filterLayerForSelect } from '../../../selectors/select'; +import '../assets/select.css'; + +export const SelectRefContext = createContext(null); + +function applyVersionParamToLegend(layer) { + // we need to pass a parameter that invalidate the cache for GetLegendGraphic + // all layer inside the dataset viewer apply a new _v_ param each time we switch page + return { ...layer, legendParams: { ...layer?.legendParams, _v_: layer?._v_ } }; +} + +export default injectIntl(({ + layers, + onUpdateNode, + onClose, + isVisible, + highlightOptions, + queryOptions, + selectTools, + storeConfiguration, + intl, + selections, + maxFeatureCount, + cleanSelection, + addOrUpdateSelection, + zoomToExtent, + addLayer, + changeLayerProperties +}) => { + const SelectRef = useRef(null); + const filterLayers = layers.filter(filterLayerForSelect); + const customLayerNodeComponent = ({node, config}) => { + const selectionData = selections[node.id] ?? {}; + return ( +
  • + + + onUpdateNode(node.id, 'layers', { isSelectQueriable: checked })} + /> + + + } + afterTitle={ + <> + {selectionData.error ? ( + + ) : ( +
    + {selectionData.features && selectionData.features.length === maxFeatureCount && } /* tooltip={"ouech"} */ glyph="exclamation-mark"/>} +

    {selectionData.loading ? '⊙' : (selectionData.features?.length ?? 0)}

    +
    + )} + + {selectionData.features?.length > 0 && } + + } + /> +
  • + ); + }; + + useEffect(() => storeConfiguration({ highlightOptions, queryOptions }), []); + + return ( + + + icon + + + + } + dialogClassName=" select-dialog" + show={isVisible} + // eslint-disable-next-line react/jsx-boolean-value + draggable={true} + style={{zIndex: 1993}}> + + Object.fromEntries(Object.entries(applyVersionParamToLegend(layer)).filter(([key]) => key !== 'group'))).reverse()} + className="select-content" + theme="legend" + layerNodeComponent={customLayerNodeComponent} + treeHeader={ +
  • + filterLayers.forEach(layer => onUpdateNode(layer.id, 'layers', { isSelectQueriable: checked }))} + /> + } + afterTitle={} + /> +
  • + } + /> +
    +
    + ); +}); diff --git a/web/client/plugins/select/components/SelectHeader/SelectHeader.css b/web/client/plugins/select/components/SelectHeader/SelectHeader.css new file mode 100644 index 0000000000..ce73fcf912 --- /dev/null +++ b/web/client/plugins/select/components/SelectHeader/SelectHeader.css @@ -0,0 +1,91 @@ +.select-header-container { + margin: 2%; +} + +.head-text { + font-size: small; + font-weight: bold; +} + +.select-header { + display: flex; + justify-content: space-between; + gap: 5%; +} + +.select-button-container { + position: relative; + flex: 1; + max-width: 65%; + border: none; +} + +.select-button { + /* background-color: #005232; */ + background-color: white; + border: 1px solid #F1F1F1; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 2% 5%; + /* color: white; */ + cursor: pointer; +} + +.select-button:hover { + /* background-color: #016e44; */ + background-color: #e0e0e0; +} + +.select-button-text { + flex: 1; + text-align: center; +} + +.select-button-arrow { + margin-left: auto; +} + +.select-button-menu { + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: white; + border: 1px solid #ccc; + border-radius: 5px; + margin-top: 1%; + z-index: 1; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +.select-button-menu p { + padding: 2%; + margin: 0; + cursor: pointer; +} + +.select-button-menu p:hover { + background-color: #f0f0f0; +} + +.clear-select-button { + padding: 10px 15px; + /* background-color: lightgray; */ + background-color: white; + color: black; + border: 1px solid #989898; + border-radius: 5px; + cursor: pointer; +} + +.clear-select-button:hover { + background-color: #e0e0e0; +} + +.selection { + margin-bottom: 2%; + font-weight: bold; +} diff --git a/web/client/plugins/select/components/SelectHeader/SelectHeader.jsx b/web/client/plugins/select/components/SelectHeader/SelectHeader.jsx new file mode 100644 index 0000000000..a007aeb606 --- /dev/null +++ b/web/client/plugins/select/components/SelectHeader/SelectHeader.jsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect, useContext } from 'react'; +import ReactDOM from "react-dom"; +import { Glyphicon } from 'react-bootstrap'; + +import Message from '../../../../components/I18N/Message'; +import InlineLoader from '../../../../plugins/TOC/components/InlineLoader'; + +import { SelectRefContext } from '../Select'; +import './SelectHeader.css'; + +export default ({ + onCleanSelect, + selectTools +}) => { + const [menuOpen, setMenuOpen] = useState(false); + const [menuClosing, setMenuClosing] = useState(false); + const [selectedTool, setSelectedTool] = useState(null); + + const selectRef = useContext(SelectRefContext); + useEffect(() => { + const selectElement = selectRef.current?.addEventListener ? selectRef.current : ReactDOM.findDOMNode(selectRef.current); + if (!selectElement || !selectElement.addEventListener) { return null; } + const handleClick = () => setMenuClosing(true); + selectElement.addEventListener("click", handleClick); + return () => selectElement.removeEventListener("click", handleClick); + }); + useEffect(() => { + if (menuClosing) { + setMenuClosing(false); + if (menuOpen) { + setTimeout(() => setMenuOpen(false), 50); // In order that onCleanSelect has the time to trigger its action. + } + } + }, [menuClosing]); + + const toggleMenu = () => setMenuOpen(!menuOpen); + + const clean = tool => { + setMenuOpen(false); + if (tool) setSelectedTool(tool); + onCleanSelect(tool?.action ?? null); + }; + + const clearSelection = () => { + clean(); + setSelectedTool(null); + }; + + const allTools = [ + { type: 'Point', action: 'Point', label: 'select.button.selectByPoint', icon: '1-point' }, + { type: 'LineString', action: 'LineString', label: 'select.button.selectByLine', icon: 'polyline' }, + { type: 'Circle', action: 'Circle', label: 'select.button.selectByCircle', icon: '1-circle' }, + { type: 'Rectangle', action: 'BBOX', label: 'select.button.selectByRectangle', icon: 'unchecked' }, + { type: 'Polygon', action: 'Polygon', label: 'select.button.selectByPolygon', icon: 'polygon' } + ]; + const availableTools = allTools.filter(tool => !Array.isArray(selectTools) || selectTools.includes(tool.type === 'LineString' ? 'Line' : tool.type)); + const orderedTools = selectedTool ? [availableTools.find(tool => tool.type === selectedTool.type), ...availableTools.filter(tool => tool.type !== selectedTool.type)] : availableTools; + + return ( +
    +
    +
    +
    + + {menuOpen && ( +
    + {orderedTools.map(tool =>

    clean(tool)}>{' '}

    )} +
    + )} +
    + +
    +   + +   +
    +
    + ); +}; diff --git a/web/client/reducers/select.js b/web/client/reducers/select.js new file mode 100644 index 0000000000..08db0c760f --- /dev/null +++ b/web/client/reducers/select.js @@ -0,0 +1,20 @@ +import { SELECT_STORE_CFG, ADD_OR_UPDATE_SELECTION } from '../actions/select'; + +export default function select(state = {cfg: {}, selections: {}}, action) { + switch (action.type) { + case SELECT_STORE_CFG: { + return { + ...state, + cfg: action.cfg + }; + } + case ADD_OR_UPDATE_SELECTION: { + return { + ...state, + selections: {...state.selections, [action.layer.id]: action.geoJsonData} + }; + } + default: + return state; + } +} diff --git a/web/client/selectors/select.js b/web/client/selectors/select.js new file mode 100644 index 0000000000..84a0cc2846 --- /dev/null +++ b/web/client/selectors/select.js @@ -0,0 +1,19 @@ +import { get } from 'lodash'; + +export const filterLayerForSelect = layer => layer && layer.group !== 'background' && ['wms', 'wfs', 'arcgis'].includes(layer.type); + +export const selectLayersSelector = state => (get(state, 'layers.flat') || []).filter(filterLayerForSelect); + +export const isSelectEnabled = state => get(state, "controls.select.enabled"); + +export const hasSelectQueriableProp = node => Object.hasOwn(node, 'isSelectQueriable'); +export const isSelectQueriable = node => hasSelectQueriableProp(node) ? node.isSelectQueriable : !!node?.visibility; + +export const getSelectObj = state => get(state, 'select') ?? {}; +export const getSelectQueryOptions = state => getSelectObj(state).cfg?.queryOptions ?? {}; +export const getSelectQueryMaxFeatureCount = state => { + const queryOptions = getSelectQueryOptions(state); + return Number.isInteger(queryOptions.maxCount) ? queryOptions.maxCount : -1; +}; +export const getSelectHighlightOptions = state => getSelectObj(state).cfg?.highlightOptions ?? {}; +export const getSelectSelections = state => getSelectObj(state).selections ?? {}; diff --git a/web/client/utils/Select.js b/web/client/utils/Select.js new file mode 100644 index 0000000000..2212f19eaf --- /dev/null +++ b/web/client/utils/Select.js @@ -0,0 +1,63 @@ +import { updateAdditionalLayer } from '../actions/additionallayers'; +import { applyMapInfoStyle } from '../selectors/mapInfo'; + +export const buildAdditionalLayerName = layerId => `"highlight-select-${layerId}-features"`; +export const buildAdditionalLayerOwnerName = layerId => `Select_${layerId}`; +export const buildAdditionalLayerId = layerId => `${buildAdditionalLayerOwnerName(layerId)}_id`; + +function getGeometryType(geometry) { + if (geometry.x !== undefined && geometry.y !== undefined) { + return "Point"; + } else if (geometry.paths) { + return "LineString"; + } else if (geometry.rings) { + return "Polygon"; + } + return null; +} + +function convertCoordinates(geometry) { + if (geometry.x !== undefined && geometry.y !== undefined) { + return [geometry.x, geometry.y]; + } else if (geometry.paths) { + return geometry.paths[0]; + } else if (geometry.rings) { + return geometry.rings; + } + return null; +} + +export const makeCrsValid = crs => { + const crsSplit = crs.toString().split(':'); + const crsSplitLength = crsSplit.length; + if (crsSplitLength === 1) { + return 'EPSG' + ':' + crsSplit[0]; + } else if (crsSplitLength > 1) { + const geodeticIndex = crsSplit.lastIndexOf(s => s.length > 0, crsSplitLength - 2); + return (geodeticIndex > -1 ? crsSplit[geodeticIndex] : 'EPSG') + ':' + crsSplit[crsSplitLength - 1]; + } + return 'EPSG:4326'; +}; + +export const arcgisToGeoJSON = (arcgisFeatures, layerName, idField) => arcgisFeatures.map(feature => ({ + type: "Feature", + id: `${layerName}.${feature.attributes[idField]}`, + geometry_name: "geometry", + geometry: { + type: getGeometryType(feature.geometry), + coordinates: convertCoordinates(feature.geometry) + }, + properties: feature.attributes +})); + +export const customUpdateAdditionalLayer = (layerId, features, isVisible, highlightStyle) => updateAdditionalLayer( + buildAdditionalLayerId(layerId), + buildAdditionalLayerOwnerName(layerId), + "overlay", + { + type: "vector", + name: buildAdditionalLayerName(layerId), + visibility: isVisible, + features: features.map(applyMapInfoStyle(highlightStyle)) + } +); From 60f145bf01bf19c71d47ba22b18567b17595ac29 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 11:17:55 +0200 Subject: [PATCH 22/49] Correction call on OnUpdate --- web/client/plugins/TOC/components/ArcGISLegend.jsx | 2 +- web/client/plugins/TOC/components/Legend.jsx | 2 +- web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/client/plugins/TOC/components/ArcGISLegend.jsx b/web/client/plugins/TOC/components/ArcGISLegend.jsx index 9274713121..b533430b08 100644 --- a/web/client/plugins/TOC/components/ArcGISLegend.jsx +++ b/web/client/plugins/TOC/components/ArcGISLegend.jsx @@ -36,7 +36,7 @@ function ArcGISLegend({ .then(({ data }) => { const dynamicLegendIsEmpty = data.layers.every(layer => layer.legend.length === 0); if ((node.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { - onUpdateNode(node.id, 'layers', { dynamicLegendIsEmpty }); + onUpdateNode({ dynamicLegendIsEmpty }); } setLegendData(data); }) diff --git a/web/client/plugins/TOC/components/Legend.jsx b/web/client/plugins/TOC/components/Legend.jsx index e5deb9cc1b..1d066a20d7 100644 --- a/web/client/plugins/TOC/components/Legend.jsx +++ b/web/client/plugins/TOC/components/Legend.jsx @@ -140,7 +140,7 @@ class Legend extends React.Component { this.onImgError(); } if ((this.props.layer.dynamicLegendIsEmpty ?? null) !== imgError) { - this.props.onUpdateNode(this.props.layer.id, 'layers', { dynamicLegendIsEmpty: imgError }); + this.props.onUpdateNode({ dynamicLegendIsEmpty: imgError }); } } } diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index f3e9be763a..dd392e73b1 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -108,7 +108,7 @@ class StyleBasedWMSJsonLegend extends React.Component { getJsonWMSLegend(jsonLegendUrl).then(data => { const dynamicLegendIsEmpty = data.length === 0 || data[0].rules.length === 0; if ((this.props.layer.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { - this.props.onUpdateNode(this.props.layer.id, 'layers', { dynamicLegendIsEmpty }); + this.props.onUpdateNode({ dynamicLegendIsEmpty }); } this.setState({ jsonLegend: data[0], loading: false }); }).catch(() => { From c4b1197cb55a7ca1850bbd470a6b5ef55048ea68 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 11:18:45 +0200 Subject: [PATCH 23/49] Trasnmit onUpdate --- web/client/plugins/TOC/components/DefaultLayer.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx index 673660267d..66fa63ca3c 100644 --- a/web/client/plugins/TOC/components/DefaultLayer.jsx +++ b/web/client/plugins/TOC/components/DefaultLayer.jsx @@ -96,6 +96,7 @@ const NodeLegend = ({
  • {visible ? : null}
  • From 7ded0c462b94de8adad93bab12870272f73e2162 Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 11:26:39 +0200 Subject: [PATCH 24/49] Correction on Transmit --- web/client/plugins/TOC/components/DefaultLayer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx index 66fa63ca3c..0448c2861c 100644 --- a/web/client/plugins/TOC/components/DefaultLayer.jsx +++ b/web/client/plugins/TOC/components/DefaultLayer.jsx @@ -96,7 +96,7 @@ const NodeLegend = ({
  • {visible ? : null}
  • From 2a40ff6f0bf151240d0a09897c8130fea31d160b Mon Sep 17 00:00:00 2001 From: pln Date: Tue, 20 May 2025 11:34:54 +0200 Subject: [PATCH 25/49] Add translations --- web/client/translations/data.de-DE.json | 8 ++++++++ web/client/translations/data.en-US.json | 8 ++++++++ web/client/translations/data.es-ES.json | 8 ++++++++ web/client/translations/data.fr-FR.json | 8 ++++++++ web/client/translations/data.it-IT.json | 8 ++++++++ 5 files changed, 40 insertions(+) diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 313ee87606..8b3888b6ba 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -1535,6 +1535,10 @@ "height": "Höhe" } }, + "dynamiclegend": { + "title": "Legende", + "tooltip": "Kartenlegende anzeigen" + }, "permalink": { "title": "Permalink", "shareLinkTitle": "Permalink generiert", @@ -3466,6 +3470,10 @@ "description": "Ermöglicht das Teilen der Karte auf verschiedene Arten (Link, QR-Code, Einbettung, soziale Netzwerke ...)", "title": "Freigabe-Werkzeug" }, + "DynamicLegend": { + "title": "Legende", + "description": "Kartenlegende anzeigen" + }, "Permalink": { "description": "Ermöglicht das Erstellen eines Permalinks der aktuell angezeigten Ressource", "title": "Permalink" diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 7f4af13a91..31a0a127c0 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -1496,6 +1496,10 @@ "height": "height" } }, + "dynamiclegend": { + "title": "Legend", + "tooltip": "Display the map legend" + }, "permalink": { "title": "Permalink", "shareLinkTitle": "Permalink generated", @@ -3437,6 +3441,10 @@ "description": "Allows to share the map in various ways (link, QR-Code, embed, social networks...)", "title": "Share Tool" }, + "DynamicLegend": { + "title": "Legend", + "description": "Display the map legend" + }, "Permalink": { "description": "Allows to create a permalink of the current resource in view", "title": "Permalink" diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 2ae860ae11..ed877f6dcd 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -1496,6 +1496,10 @@ "height": "altura" } }, + "dynamiclegend": { + "title": "Leyenda", + "tooltip": "Mostrar la leyenda del mapa" + }, "permalink": { "title": "Permalink", "shareLinkTitle": "Enlace permanente generado", @@ -3427,6 +3431,10 @@ "description": "Permite compartir el mapa de varias maneras (enlace, código QR, incrustar, redes sociales ...)", "title": "Compartir herramienta" }, + "DynamicLegend": { + "title": "Leyenda", + "description": "Mostrar la leyenda del mapa" + }, "Permalink": { "description": "Permite crear un enlace permanente del recurso actual a la vista", "title": "Enlace permanente" diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 890f97c50a..fc3fc045eb 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -1497,6 +1497,10 @@ "height": "la taille" } }, + "dynamiclegend": { + "title": "Légende", + "tooltip": "Affiche la légende de la carte" + }, "permalink": { "title": "Permalink", "shareLinkTitle": "Permalien généré", @@ -3428,6 +3432,10 @@ "description": "Permet de partager la carte de diverses manières (lien, QR-Code, embarqué, réseaux sociaux ...)", "title": "Outil de partage" }, + "DynamicLegend": { + "title": "Légende", + "description": "Affiche la légende de la carte" + }, "Permalink": { "description": "Permet de créer un permalien de la ressource courante en vue", "title": "Lien permanent" diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index e78b24aaad..438167ba4d 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -1496,6 +1496,10 @@ "height": "altezza" } }, + "dynamiclegend": { + "title": "Legenda", + "tooltip": "Visualizza la legenda della mappa" + }, "permalink": { "title": "Permalink", "shareLinkTitle": "Permalink generato", @@ -3429,6 +3433,10 @@ "description": "Permette di condividere la mappa in diversi modi (link, QR-Code, embed, social network ...)", "title": "Strumento di condivisione" }, + "DynamicLegend": { + "title": "Legenda", + "description": "Visualizza la legenda della mappa" + }, "Permalink": { "description": "Permette di creare un permalink della risorsa attualmente in vista", "title": "Permalink" From 5d60a51316c6dde90b598b1d21bb8b407b661f27 Mon Sep 17 00:00:00 2001 From: pln Date: Wed, 28 May 2025 15:37:34 +0200 Subject: [PATCH 26/49] Use of disableResolutionLimits for min- and maxResolution --- web/client/selectors/dynamiclegend.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/client/selectors/dynamiclegend.js b/web/client/selectors/dynamiclegend.js index 7a331e4da3..48fd7dd6be 100644 --- a/web/client/selectors/dynamiclegend.js +++ b/web/client/selectors/dynamiclegend.js @@ -7,6 +7,8 @@ export const keepNode = node => !isLayer(node) || keepLayer(node); export const isLayerVisible = (node, currentResolution) => keepLayer(node) && (!node.hasOwnProperty('dynamicLegendIsEmpty') || !node.dynamicLegendIsEmpty) && ( - (!node.minResolution || node.minResolution <= currentResolution) && - (!node.maxResolution || node.maxResolution > currentResolution) + (node.disableResolutionLimits ?? true) || ( + (!node.minResolution || node.minResolution <= currentResolution) && + (!node.maxResolution || node.maxResolution > currentResolution) + ) ); From a11b6736dd0cc5175977ec75a212d3c7d205d309 Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Thu, 29 May 2025 17:37:52 +0200 Subject: [PATCH 27/49] review changes --- .../templates/configs/pluginsConfig.json | 6 +- web/client/configs/localConfig.json | 2 +- web/client/configs/pluginsConfig.json | 6 +- .../plugins/TOC/components/ArcGISLegend.jsx | 15 +- .../plugins/TOC/components/DefaultLayer.jsx | 2 +- .../plugins/TOC/components/LayersTree.jsx | 7 +- web/client/plugins/TOC/components/Legend.jsx | 8 +- .../components/StyleBasedWMSJsonLegend.jsx | 14 +- web/client/plugins/TOC/components/TOC.jsx | 4 +- .../plugins/TOC/components/WMSLegend.jsx | 4 +- .../{ => dynamicLegend}/DynamicLegend.jsx | 163 +++++++++--------- .../components/DynamicLegend.jsx | 98 +++++------ web/client/plugins/dynamicLegend/index.js | 9 + .../dynamicLegend/utils/DynamicLegendUtils.js | 29 ++++ web/client/product/plugins.js | 2 +- web/client/selectors/dynamiclegend.js | 14 -- 16 files changed, 195 insertions(+), 188 deletions(-) rename web/client/plugins/{ => dynamicLegend}/DynamicLegend.jsx (52%) create mode 100644 web/client/plugins/dynamicLegend/index.js create mode 100644 web/client/plugins/dynamicLegend/utils/DynamicLegendUtils.js delete mode 100644 web/client/selectors/dynamiclegend.js diff --git a/project/standard/templates/configs/pluginsConfig.json b/project/standard/templates/configs/pluginsConfig.json index b9caa68ce3..bc6b987537 100644 --- a/project/standard/templates/configs/pluginsConfig.json +++ b/project/standard/templates/configs/pluginsConfig.json @@ -144,11 +144,7 @@ "glyph": "align-left", "title": "plugins.DynamicLegend.title", "description": "plugins.DynamicLegend.description", - "dependencies": [ - "Toolbar", - "BurgerMenu", - "SidebarMenu" - ] + "dependencies": ["SidebarMenu"] }, { "name": "Permalink", diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index a09e55ad0e..c861e0e98d 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -412,7 +412,7 @@ { "name": "WidgetsTray" } ], "desktop": [ - "Details","DynamicLegend", + "Details", { "name": "BrandNavbar", "cfg": { diff --git a/web/client/configs/pluginsConfig.json b/web/client/configs/pluginsConfig.json index 010cce3cb6..58bd70afcb 100644 --- a/web/client/configs/pluginsConfig.json +++ b/web/client/configs/pluginsConfig.json @@ -144,11 +144,7 @@ "glyph": "align-left", "title": "plugins.DynamicLegend.title", "description": "plugins.DynamicLegend.description", - "dependencies": [ - "Toolbar", - "BurgerMenu", - "SidebarMenu" - ] + "dependencies": ["SidebarMenu"] }, { "name": "Permalink", diff --git a/web/client/plugins/TOC/components/ArcGISLegend.jsx b/web/client/plugins/TOC/components/ArcGISLegend.jsx index b533430b08..00723a34f7 100644 --- a/web/client/plugins/TOC/components/ArcGISLegend.jsx +++ b/web/client/plugins/TOC/components/ArcGISLegend.jsx @@ -17,11 +17,11 @@ import { getLayerIds } from '../../../utils/ArcGISUtils'; /** * ArcGISLegend renders legend from a MapServer or ImageServer service * @prop {object} node layer node options - * @prop {function} onUpdateNode return the changes of a specific node + * @prop {function} onChange return the changes of a specific node */ function ArcGISLegend({ node = {}, - onUpdateNode = () => {} + onChange = () => {} }) { const [legendData, setLegendData] = useState(null); const [error, setError] = useState(false); @@ -34,13 +34,14 @@ function ArcGISLegend({ } }) .then(({ data }) => { - const dynamicLegendIsEmpty = data.layers.every(layer => layer.legend.length === 0); - if ((node.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { - onUpdateNode({ dynamicLegendIsEmpty }); - } + const legendEmpty = data.layers.every(layer => layer.legend.length === 0); + onChange({ legendEmpty }); setLegendData(data); }) - .catch(() => setError(true)); + .catch(() => { + onChange({ legendEmpty: true }); + setError(true); + }); } }, [legendUrl]); diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx index 0448c2861c..66fa63ca3c 100644 --- a/web/client/plugins/TOC/components/DefaultLayer.jsx +++ b/web/client/plugins/TOC/components/DefaultLayer.jsx @@ -96,7 +96,7 @@ const NodeLegend = ({
  • {visible ? : null}
  • diff --git a/web/client/plugins/TOC/components/LayersTree.jsx b/web/client/plugins/TOC/components/LayersTree.jsx index 94dd104080..8f913ee259 100644 --- a/web/client/plugins/TOC/components/LayersTree.jsx +++ b/web/client/plugins/TOC/components/LayersTree.jsx @@ -90,7 +90,8 @@ const LayersTree = ({ nodeToolItems, nodeContentItems, singleDefaultGroup = isSingleDefaultGroup(tree), - theme + theme, + getNodeStyle = () => ({}) }) => { const containerNode = useRef(); @@ -117,10 +118,6 @@ const LayersTree = ({ return (); }; - const getNodeStyle = () => { - return {}; - }; - const getNodeClassName = (currentNode) => { const selected = selectedNodes.find((selectedNode) => currentNode.id === selectedNode.id); const contextMenuHighlight = contextMenu?.id === currentNode.id; diff --git a/web/client/plugins/TOC/components/Legend.jsx b/web/client/plugins/TOC/components/Legend.jsx index 1d066a20d7..02f1f38f96 100644 --- a/web/client/plugins/TOC/components/Legend.jsx +++ b/web/client/plugins/TOC/components/Legend.jsx @@ -51,7 +51,7 @@ class Legend extends React.Component { projection: PropTypes.string, mapSize: PropTypes.object, bbox: PropTypes.object, - onUpdateNode: PropTypes.func + onChange: PropTypes.func }; static defaultProps = { @@ -60,7 +60,7 @@ class Legend extends React.Component { legendOptions: "forceLabels:on", style: {maxWidth: "100%"}, scaleDependent: true, - onUpdateNode: () => {} + onChange: () => {} }; state = { error: false @@ -139,9 +139,7 @@ class Legend extends React.Component { if (imgError) { this.onImgError(); } - if ((this.props.layer.dynamicLegendIsEmpty ?? null) !== imgError) { - this.props.onUpdateNode({ dynamicLegendIsEmpty: imgError }); - } + this.props.onChange({ legendEmpty: imgError }); } } diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index dd392e73b1..b9924bbc56 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -46,8 +46,7 @@ class StyleBasedWMSJsonLegend extends React.Component { interactive: PropTypes.bool, // the indicator flag that refers if this legend is interactive or not projection: PropTypes.string, mapSize: PropTypes.object, - mapBbox: PropTypes.object, - onUpdateNode: PropTypes.func + mapBbox: PropTypes.object }; static defaultProps = { @@ -57,8 +56,7 @@ class StyleBasedWMSJsonLegend extends React.Component { style: {maxWidth: "100%"}, scaleDependent: true, onChange: () => {}, - interactive: false, - onUpdateNode: () => {} + interactive: false }; state = { error: false, @@ -78,7 +76,6 @@ class StyleBasedWMSJsonLegend extends React.Component { const currEnableDynamicLegend = this.props?.layer?.enableDynamicLegend; const prevEnableDynamicLegend = prevProps?.layer?.enableDynamicLegend; - const [prevFilter, currFilter] = [prevProps?.layer, this.props?.layer] .map(_layer => getLayerFilterByLegendFormat(_layer, LEGEND_FORMAT.JSON)); @@ -106,12 +103,11 @@ class StyleBasedWMSJsonLegend extends React.Component { } this.setState({ loading: true }); getJsonWMSLegend(jsonLegendUrl).then(data => { - const dynamicLegendIsEmpty = data.length === 0 || data[0].rules.length === 0; - if ((this.props.layer.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { - this.props.onUpdateNode({ dynamicLegendIsEmpty }); - } + const legendEmpty = data.length === 0 || data[0].rules.length === 0; + this.props.onChange({ legendEmpty: legendEmpty }); this.setState({ jsonLegend: data[0], loading: false }); }).catch(() => { + this.props.onChange({ legendEmpty: true }); this.setState({ error: true, loading: false }); }); } diff --git a/web/client/plugins/TOC/components/TOC.jsx b/web/client/plugins/TOC/components/TOC.jsx index 825d0516b6..c4b6a4f133 100644 --- a/web/client/plugins/TOC/components/TOC.jsx +++ b/web/client/plugins/TOC/components/TOC.jsx @@ -78,7 +78,8 @@ export function ControlledTOC({ nodeToolItems, nodeContentItems, singleDefaultGroup, - theme + theme, + getNodeStyle }) { return ( ); } diff --git a/web/client/plugins/TOC/components/WMSLegend.jsx b/web/client/plugins/TOC/components/WMSLegend.jsx index ffd1422f33..d880c73ce9 100644 --- a/web/client/plugins/TOC/components/WMSLegend.jsx +++ b/web/client/plugins/TOC/components/WMSLegend.jsx @@ -68,7 +68,7 @@ class WMSLegend extends React.Component { this.setState({ containerWidth, ...this.state }); // eslint-disable-line -- TODO: need to be fixed } getLegendProps = () => { - return pick(this.props, ['currentZoomLvl', 'scales', 'scaleDependent', 'language', 'projection', 'mapSize', 'mapBbox', 'enableDynamicLegend']); + return pick(this.props, ['currentZoomLvl', 'scales', 'scaleDependent', 'language', 'projection', 'mapSize', 'mapBbox']); } render() { let node = this.props.node || {}; @@ -97,7 +97,7 @@ class WMSLegend extends React.Component { } legendOptions={this.props.WMSLegendOptions} {...this.getLegendProps()} - onUpdateNode={this.props.onChange} + onChange={this.props.onChange} /> ); diff --git a/web/client/plugins/DynamicLegend.jsx b/web/client/plugins/dynamicLegend/DynamicLegend.jsx similarity index 52% rename from web/client/plugins/DynamicLegend.jsx rename to web/client/plugins/dynamicLegend/DynamicLegend.jsx index 0a361f2c31..808516fe00 100644 --- a/web/client/plugins/DynamicLegend.jsx +++ b/web/client/plugins/dynamicLegend/DynamicLegend.jsx @@ -1,78 +1,85 @@ -/* eslint-disable react/jsx-boolean-value */ -import React from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { get } from 'lodash'; -import { Glyphicon } from 'react-bootstrap'; - -import { createPlugin } from '../utils/PluginsUtils'; -import { groupsSelector, layersSelector } from '../selectors/layers'; -import { keepLayer } from '../selectors/dynamiclegend'; -import { mapSelector } from '../selectors/map'; -import { updateNode } from '../actions/layers'; -import controls from '../reducers/controls'; -import { toggleControl } from '../actions/controls'; -import Message from '../components/I18N/Message'; - -import DynamicLegend from './dynamicLegend/components/DynamicLegend'; - -export default createPlugin('DynamicLegend', { - component: connect( - createSelector([ - (state) => get(state, 'controls.dynamic-legend.enabled'), - groupsSelector, - layersSelector, - mapSelector - ], (isVisible, groups, layers, map) => ({ - isVisible, - groups, - layers: layers.filter(keepLayer), - currentZoomLvl: map?.zoom, - mapBbox: map?.bbox - })), - { - onClose: toggleControl.bind(null, 'dynamic-legend', null), - onUpdateNode: updateNode - } - )(DynamicLegend), - options: { - disablePluginIf: "{state('router') && (state('router').endsWith('new') || state('router').includes('newgeostory') || state('router').endsWith('dashboard'))}" - }, - reducers: { controls }, - epics: {}, - containers: { - BurgerMenu: { - name: 'dynamic-legend', - position: 1000, - priority: 2, - doNotHide: true, - text: , - tooltip: , - icon: , - action: toggleControl.bind(null, 'dynamic-legend', null), - toggle: true - }, - SidebarMenu: { - name: 'dynamic-legend', - position: 1000, - priority: 1, - doNotHide: true, - text: , - tooltip: , - icon: , - action: toggleControl.bind(null, 'dynamic-legend', null), - toggle: true - }, - Toolbar: { - name: 'dynamic-legend', - alwaysVisible: true, - position: 2, - priority: 0, - doNotHide: true, - tooltip: , - icon: , - action: toggleControl.bind(null, 'dynamic-legend', null), - toggle: true - } - } -}); +/* +* Copyright 2025, GeoSolutions Sas. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { get } from 'lodash'; +import { Glyphicon } from 'react-bootstrap'; + +import { createPlugin } from '../../utils/PluginsUtils'; +import { groupsSelector } from '../../selectors/layers'; +import { currentZoomLevelSelector, mapBboxSelector, currentResolutionSelector } from '../../selectors/map'; +import { updateNode } from '../../actions/layers'; +import controls from '../../reducers/controls'; +import { toggleControl } from '../../actions/controls'; +import Message from '../../components/I18N/Message'; + +import DynamicLegend from './components/DynamicLegend'; + +const DynamicLegendPlugin = connect( + createSelector([ + (state) => get(state, 'controls.dynamic-legend.enabled'), + groupsSelector, + currentZoomLevelSelector, + mapBboxSelector, + currentResolutionSelector + ], (isVisible, groups, currentZoomLvl, mapBbox, resolution) => ({ + isVisible, + groups, + currentZoomLvl, + mapBbox, + resolution + })), + { + onClose: toggleControl.bind(null, 'dynamic-legend', null), + onUpdateNode: updateNode + } +)(DynamicLegend); + +export default createPlugin('DynamicLegend', { + component: DynamicLegendPlugin, + reducers: { controls }, + epics: {}, + containers: { + // review containers + BurgerMenu: { + name: 'dynamic-legend', + position: 1000, + priority: 2, + doNotHide: true, + text: , + tooltip: , + icon: , + action: toggleControl.bind(null, 'dynamic-legend', null), + toggle: true + }, + SidebarMenu: { + name: 'dynamic-legend', + position: 1000, + priority: 1, + doNotHide: true, + text: , + tooltip: , + icon: , + action: toggleControl.bind(null, 'dynamic-legend', null), + toggle: true + }, + Toolbar: { + name: 'dynamic-legend', + alwaysVisible: true, + position: 2, + priority: 0, + doNotHide: true, + tooltip: , + icon: , + action: toggleControl.bind(null, 'dynamic-legend', null), + toggle: true + } + } +}); diff --git a/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx b/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx index 5b71d4a35b..fde6d5b9b0 100644 --- a/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx +++ b/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx @@ -1,70 +1,57 @@ +/* +* Copyright 2025, GeoSolutions Sas. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + import React from 'react'; -import { keepNode, isLayerVisible } from '../../../selectors/dynamiclegend'; -import { getResolutions } from '../../../utils/MapUtils'; import Message from '../../../components/I18N/Message'; import ResizableModal from '../../../components/misc/ResizableModal'; import { ControlledTOC } from '../../TOC/components/TOC'; import DefaultGroup from '../../TOC/components/DefaultGroup'; import DefaultLayer from '../../TOC/components/DefaultLayer'; +import { getNodeStyle, keepLayer } from '../utils/DynamicLegendUtils'; import '../assets/dynamicLegend.css'; -function applyVersionParamToLegend(layer) { - // we need to pass a parameter that invalidate the cache for GetLegendGraphic - // all layer inside the dataset viewer apply a new _v_ param each time we switch page - return { ...layer, legendParams: { ...layer?.legendParams, _v_: layer?._v_ } }; -} - -function filterNode(node, currentResolution) { - const nodes = Array.isArray(node.nodes) ? node.nodes.filter(keepNode).map(applyVersionParamToLegend).map(n => filterNode(n, currentResolution)) : undefined; +// keep the custom component outside of the component render function +// to avoid too many remount of the nodes +const CustomGroupNodeComponent = props => { + return ( + + ); +}; - return { - ...node, - isVisible: (node.visibility ?? true) && (nodes ? nodes.length > 0 && nodes.some(n => n.isVisible) : isLayerVisible(node, currentResolution)), - ...(nodes && { nodes }) - }; -} +const CustomLayerNodeComponent = ({ node, ...props }) => { + if (!keepLayer(node)) { + return null; + } + return ( + + ); +}; -export default ({ - layers, +const DynamicLegend = ({ onUpdateNode, currentZoomLvl, onClose, isVisible, groups, - mapBbox + mapBbox, + resolution }) => { - const layerDict = layers.reduce((acc, layer) => ({ - ...acc, - ...{ - [layer.id]: { - ...layer, - ...{enableDynamicLegend: true, enableInteractiveLegend: false} - } - } - }), {}); - const getVisibilityStyle = nodeVisibility => ({ - opacity: nodeVisibility ? 1 : 0, - height: nodeVisibility ? "auto" : "0" - }); - const customGroupNodeComponent = props => ( -
    - -
    - ); - const customLayerNodeComponent = props => { - const layer = layerDict[props.node.id]; - if (!layer) { - return null; - } - - return ( -
    - -
    - ); - }; + // TODO: show message about empty legend root + // const legendVisible = isLegendGroupVisible(groups.length === 1 ? groups[0] : { nodes: groups }, resolution); return ( getNodeStyle(node, nodeType, resolution)} className="legend-content" theme="legend" onChange={onUpdateNode} - groupNodeComponent={customGroupNodeComponent} - layerNodeComponent={customLayerNodeComponent} + groupNodeComponent={CustomGroupNodeComponent} + layerNodeComponent={CustomLayerNodeComponent} config={{ sortable: false, showFullTitle: true, @@ -90,11 +78,11 @@ export default ({ expanded: true, zoom: currentZoomLvl, layerOptions: { - enableDynamicLegend: true, legendOptions: { legendWidth: 12, legendHeight: 12, - mapBbox + mapBbox, + WMSLegendOptions: 'countMatched:true;fontAntiAliasing:true;' } } }} @@ -102,3 +90,5 @@ export default ({ ); }; + +export default DynamicLegend; diff --git a/web/client/plugins/dynamicLegend/index.js b/web/client/plugins/dynamicLegend/index.js new file mode 100644 index 0000000000..5fd95b29f0 --- /dev/null +++ b/web/client/plugins/dynamicLegend/index.js @@ -0,0 +1,9 @@ +/* +* Copyright 2025, GeoSolutions Sas. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +export { default } from './DynamicLegend'; diff --git a/web/client/plugins/dynamicLegend/utils/DynamicLegendUtils.js b/web/client/plugins/dynamicLegend/utils/DynamicLegendUtils.js new file mode 100644 index 0000000000..a7f2ecae40 --- /dev/null +++ b/web/client/plugins/dynamicLegend/utils/DynamicLegendUtils.js @@ -0,0 +1,29 @@ +/* +* Copyright 2025, GeoSolutions Sas. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +import { flattenArrayOfObjects, NodeTypes, isInsideResolutionsLimits } from '../../../utils/LayersUtils'; + +export const keepLayer = layer => (layer.group !== 'background' && ['wms', 'arcgis'].includes(layer.type)); + +export const isLegendLayerVisible = (node, resolution) => { + return node.visibility !== false && !node.legendEmpty && isInsideResolutionsLimits(node, resolution); +}; + +export const isLegendGroupVisible = (node, resolution) => { + const flattenChildren = flattenArrayOfObjects(node.nodes); + const flattenLayers = flattenChildren.filter(child => !child.nodes); + const childrenLayersVisible = flattenLayers.some(child => isLegendLayerVisible(child, resolution)); + return node.visibility !== false && flattenLayers.length > 0 && childrenLayersVisible; +}; + +export const getNodeStyle = (node, nodeType, resolution) => { + const visible = nodeType === NodeTypes.GROUP + ? isLegendGroupVisible(node, resolution) + : isLegendLayerVisible(node, resolution); + return visible ? { } : { display: 'none' }; +}; diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index f65f458392..173a81ebf6 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -70,7 +70,7 @@ export const plugins = { DashboardImport: toModulePlugin('DashboardImport', () => import( /* webpackChunkName: 'plugins/dashboardImport' */'../plugins/DashboardImport')), DetailsPlugin: toModulePlugin('Details', () => import(/* webpackChunkName: 'plugins/details' */ '../plugins/Details')), DrawerMenuPlugin: toModulePlugin('DrawerMenu', () => import(/* webpackChunkName: 'plugins/drawerMenu' */ '../plugins/DrawerMenu')), - DynamicLegendPlugin: toModulePlugin('DynamicLegend', () => import(/* webpackChunkName: 'plugins/dynamiclegend' */ '../plugins/DynamicLegend')), + DynamicLegendPlugin: toModulePlugin('DynamicLegend', () => import(/* webpackChunkName: 'plugins/dynamicLegend' */ '../plugins/DynamicLegend')), ExpanderPlugin: toModulePlugin('Expander', () => import(/* webpackChunkName: 'plugins/expander' */ '../plugins/Expander')), FilterLayerPlugin: toModulePlugin('FilterLayer', () => import(/* webpackChunkName: 'plugins/filterLayer' */ '../plugins/FilterLayer')), FullScreenPlugin: toModulePlugin('FullScreen', () => import(/* webpackChunkName: 'plugins/fullScreen' */ '../plugins/FullScreen')), diff --git a/web/client/selectors/dynamiclegend.js b/web/client/selectors/dynamiclegend.js deleted file mode 100644 index 48fd7dd6be..0000000000 --- a/web/client/selectors/dynamiclegend.js +++ /dev/null @@ -1,14 +0,0 @@ -const isLayer = node => node.nodeType === "layers"; - -export const keepLayer = layer => (layer.group !== 'background' && ['wms', 'arcgis'].includes(layer.type)); - -export const keepNode = node => !isLayer(node) || keepLayer(node); - -export const isLayerVisible = (node, currentResolution) => keepLayer(node) - && (!node.hasOwnProperty('dynamicLegendIsEmpty') || !node.dynamicLegendIsEmpty) - && ( - (node.disableResolutionLimits ?? true) || ( - (!node.minResolution || node.minResolution <= currentResolution) && - (!node.maxResolution || node.maxResolution > currentResolution) - ) - ); From 12fb4ab2015ad2349b1a47d5636bccb704b3be1f Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 6 Jun 2025 09:08:17 +0200 Subject: [PATCH 28/49] Revert to original code --- web/client/utils/LegendUtils.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js index e328b9acda..bc33ab8f5a 100644 --- a/web/client/utils/LegendUtils.js +++ b/web/client/utils/LegendUtils.js @@ -58,12 +58,11 @@ export const getWMSLegendConfig = ({ }; if (layer.serverType !== ServerTypes.NO_VENDOR) { const addContentDependantParams = layer.enableDynamicLegend || layer.enableInteractiveLegend; - const layerisNotBackground = layer.group !== "background"; return { ...baseParams, ...(addContentDependantParams && { - // hideEmptyRules, countMatched and fontAntiAliasing are applied for all layers except background layers - LEGEND_OPTIONS: `hideEmptyRules:${layerisNotBackground};countMatched:${layerisNotBackground};fontAntiAliasing:${layerisNotBackground};${legendOptions}`, + // hideEmptyRules is applied for all layers except background layers + LEGEND_OPTIONS: `hideEmptyRules:${layer.group !== "background"};${legendOptions}`, SRCWIDTH: mapSize?.width ?? 512, SRCHEIGHT: mapSize?.height ?? 512, SRS: projection, From 9890e7b1018798ffc01cf674a7b04d431ef0a797 Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 6 Jun 2025 09:08:34 +0200 Subject: [PATCH 29/49] Add Dynamic Legend options --- web/client/plugins/dynamicLegend/components/DynamicLegend.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx b/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx index 5b71d4a35b..1f825a97d5 100644 --- a/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx +++ b/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx @@ -92,6 +92,7 @@ export default ({ layerOptions: { enableDynamicLegend: true, legendOptions: { + WMSLegendOptions: "countMatched:true;fontAntiAliasing:true;", legendWidth: 12, legendHeight: 12, mapBbox From 6880a6056364eac5f51e351d3e3c4579d9e96c13 Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 6 Jun 2025 09:13:18 +0200 Subject: [PATCH 30/49] Add WMSLegendOptions --- web/client/plugins/dynamicLegend/components/DynamicLegend.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx b/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx index 5b71d4a35b..1f825a97d5 100644 --- a/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx +++ b/web/client/plugins/dynamicLegend/components/DynamicLegend.jsx @@ -92,6 +92,7 @@ export default ({ layerOptions: { enableDynamicLegend: true, legendOptions: { + WMSLegendOptions: "countMatched:true;fontAntiAliasing:true;", legendWidth: 12, legendHeight: 12, mapBbox From e23cb1ebf68313a099b065e1c80e004104a5909b Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 6 Jun 2025 10:40:55 +0200 Subject: [PATCH 31/49] =?UTF-8?q?Select=20=E2=86=92=20LayersSelection=20in?= =?UTF-8?q?=20translations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/client/translations/data.de-DE.json | 4 ++-- web/client/translations/data.en-US.json | 6 +++--- web/client/translations/data.es-ES.json | 4 ++-- web/client/translations/data.fr-FR.json | 4 ++-- web/client/translations/data.it-IT.json | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 5328975c8f..584a1872d3 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -1567,7 +1567,7 @@ } } }, - "select": { + "layersSelection": { "title": "Auswählen", "tooltip": "Auswahlwerkzeug anzeigen", "description": "Auswahlwerkzeug anzeigen", @@ -3507,7 +3507,7 @@ "description": "Ermöglicht das Erstellen eines Permalinks der aktuell angezeigten Ressource", "title": "Permalink" }, - "Select": { + "LayersSelection": { "title": "Auswählen", "description": "Auswahlwerkzeug anzeigen" }, diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 8a7dd9c67b..d501f8c227 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -1528,7 +1528,7 @@ } } }, - "select": { + "layersSelection": { "title": "Select", "tooltip": "Display the selection tool", "description": "Display the selection tool", @@ -3478,9 +3478,9 @@ "description": "Allows to create a permalink of the current resource in view", "title": "Permalink" }, - "Select": { + "LayersSelection": { "title": "Select", - "description": "Kartenlegende anzeigen" + "description": "Display the selection tool" }, "StreetView": { "title": "Street View", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index f8464a49ea..6726d51763 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -1528,7 +1528,7 @@ } } }, - "select": { + "layersSelection": { "title": "Seleccionar", "tooltip": "Mostrar la herramienta de selección", "description": "Mostrar la herramienta de selección", @@ -3468,7 +3468,7 @@ "description": "Permite crear un enlace permanente del recurso actual a la vista", "title": "Enlace permanente" }, - "Select": { + "LayersSelection": { "title": "Seleccionar", "description": "Mostrar la herramienta de selección" }, diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 64aa029be6..a1ab8d5d6c 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -1529,7 +1529,7 @@ } } }, - "select": { + "layersSelection": { "title": "Sélectionner", "tooltip": "Afficher l'outil de sélection", "description": "Afficher l'outil de sélection", @@ -3469,7 +3469,7 @@ "description": "Permet de créer un permalien de la ressource courante en vue", "title": "Lien permanent" }, - "Select": { + "LayersSelection": { "title": "Sélectionner", "description": "Afficher l'outil de sélection" }, diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index a89082f8fd..7fdb846d39 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -1528,7 +1528,7 @@ } } }, - "select": { + "layersSelection": { "title": "Selezionare", "tooltip": "Mostra lo strumento di selezione", "description": "Mostra lo strumento di selezione", @@ -3470,7 +3470,7 @@ "description": "Permette di creare un permalink della risorsa attualmente in vista", "title": "Permalink" }, - "Select": { + "LayersSelection": { "title": "Selezionare", "description": "Mostra lo strumento di selezione" }, From 2183893f053ec2d40a6bb20271e4e35517ba8a1b Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 6 Jun 2025 10:43:20 +0200 Subject: [PATCH 32/49] =?UTF-8?q?Select=20=E2=86=92=20LayersSelection=20fo?= =?UTF-8?q?r=20configs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project/standard/templates/configs/pluginsConfig.json | 6 +++--- web/client/configs/localConfig.json | 2 +- web/client/configs/pluginsConfig.json | 6 +++--- web/client/configs/simple.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/project/standard/templates/configs/pluginsConfig.json b/project/standard/templates/configs/pluginsConfig.json index d330878a0e..3113d7b773 100644 --- a/project/standard/templates/configs/pluginsConfig.json +++ b/project/standard/templates/configs/pluginsConfig.json @@ -147,10 +147,10 @@ "denyUserSelection": true }, { - "name": "Select", + "name": "LayersSelection", "glyph": "hand-down", - "title": "plugins.Select.title", - "description": "plugins.Select.description", + "title": "plugins.LayersSelection.title", + "description": "plugins.LayersSelection.description", "dependencies": [ "Toolbar", "BurgerMenu", diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index e1b4558aa9..97969cb8f7 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -420,7 +420,7 @@ } }, { - "name": "Select", + "name": "LayersSelection", "cfg": { "highlightOptions": { "color": "#3388ff", diff --git a/web/client/configs/pluginsConfig.json b/web/client/configs/pluginsConfig.json index 2eebde5412..610d8880c4 100644 --- a/web/client/configs/pluginsConfig.json +++ b/web/client/configs/pluginsConfig.json @@ -146,10 +146,10 @@ "description": "plugins.Permalink.description" }, { - "name": "Select", + "name": "LayersSelection", "glyph": "hand-down", - "title": "plugins.Select.title", - "description": "plugins.Select.description", + "title": "plugins.LayersSelection.title", + "description": "plugins.LayersSelection.description", "dependencies": [ "Toolbar", "BurgerMenu", diff --git a/web/client/configs/simple.json b/web/client/configs/simple.json index c69cfcbd81..b1f942ffca 100644 --- a/web/client/configs/simple.json +++ b/web/client/configs/simple.json @@ -90,7 +90,7 @@ } }, { - "name": "SelectExtension", + "name": "LayersSelection", "cfg": { "highlightOptions": { "color": "#3388ff", From 6b790cbf04cb8893fd5c83d7d977d149c603d79a Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 6 Jun 2025 10:43:55 +0200 Subject: [PATCH 33/49] =?UTF-8?q?Select=20=E2=86=92=20LayersSelection=20fo?= =?UTF-8?q?r=20plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/client/product/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index e97b2f0b19..6aea707181 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -87,6 +87,7 @@ export const plugins = { LanguagePlugin: toModulePlugin('Language', () => import(/* webpackChunkName: 'plugins/language' */ '../plugins/Language')), LayerDownload: toModulePlugin('LayerDownload', () => import(/* webpackChunkName: 'plugins/layerDownload' */ '../plugins/LayerDownload')), LayerInfoPlugin: toModulePlugin('LayerInfo', () => import(/* webpackChunkName: 'plugins/layerInfo' */ '../plugins/LayerInfo')), + LayersSelectionPlugin: toModulePlugin('LayersSelection', () => import(/* webpackChunkName: 'plugins/snapshot' */ '../plugins/LayersSelection')), LocatePlugin: toModulePlugin('Locate', () => import(/* webpackChunkName: 'plugins/locate' */ '../plugins/Locate')), LongitudinalProfileToolPlugin: toModulePlugin('LongitudinalProfileTool', () => import(/* webpackChunkName: 'plugins/LongitudinalProfileTool' */ '../plugins/LongitudinalProfileTool')), ManagerMenuPlugin: toModulePlugin('ManagerMenu', () => import(/* webpackChunkName: 'plugins/managerMenu' */ '../plugins/manager/ManagerMenu')), @@ -118,7 +119,6 @@ export const plugins = { SidebarMenuPlugin: toModulePlugin('SidebarMenu', () => import(/* webpackChunkName: 'plugins/sidebarMenu' */ '../plugins/SidebarMenu')), SharePlugin: toModulePlugin('Share', () => import(/* webpackChunkName: 'plugins/share' */ '../plugins/Share')), PermalinkPlugin: toModulePlugin('Permalink', () => import(/* webpackChunkName: 'plugins/permalink' */ '../plugins/Permalink')), - SelectPlugin: toModulePlugin('Select', () => import(/* webpackChunkName: 'plugins/snapshot' */ '../plugins/Select')), SnapshotPlugin: toModulePlugin('Snapshot', () => import(/* webpackChunkName: 'plugins/snapshot' */ '../plugins/Snapshot')), StreetView: toModulePlugin('StreetView', () => import(/* webpackChunkName: 'plugins/streetView' */ '../plugins/StreetView')), StyleEditor: toModulePlugin('StyleEditor', () => import(/* webpackChunkName: 'plugins/styleEditor' */ '../plugins/StyleEditor')), From bb840f7b2692ea9afc7161aa3f9383885ad29710 Mon Sep 17 00:00:00 2001 From: pln Date: Fri, 6 Jun 2025 10:44:17 +0200 Subject: [PATCH 34/49] =?UTF-8?q?Select=20=E2=86=92=20LayersSelection=20fo?= =?UTF-8?q?r=20the=20plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Select.jsx => LayersSelection.jsx} | 20 ++++++------- .../actions/layersSelection.js} | 0 .../assets/select.css | 0 .../EllipsisButton/EllipsisButton.css | 0 .../EllipsisButton/EllipsisButton.jsx | 20 ++++++------- .../EllipsisButton/Statistics/Statistics.css | 0 .../EllipsisButton/Statistics/Statistics.jsx | 16 +++++----- .../components/LayersSelection.jsx} | 10 +++---- .../LayersSelectionHeader.css} | 0 .../LayersSelectionHeader.jsx} | 14 ++++----- .../layersSelection/epics/layersSelection.js} | 30 +++++++++---------- .../reducers/layersSelection.js} | 2 +- .../selectors/layersSelection.js} | 0 .../layersSelection/utils/LayersSelection.js} | 4 +-- 14 files changed, 58 insertions(+), 58 deletions(-) rename web/client/plugins/{Select.jsx => LayersSelection.jsx} (78%) rename web/client/{actions/select.js => plugins/layersSelection/actions/layersSelection.js} (100%) rename web/client/plugins/{select => layersSelection}/assets/select.css (100%) rename web/client/plugins/{select => layersSelection}/components/EllipsisButton/EllipsisButton.css (100%) rename web/client/plugins/{select => layersSelection}/components/EllipsisButton/EllipsisButton.jsx (91%) rename web/client/plugins/{select => layersSelection}/components/EllipsisButton/Statistics/Statistics.css (100%) rename web/client/plugins/{select => layersSelection}/components/EllipsisButton/Statistics/Statistics.jsx (71%) rename web/client/plugins/{select/components/Select.jsx => layersSelection/components/LayersSelection.jsx} (95%) rename web/client/plugins/{select/components/SelectHeader/SelectHeader.css => layersSelection/components/LayersSelectionHeader/LayersSelectionHeader.css} (100%) rename web/client/plugins/{select/components/SelectHeader/SelectHeader.jsx => layersSelection/components/LayersSelectionHeader/LayersSelectionHeader.jsx} (86%) rename web/client/{epics/select.js => plugins/layersSelection/epics/layersSelection.js} (89%) rename web/client/{reducers/select.js => plugins/layersSelection/reducers/layersSelection.js} (91%) rename web/client/{selectors/select.js => plugins/layersSelection/selectors/layersSelection.js} (100%) rename web/client/{utils/Select.js => plugins/layersSelection/utils/LayersSelection.js} (91%) diff --git a/web/client/plugins/Select.jsx b/web/client/plugins/LayersSelection.jsx similarity index 78% rename from web/client/plugins/Select.jsx rename to web/client/plugins/LayersSelection.jsx index 956dd03e65..17659e6a1d 100644 --- a/web/client/plugins/Select.jsx +++ b/web/client/plugins/LayersSelection.jsx @@ -12,11 +12,11 @@ import controls from '../reducers/controls'; import { toggleControl } from '../actions/controls'; import Message from '../components/I18N/Message'; -import SelectComponent from './select/components/Select'; -import epics from '../epics/select'; -import select from '../reducers/select'; -import { storeConfiguration, cleanSelection, addOrUpdateSelection } from '../actions/select'; -import { getSelectSelections, getSelectQueryMaxFeatureCount } from '../selectors/select'; +import SelectComponent from './layersSelection/components/LayersSelection'; +import epics from './layersSelection/epics/layersSelection'; +import select from './layersSelection/reducers/layersSelection'; +import { storeConfiguration, cleanSelection, addOrUpdateSelection } from './layersSelection/actions/layersSelection'; +import { getSelectSelections, getSelectQueryMaxFeatureCount } from './layersSelection/selectors/layersSelection'; export default createPlugin('Select', { component: connect( @@ -56,8 +56,8 @@ export default createPlugin('Select', { position: 1000, priority: 2, doNotHide: true, - text: , - tooltip: , + text: , + tooltip: , icon: , action: toggleControl.bind(null, 'select', null), toggle: true @@ -67,8 +67,8 @@ export default createPlugin('Select', { position: 1000, priority: 1, doNotHide: true, - text: , - tooltip: , + text: , + tooltip: , icon: , action: toggleControl.bind(null, 'select', null), toggle: true @@ -79,7 +79,7 @@ export default createPlugin('Select', { position: 2, priority: 0, doNotHide: true, - tooltip: , + tooltip: , icon: , action: toggleControl.bind(null, 'select', null), toggle: true diff --git a/web/client/actions/select.js b/web/client/plugins/layersSelection/actions/layersSelection.js similarity index 100% rename from web/client/actions/select.js rename to web/client/plugins/layersSelection/actions/layersSelection.js diff --git a/web/client/plugins/select/assets/select.css b/web/client/plugins/layersSelection/assets/select.css similarity index 100% rename from web/client/plugins/select/assets/select.css rename to web/client/plugins/layersSelection/assets/select.css diff --git a/web/client/plugins/select/components/EllipsisButton/EllipsisButton.css b/web/client/plugins/layersSelection/components/EllipsisButton/EllipsisButton.css similarity index 100% rename from web/client/plugins/select/components/EllipsisButton/EllipsisButton.css rename to web/client/plugins/layersSelection/components/EllipsisButton/EllipsisButton.css diff --git a/web/client/plugins/select/components/EllipsisButton/EllipsisButton.jsx b/web/client/plugins/layersSelection/components/EllipsisButton/EllipsisButton.jsx similarity index 91% rename from web/client/plugins/select/components/EllipsisButton/EllipsisButton.jsx rename to web/client/plugins/layersSelection/components/EllipsisButton/EllipsisButton.jsx index 79d67c611d..af4985c126 100644 --- a/web/client/plugins/select/components/EllipsisButton/EllipsisButton.jsx +++ b/web/client/plugins/layersSelection/components/EllipsisButton/EllipsisButton.jsx @@ -7,7 +7,7 @@ import axios from 'axios'; import Message from '../../../../components/I18N/Message'; import { describeFeatureType } from '../../../../api/WFS'; -import { SelectRefContext } from '../Select'; +import { SelectRefContext } from '../LayersSelection'; import Statistics from './Statistics/Statistics'; import './EllipsisButton.css'; @@ -196,24 +196,24 @@ export default ({ {menuOpen && (
    -

    triggerAction('zoomTo')}>

    -

    { toggleMenu(); selectionData.features?.length > 0 ? setStatisticsOpen(true) : null;}}>

    -

    triggerAction('createLayer')}>

    - {node.type !== 'arcgis' &&

    triggerAction('filterData')}>

    } +

    triggerAction('zoomTo')}>

    +

    { toggleMenu(); selectionData.features?.length > 0 ? setStatisticsOpen(true) : null;}}>

    +

    triggerAction('createLayer')}>

    + {node.type !== 'arcgis' &&

    triggerAction('filterData')}>

    }

    - + {exportOpen ? "−" : "+"}

    {exportOpen && (
    -

    triggerAction('exportToGeoJson')}> -

    -

    triggerAction('exportToJson')}> -

    -

    triggerAction('exportToCsv')}> -

    +

    triggerAction('exportToGeoJson')}> -

    +

    triggerAction('exportToJson')}> -

    +

    triggerAction('exportToCsv')}> -

    )}
    -

    triggerAction('clear')}>

    +

    triggerAction('clear')}>

    )} {statisticsOpen && } + title={} size="sm" // eslint-disable-next-line react/jsx-boolean-value show={true} @@ -46,7 +46,7 @@ export default ({ }]}>
    - +