From f9cca03ddcf77236c6316975ec16f7ae8375dd77 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 5 Dec 2024 19:13:05 +0530 Subject: [PATCH 1/7] #10684: Legend filtering for GeoServer WMS layers --- .../TOC/fragments/settings/Display.jsx | 16 +- .../widgets/builder/wizard/map/TOC.jsx | 3 + .../wizard/map/enhancers/nodeEditor.js | 3 + .../widgets/enhancers/legendWidget.js | 37 +++- .../components/widgets/widget/LegendView.jsx | 7 +- web/client/plugins/Print.jsx | 3 +- web/client/plugins/TOC/components/Legend.jsx | 49 +++--- .../components/StyleBasedWMSJsonLegend.jsx | 115 ++++++++---- web/client/plugins/TOC/components/TOC.jsx | 13 +- .../plugins/TOC/components/WMSLegend.jsx | 23 +-- web/client/plugins/TOC/index.js | 14 +- .../tocitemssettings/defaultSettingsTabs.js | 2 +- web/client/themes/default/less/toc.less | 8 + web/client/translations/data.de-DE.json | 6 +- web/client/translations/data.en-US.json | 6 +- web/client/translations/data.es-ES.json | 6 +- web/client/translations/data.fr-FR.json | 6 +- web/client/translations/data.it-IT.json | 7 +- web/client/utils/FilterUtils.js | 16 +- web/client/utils/LegendUtils.js | 81 +++++++++ web/client/utils/PrintUtils.js | 50 +++--- .../utils/__tests__/FilterUtils-test.js | 19 +- .../utils/__tests__/LegendUtils-test.js | 164 ++++++++++++++++++ 23 files changed, 522 insertions(+), 132 deletions(-) create mode 100644 web/client/utils/LegendUtils.js create mode 100644 web/client/utils/__tests__/LegendUtils-test.js diff --git a/web/client/components/TOC/fragments/settings/Display.jsx b/web/client/components/TOC/fragments/settings/Display.jsx index 2b7a97d497..93eb288ce5 100644 --- a/web/client/components/TOC/fragments/settings/Display.jsx +++ b/web/client/components/TOC/fragments/settings/Display.jsx @@ -6,10 +6,14 @@ * LICENSE file in the root directory of this source tree. */ -import { clamp, isNil, isNumber } from 'lodash'; -import PropTypes from 'prop-types'; import React from 'react'; +import clamp from 'lodash/clamp'; +import isNil from 'lodash/isNil'; +import isNumber from 'lodash/isNumber'; +import pick from 'lodash/pick'; +import PropTypes from 'prop-types'; import {Checkbox, Col, ControlLabel, FormGroup, Glyphicon, Grid, Row, Button as ButtonRB } from 'react-bootstrap'; + import tooltip from '../../../misc/enhancers/buttonTooltip'; const Button = tooltip(ButtonRB); import IntlNumberFormControl from '../../../I18N/IntlNumberFormControl'; @@ -26,6 +30,7 @@ import ThreeDTilesSettings from './ThreeDTilesSettings'; import ModelTransformation from './ModelTransformation'; import StyleBasedWMSJsonLegend from '../../../../plugins/TOC/components/StyleBasedWMSJsonLegend'; import { getMiscSetting } from '../../../../utils/ConfigUtils'; + export default class extends React.Component { static propTypes = { opacityText: PropTypes.node, @@ -38,6 +43,8 @@ export default class extends React.Component { isLocalizedLayerStylesEnabled: PropTypes.bool, isCesiumActive: PropTypes.bool, projection: PropTypes.string, + mapSize: PropTypes.object, + mapBbox: PropTypes.object, resolutions: PropTypes.array, zoom: PropTypes.number, hideInteractiveLegendOption: PropTypes.bool @@ -122,6 +129,9 @@ export default class extends React.Component { } return null; }; + getLegendProps = () => { + return pick(this.props, ['projection', 'mapSize', 'mapBbox']); + } render() { const formatValue = this.props.element && this.props.element.format || "image/png"; const experimentalInteractiveLegend = getMiscSetting('experimentalInteractiveLegend', false); @@ -324,6 +334,7 @@ export default class extends React.Component { this.useLegendOptions() && this.state.legendOptions.legendWidth || undefined} language={ this.props.isLocalizedLayerStylesEnabled ? this.props.currentLocaleLanguage : undefined} + {...this.getLegendProps()} /> : } diff --git a/web/client/components/widgets/builder/wizard/map/TOC.jsx b/web/client/components/widgets/builder/wizard/map/TOC.jsx index 17c899c28c..e71827eb05 100644 --- a/web/client/components/widgets/builder/wizard/map/TOC.jsx +++ b/web/client/components/widgets/builder/wizard/map/TOC.jsx @@ -35,6 +35,9 @@ function WidgetTOC({ visualizationMode: map?.visualizationMode, layerOptions: { legendOptions: { + projection: map?.projection, + mapSize: map?.size, + mapBbox: map?.bbox, WMSLegendOptions: 'forceLabels:on', scaleDependent: true, legendWidth: 12, diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/nodeEditor.js b/web/client/components/widgets/builder/wizard/map/enhancers/nodeEditor.js index 29e682fa63..aab1cb246f 100644 --- a/web/client/components/widgets/builder/wizard/map/enhancers/nodeEditor.js +++ b/web/client/components/widgets/builder/wizard/map/enhancers/nodeEditor.js @@ -80,6 +80,9 @@ export default compose( : 1 } }, + projection: map.projection, + mapSize: map.size, + mapBbox: map.bbox, groups: get(splitMapAndLayers(map), 'layers.groups') })), // adapter for handlers diff --git a/web/client/components/widgets/enhancers/legendWidget.js b/web/client/components/widgets/enhancers/legendWidget.js index 635e3b23a6..8336438b0f 100644 --- a/web/client/components/widgets/enhancers/legendWidget.js +++ b/web/client/components/widgets/enhancers/legendWidget.js @@ -7,34 +7,63 @@ */ import {compose, withHandlers, withProps} from 'recompose'; -import { castArray, get } from 'lodash'; +import { castArray, get, isEmpty, find } from 'lodash'; import deleteWidget from './deleteWidget'; import { editableWidget, defaultIcons, withHeaderTools } from './tools'; import { getScales } from '../../../utils/MapUtils'; import { WIDGETS_MAPS_REGEX } from "../../../actions/widgets"; import { getInactiveNode, DEFAULT_GROUP_ID } from '../../../utils/LayersUtils'; +import { composeFilterObject } from './utils'; +import { toCQLFilter } from '../../../utils/FilterUtils'; +import { arrayUpdate } from '../../../utils/ImmutableUtils'; +import { optionsToVendorParams } from '../../../utils/VendorParamsUtils'; /** * map dependencies to layers, scales and current zoom level to show legend items for current zoom. * Add also base tools and menu to the widget */ export default compose( - withProps(({ dependencies = {}, dependenciesMap = {} }) => { + withProps(({ dependencies = {}, dependenciesMap = {}, mapSync }) => { const allLayers = dependencies[dependenciesMap.layers] || dependencies.layers || []; const groups = castArray(dependencies[dependenciesMap.groups] || dependencies.groups || []); - const layers = allLayers + let layers = allLayers // filter backgrounds and inactive layer // the inactive layers are the one with a not visible parent group .filter((layer = {}) => layer.group !== 'background' && !getInactiveNode(layer?.group || DEFAULT_GROUP_ID, groups) ) .map(({ group, ...layer }) => layer); + const targetLayerName = dependencies && dependencies.layer && dependencies.layer.name; + const filterObj = dependencies.filter || {}; + const layerInCommon = find(layers, {name: targetLayerName}) || {}; + let filterObjCollection = {}; + let layersUpdatedWithCql = {}; + let cqlFilter = undefined; + + if (mapSync && !isEmpty(layerInCommon) && (filterObj.featureTypeName ? filterObj.featureTypeName === targetLayerName : true)) { + if (dependencies.quickFilters) { + filterObjCollection = {...filterObjCollection, ...composeFilterObject(filterObj, dependencies.quickFilters, dependencies.options)}; + } + cqlFilter = toCQLFilter(filterObjCollection); + if (!isEmpty(filterObjCollection) && cqlFilter) { + layersUpdatedWithCql = arrayUpdate(false, + {...layerInCommon, params: optionsToVendorParams({ params: {CQL_FILTER: cqlFilter}})}, + {name: targetLayerName}, + layers + ); + } + } else { + layersUpdatedWithCql = layers.map(l => ({...l, params: {...l.params, CQL_FILTER: undefined}})); + } + layers = layersUpdatedWithCql; return { allLayers, map: { layers, // use empty so it creates the default group that will be hidden in the layers tree - groups: [] + groups: [], + projection: dependencies.projection, + bbox: dependencies.viewport }, dependencyMapPath: dependenciesMap.layers || '', scales: getScales( diff --git a/web/client/components/widgets/widget/LegendView.jsx b/web/client/components/widgets/widget/LegendView.jsx index 69a3b35fac..b7dbe1e004 100644 --- a/web/client/components/widgets/widget/LegendView.jsx +++ b/web/client/components/widgets/widget/LegendView.jsx @@ -35,7 +35,12 @@ export default ({ scales, zoom: currentZoomLvl, layerOptions: { - legendOptions: legendProps, + legendOptions: { + ...legendProps, + projection: map?.projection, + mapSize: map?.size, + mapBbox: map?.bbox + }, hideFilter: true } }} diff --git a/web/client/plugins/Print.jsx b/web/client/plugins/Print.jsx index 045f83f430..12d830b4af 100644 --- a/web/client/plugins/Print.jsx +++ b/web/client/plugins/Print.jsx @@ -605,7 +605,8 @@ export default { this.props.onBeforePrint(); this.props.printingService.print({ layers: this.getMapConfiguration()?.layers, - scales: this.props.useFixedScales ? getPrintScales(this.props.capabilities) : undefined + scales: this.props.useFixedScales ? getPrintScales(this.props.capabilities) : undefined, + bbox: this.props.map?.bbox }) .then((spec) => this.props.onPrint(this.props.capabilities.createURL, { ...spec, ...this.props.overrideOptions }) diff --git a/web/client/plugins/TOC/components/Legend.jsx b/web/client/plugins/TOC/components/Legend.jsx index 9646c8c71a..7f2e3bada7 100644 --- a/web/client/plugins/TOC/components/Legend.jsx +++ b/web/client/plugins/TOC/components/Legend.jsx @@ -6,12 +6,12 @@ * LICENSE file in the root directory of this source tree. */ -import urlUtil from 'url'; - -import { isArray, isNil } from 'lodash'; -import assign from 'object-assign'; import PropTypes from 'prop-types'; import React from 'react'; +import urlUtil from 'url'; +import isArray from 'lodash/isArray'; +import isNil from 'lodash/isNil'; +import pick from 'lodash/pick'; import { addAuthenticationToSLD, @@ -21,6 +21,8 @@ import Message from '../../../components/I18N/Message'; import SecureImage from '../../../components/misc/SecureImage'; import { randomInt } from '../../../utils/RandomUtils'; +import { normalizeSRS } from '../../../utils/CoordinatesUtils'; +import { getWMSLegendConfig, LEGEND_FORMAT } from '../../../utils/LegendUtils'; /** * Legend renders the wms legend image @@ -44,7 +46,10 @@ class Legend extends React.Component { currentZoomLvl: PropTypes.number, scales: PropTypes.array, scaleDependent: PropTypes.bool, - language: PropTypes.string + language: PropTypes.string, + projection: PropTypes.string, + mapSize: PropTypes.object, + bbox: PropTypes.object }; static defaultProps = { @@ -86,26 +91,20 @@ class Legend extends React.Component { const cleanParams = clearNilValuesForParams(layer.params); const scale = this.getScale(props); - let query = assign( - {}, - { - service: "WMS", - request: "GetLegendGraphic", - format: "image/png", - height: props.legendHeight, - width: props.legendWidth, - layer: layer.name, - style: layer.style || null, - version: layer.version || "1.3.0", - SLD_VERSION: "1.1.0", - LEGEND_OPTIONS: props.legendOptions - }, - layer.legendParams || {}, - props.language && layer.localizedLayerStyles ? {LANGUAGE: props.language} : {}, - addAuthenticationToSLD(cleanParams || {}, props.layer), - cleanParams && cleanParams.SLD_BODY ? {SLD_BODY: cleanParams.SLD_BODY} : {}, - scale !== null ? { SCALE: scale } : {} - ); + const projection = normalizeSRS(this.props.projection || 'EPSG:3857', layer.allowedSRS); + const query = { + ...getWMSLegendConfig({ + layer, + format: LEGEND_FORMAT.IMAGE, + ...pick(props, ['legendHeight', 'legendWidth', 'mapSize', 'legendOptions', 'mapBbox']), + projection + }), + ...layer.legendParams, + ...(props.language && layer.localizedLayerStyles ? { LANGUAGE: props.language } : {}), + ...addAuthenticationToSLD(cleanParams || {}, props.layer), + ...(cleanParams && cleanParams.SLD_BODY ? { SLD_BODY: cleanParams.SLD_BODY } : {}), + ...(scale !== null ? { SCALE: scale } : {}) + }; return urlUtil.format({ host: urlObj.host, diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index 4a0a54bdc3..89dc75b544 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -6,24 +6,30 @@ * LICENSE file in the root directory of this source tree. */ -import urlUtil from 'url'; - -import { isArray, isNil } from 'lodash'; -import assign from 'object-assign'; import PropTypes from 'prop-types'; import React from 'react'; -import { Tooltip, Glyphicon } from 'react-bootstrap'; +import urlUtil from 'url'; +import isArray from 'lodash/isArray'; +import isNil from 'lodash/isNil'; +import pick from 'lodash/pick'; +import get from 'lodash/get'; +import isEqual from 'lodash/isEqual'; +import { Alert, Tooltip, Glyphicon } from 'react-bootstrap'; + +import { ButtonWithTooltip } from '../../../components/misc/Button'; import Loader from '../../../components/misc/Loader'; import WMSJsonLegendIcon from '../../../components/styleeditor/WMSJsonLegendIcon'; +import Message from '../../../components/I18N/Message'; +import OverlayTrigger from '../../../components/misc/OverlayTrigger'; import { addAuthenticationParameter, addAuthenticationToSLD, clearNilValuesForParams } from '../../../utils/SecurityUtils'; import { getJsonWMSLegend } from '../../../api/WMS'; -import Message from '../../../components/I18N/Message'; -import {updateLayerLegendFilter} from '../../../utils/FilterUtils'; -import OverlayTrigger from '../../../components/misc/OverlayTrigger'; +import { updateLayerLegendFilter } from '../../../utils/FilterUtils'; +import { normalizeSRS } from '../../../utils/CoordinatesUtils'; +import { getLayerFilterByLegendFormat, getWMSLegendConfig, INTERACTIVE_LEGEND_ID, LEGEND_FORMAT } from '../../../utils/LegendUtils'; class StyleBasedWMSJsonLegend extends React.Component { static propTypes = { layer: PropTypes.object, @@ -36,7 +42,10 @@ class StyleBasedWMSJsonLegend extends React.Component { scaleDependent: PropTypes.bool, language: PropTypes.string, onChange: PropTypes.func, - owner: PropTypes.string + owner: PropTypes.string, + projection: PropTypes.string, + mapSize: PropTypes.object, + mapBbox: PropTypes.object }; static defaultProps = { @@ -60,12 +69,23 @@ class StyleBasedWMSJsonLegend extends React.Component { componentDidUpdate(prevProps) { const prevLayerStyle = prevProps?.layer?.style; const currentLayerStyle = this.props?.layer?.style; - // get the new json legend and rerender it in case change style - if (currentLayerStyle !== prevLayerStyle) { + + const prevFilter = getLayerFilterByLegendFormat(prevProps?.layer, LEGEND_FORMAT.JSON); + const currFilter = getLayerFilterByLegendFormat(this.props?.layer, LEGEND_FORMAT.JSON); + + // get the new json legend and rerender in case of change in style or layer filter + if (currentLayerStyle !== prevLayerStyle + || !isEqual(prevFilter, currFilter) + || !isEqual(prevProps.mapBbox, this.props.mapBbox) + ) { this.getLegendData(); } } + onResetLegendFilter = () => { + const newLayerFilter = updateLayerLegendFilter(this.props?.layer?.layerFilter); + this.props.onChange({ layerFilter: newLayerFilter }); + } getLegendData() { let jsonLegendUrl = this.getUrl(this.props); if (!jsonLegendUrl) { @@ -101,22 +121,20 @@ class StyleBasedWMSJsonLegend extends React.Component { const cleanParams = clearNilValuesForParams(layer.params); const scale = this.getScale(props); - let query = assign({}, { - service: "WMS", - request: "GetLegendGraphic", - format: "application/json", - height: props.legendHeight, - width: props.legendWidth, - layer: layer.name, - style: layer.style || null, - version: layer.version || "1.3.0", - SLD_VERSION: "1.1.0", - LEGEND_OPTIONS: props.legendOptions - }, layer.legendParams || {}, - props.language && layer.localizedLayerStyles ? {LANGUAGE: props.language} : {}, - addAuthenticationToSLD(cleanParams || {}, props.layer), - cleanParams && cleanParams.SLD_BODY ? {SLD_BODY: cleanParams.SLD_BODY} : {}, - scale !== null ? { SCALE: scale } : {}); + const projection = normalizeSRS(props.projection || 'EPSG:3857', layer.allowedSRS); + const query = { + ...getWMSLegendConfig({ + layer, + format: LEGEND_FORMAT.JSON, + ...pick(props, ['legendHeight', 'legendWidth', 'mapSize', 'legendOptions', 'mapBbox']), + projection + }), + ...layer.legendParams, + ...(props.language && layer.localizedLayerStyles ? { LANGUAGE: props.language } : {}), + ...addAuthenticationToSLD(cleanParams || {}, props.layer), + ...(cleanParams && cleanParams.SLD_BODY ? { SLD_BODY: cleanParams.SLD_BODY } : {}), + ...(scale !== null ? { SCALE: scale } : {}) + }; addAuthenticationParameter(url, query); return urlUtil.format({ @@ -129,17 +147,36 @@ class StyleBasedWMSJsonLegend extends React.Component { return ''; } renderRules = (rules) => { - const isLegendFilterIncluded = this.props?.layer?.layerFilter?.filters?.find(f=>f.id === 'interactiveLegend'); + const isLegendFilterIncluded = get(this.props, 'layer.layerFilter.filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); const legendFilters = isLegendFilterIncluded ? isLegendFilterIncluded?.filters : []; - return (rules || []).map((rule) => { - const isFilterExistBefore = legendFilters?.find(f => f.id === rule.filter); - const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; - const activeFilter = rule.filter && isFilterExistBefore; - return (
this.filterWMSLayerHandler(rule.filter)}> - - {rule.name || rule.title || ''} -
); - }); + const isPreviousFilterValid = this.checkPreviousFiltersAreValid(rules, legendFilters); + return ( + <> + {!isPreviousFilterValid ? + + + + + : null} + {(rules || []).map((rule) => { + const activeFilter = legendFilters?.some(f => f.id === rule.filter); + const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; + return ( +
this.filterWMSLayerHandler(rule.filter)}> + + {rule.name || rule.title || ''} +
+ ); + })} + + ); }; render() { if (!this.state.error && this.props.layer && this.props.layer.type === "wms" && this.props.layer.url) { @@ -167,6 +204,10 @@ class StyleBasedWMSJsonLegend extends React.Component { const newLayerFilter = updateLayerLegendFilter(this.props?.layer?.layerFilter, filter); this.props.onChange({ layerFilter: newLayerFilter }); }; + checkPreviousFiltersAreValid = (rules, prevLegendFilters) => { + const rulesFilters = rules.map(rule => rule.filter); + return prevLegendFilters?.every(f => rulesFilters.includes(f.id)); + } } export default StyleBasedWMSJsonLegend; diff --git a/web/client/plugins/TOC/components/TOC.jsx b/web/client/plugins/TOC/components/TOC.jsx index d726c4e7e0..6801d3f446 100644 --- a/web/client/plugins/TOC/components/TOC.jsx +++ b/web/client/plugins/TOC/components/TOC.jsx @@ -192,7 +192,18 @@ function TOC({ onSelectNode={onSelectNode} onSort={handleOnSort} onChange={handleUpdateNode} - config={config} + config={{ + ...config, + layerOptions: { + ...config?.layerOptions, + legendOptions: { + ...config?.layerOptions?.legendOptions, + mapSize: map?.size, + mapBbox: map?.bbox, + projection: map?.projection + } + } + }} nodeItems={nodeItems} nodeToolItems={nodeToolItems} singleDefaultGroup={singleDefaultGroup} diff --git a/web/client/plugins/TOC/components/WMSLegend.jsx b/web/client/plugins/TOC/components/WMSLegend.jsx index 6435f3fad7..cd8db22cea 100644 --- a/web/client/plugins/TOC/components/WMSLegend.jsx +++ b/web/client/plugins/TOC/components/WMSLegend.jsx @@ -9,7 +9,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { isEmpty, isNumber } from 'lodash'; +import pick from 'lodash/pick'; +import isEmpty from 'lodash/isEmpty'; +import isNumber from 'lodash/isNumber'; import StyleBasedWMSJsonLegend from './StyleBasedWMSJsonLegend'; import Legend from './Legend'; import { getMiscSetting } from '../../../utils/ConfigUtils'; @@ -40,7 +42,10 @@ class WMSLegend extends React.Component { language: PropTypes.string, legendWidth: PropTypes.number, legendHeight: PropTypes.number, - onChange: PropTypes.func + onChange: PropTypes.func, + projection: PropTypes.string, + mapSize: PropTypes.object, + mapBbox: PropTypes.object }; static defaultProps = { @@ -63,7 +68,9 @@ class WMSLegend extends React.Component { const containerWidth = this.containerRef.current && this.containerRef.current.clientWidth; this.setState({ containerWidth, ...this.state }); } - + getLegendProps = () => { + return pick(this.props, ['currentZoomLvl', 'scales', 'scaleDependent', 'language', 'projection', 'mapSize', 'mapBbox']); + } render() { let node = this.props.node || {}; const experimentalInteractiveLegend = getMiscSetting('experimentalInteractiveLegend', false); @@ -76,8 +83,6 @@ class WMSLegend extends React.Component { ); @@ -105,8 +109,6 @@ class WMSLegend extends React.Component { ); diff --git a/web/client/plugins/TOC/index.js b/web/client/plugins/TOC/index.js index 7ea8b64786..e0c10ef4c4 100644 --- a/web/client/plugins/TOC/index.js +++ b/web/client/plugins/TOC/index.js @@ -354,6 +354,9 @@ function TOC({ groupOptions = {}, layerOptions = {}, + projection, + mapSize, + mapBbox, currentLocale, language, scales, @@ -499,7 +502,13 @@ function TOC({ groupOptions, layerOptions: { ...layerOptions, - hideLegend: !activateLegendTool + hideLegend: !activateLegendTool, + legendOptions: { + ...layerOptions?.legendOptions, + projection, + mapSize, + mapBbox + } } }} onContextMenu={({ event, node: currentNode, nodeType, parentId }) => { @@ -599,7 +608,10 @@ const tocSelector = createShallowSelectorCreator(isEqual)( map && map.projection || 'EPSG:3857', map && map.mapOptions && map.mapOptions.view && map.mapOptions.view.DPI || null ), + projection: map && map.projection || 'EPSG:3857', zoom: map?.zoom, + mapSize: map?.size, + mapBbox: map?.bbox, resolutions, resolution, visualizationMode, diff --git a/web/client/plugins/tocitemssettings/defaultSettingsTabs.js b/web/client/plugins/tocitemssettings/defaultSettingsTabs.js index a0469544e5..350241406c 100644 --- a/web/client/plugins/tocitemssettings/defaultSettingsTabs.js +++ b/web/client/plugins/tocitemssettings/defaultSettingsTabs.js @@ -37,7 +37,7 @@ import { StyleSelector } from '../styleeditor/index'; const StyleList = defaultProps({ readOnly: true })(StyleSelector); const ConnectedDisplay = connect( - createSelector([mapSelector], ({ zoom, projection }) => ({ zoom, projection })) + createSelector([mapSelector], ({ zoom, projection, size, bbox }) => ({ zoom, projection, mapSize: size, mapBbox: bbox })) )(Display); const ConnectedVectorStyleEditor = connect( diff --git a/web/client/themes/default/less/toc.less b/web/client/themes/default/less/toc.less index 74c2487c92..5ce4de78e1 100644 --- a/web/client/themes/default/less/toc.less +++ b/web/client/themes/default/less/toc.less @@ -403,6 +403,14 @@ margin-right: 8px; } } + .legend-filter-warning { + display: flex; + align-items: center; + margin: 8px 0; + .reset-legend-filter { + width: 45px; + } + } } // legend style diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 770dd84292..57e8411159 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -240,7 +240,11 @@ "clearCustomizationConfirm": "Möchten Sie wirklich alle Anpassungen entfernen?", "error": "Die Felder konnten nicht automatisch geladen werden" }, - "disableFeaturesEditing": "Deaktivieren Sie die Bearbeitung der Attributtabelle" + "disableFeaturesEditing": "Deaktivieren Sie die Bearbeitung der Attributtabelle", + "interactiveLegend": { + "incompatibleFilterWarning": "Angewendete Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel", + "resetLegendFilterTooltip": "Setzen Sie den Legendenfilter auf den Anfangszustand zurück" + } }, "localizedInput": { "localize": "Diesen Text lokalisieren...", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index ecfcea85b3..ae572a18f2 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -240,7 +240,11 @@ "clearCustomizationConfirm": "Are you sure you want to remove all customizations?", "error": "It was not possible to automatically load the fields" }, - "disableFeaturesEditing": "Disable editing on Attribute table" + "disableFeaturesEditing": "Disable editing on Attribute table", + "interactiveLegend": { + "incompatibleFilterWarning": "Applied legend filters are incompatible with the active layer filter", + "resetLegendFilterTooltip": "Reset the legend filter to the initial state" + } }, "localizedInput": { "localize": "Localize this text...", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index ca6ee04960..7e36fcf102 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -240,7 +240,11 @@ "clearCustomizationConfirm": "¿Está seguro de que desea borrar la personalización de los campos?", "error": "Error al recuperar los campos" }, - "disableFeaturesEditing": "Deshabilitar la edición en la tabla de atributos" + "disableFeaturesEditing": "Deshabilitar la edición en la tabla de atributos", + "interactiveLegend": { + "incompatibleFilterWarning": "Los filtros de leyenda aplicados son incompatibles con el filtro de capa activo", + "resetLegendFilterTooltip": "Restablecer el filtro de leyenda al estado inicial" + } }, "localizedInput": { "localize": "Localizar cadena...", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index c6c237b591..7b0aa69b5d 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -240,7 +240,11 @@ "clearCustomizationConfirm": "Voulez-vous vraiment supprimer les personnalisations ?", "error": "Échec de la récupération des champs" }, - "disableFeaturesEditing": "Désactiver la modification sur la table attributaire" + "disableFeaturesEditing": "Désactiver la modification sur la table attributaire", + "interactiveLegend": { + "incompatibleFilterWarning": "Les filtres de légende appliqués sont incompatibles avec le filtre de couche actif", + "resetLegendFilterTooltip": "Réinitialiser le filtre de légende à l'état initial" + } }, "localizedInput": { "localize": "Localiser ce texte...", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 3f8147e03c..67be004473 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -240,8 +240,11 @@ "clearCustomizationConfirm": "Sei sicuro di voler rimuovere tutte le modifiche effettuate ai campi?", "error": "Non è stato possibile recuperare i campi dalla sorgente dati" }, - "disableFeaturesEditing": "Disabilita la modifica sulla tabella degli attributi" - }, + "disableFeaturesEditing": "Disabilita la modifica sulla tabella degli attributi", + "interactiveLegend": { + "incompatibleFilterWarning": "I filtri della legenda applicati non sono compatibili con il filtro del livello attivo", + "resetLegendFilterTooltip": "Reimposta il filtro della legenda allo stato iniziale" + } }, "localizedInput": { "localize": "Traduci questo testo...", "title": "Traduci testo", diff --git a/web/client/utils/FilterUtils.js b/web/client/utils/FilterUtils.js index b34ca4d18a..ce9297b606 100644 --- a/web/client/utils/FilterUtils.js +++ b/web/client/utils/FilterUtils.js @@ -30,6 +30,7 @@ export const cqlToOgc = (cqlFilter, fOpts) => { }; import { get, isNil, isArray, find, findIndex, isString, flatten } from 'lodash'; +import { INTERACTIVE_LEGEND_ID } from './LegendUtils'; let FilterUtils; const wrapValueWithWildcard = (value, condition) => { @@ -1323,12 +1324,12 @@ export const updateLayerLegendFilter = (layerFilterObj, legendFilter) => { } }; let filterObj = {...defaultLayerFilter, ...layerFilterObj}; - const isLegendFilterExist = filterObj?.filters?.find(f => f.id === 'interactiveLegend'); + const isLegendFilterExist = filterObj?.filters?.find(f => f.id === INTERACTIVE_LEGEND_ID); if (!legendFilter) { // clear legend filter with id = 'interactiveLegend' if (isLegendFilterExist) { filterObj = { - ...filterObj, filters: filterObj?.filters?.filter(f => f.id !== 'interactiveLegend') + ...filterObj, filters: filterObj?.filters?.filter(f => f.id !== INTERACTIVE_LEGEND_ID) }; } let newFilter = filterObj ? filterObj : undefined; @@ -1354,9 +1355,9 @@ export const updateLayerLegendFilter = (layerFilterObj, legendFilter) => { } let newFilter = { ...(filterObj || {}), filters: [ - ...(filterObj?.filters?.filter(f => f.id !== 'interactiveLegend') || []), ...[ + ...(filterObj?.filters?.filter(f => f.id !== INTERACTIVE_LEGEND_ID) || []), ...[ { - "id": "interactiveLegend", + "id": INTERACTIVE_LEGEND_ID, "format": "logic", "version": "1.0.0", "logic": "OR", @@ -1379,10 +1380,10 @@ export function resetLayerLegendFilter(layer, reason, value) { let filterObj = layer.layerFilter ? layer.layerFilter : undefined; if (!needReset || !isLayerWithJSONLegend || !filterObj) return false; // reset thte filter if legendCQLFilter is empty - const isLegendFilterExist = filterObj?.filters?.find(f => f.id === 'interactiveLegend'); + const isLegendFilterExist = filterObj?.filters?.find(f => f.id === INTERACTIVE_LEGEND_ID); if (isLegendFilterExist) { filterObj = { - ...filterObj, filters: filterObj?.filters?.filter(f => f.id !== 'interactiveLegend') + ...filterObj, filters: filterObj?.filters?.filter(f => f.id !== INTERACTIVE_LEGEND_ID) }; return filterObj; } @@ -1414,5 +1415,6 @@ FilterUtils = { processOGCSpatialFilter, createFeatureFilter, mergeFiltersToOGC, - convertFiltersToOGC + convertFiltersToOGC, + INTERACTIVE_LEGEND_ID }; diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js new file mode 100644 index 0000000000..53f798f94d --- /dev/null +++ b/web/client/utils/LegendUtils.js @@ -0,0 +1,81 @@ +/* + * Copyright 2024, 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 { isEmpty } from "lodash"; +import { getExtentFromViewport } from "./CoordinatesUtils"; +import { ServerTypes } from "./LayersUtils"; +import { optionsToVendorParams } from "./VendorParamsUtils"; + +export const INTERACTIVE_LEGEND_ID = "interactiveLegend"; +export const LEGEND_FORMAT = { + IMAGE: "image/png", + JSON: "application/json" +}; + +export const getLayerFilterByLegendFormat = (layer, format) => { + const layerFilter = layer?.layerFilter; + if (layer && layer.type === "wms" && layer.url) { + if (format === LEGEND_FORMAT.JSON && !isEmpty(layerFilter)) { + return { + ...layerFilter, + filters: (layerFilter?.filters ?? [])?.filter(f => f.id !== INTERACTIVE_LEGEND_ID) + }; + } + return layerFilter; + } + return layerFilter; +}; + +export const getWMSLegendConfig = ({ + format, + legendHeight, + legendWidth, + layer, + mapSize, + projection, + mapBbox, + legendOptions +}) => { + const baseParams = { + service: "WMS", + request: "GetLegendGraphic", + format, + height: legendHeight, + width: legendWidth, + layer: layer.name, + style: layer.style || null, + version: layer.version || "1.3.0", + SLD_VERSION: "1.1.0", + LEGEND_OPTIONS: legendOptions + }; + + if (layer.serverType !== ServerTypes.NO_VENDOR) { + return { + ...baseParams, + LEGEND_OPTIONS: `hideEmptyRules:${layer.group !== "background"};${legendOptions}`, + SRCWIDTH: mapSize?.width ?? 512, + SRCHEIGHT: mapSize?.height ?? 512, + SRS: projection, + CRS: projection, + ...(mapBbox?.bounds && {BBOX: getExtentFromViewport(mapBbox, projection)?.join(',')}), + ...optionsToVendorParams({ ...layer, layerFilter: getLayerFilterByLegendFormat(layer, format) }) + }; + } + + return { + ...baseParams, + ...layer.params + }; +}; + +export default { + INTERACTIVE_LEGEND_ID, + getLayerFilterByLegendFormat, + getWMSLegendConfig +}; + diff --git a/web/client/utils/PrintUtils.js b/web/client/utils/PrintUtils.js index 9068137b69..4a011618a2 100644 --- a/web/client/utils/PrintUtils.js +++ b/web/client/utils/PrintUtils.js @@ -38,6 +38,7 @@ import trimEnd from 'lodash/trimEnd'; import { getGridGeoJson } from "./grids/MapGridsUtils"; import { isImageServerUrl } from './ArcGISUtils'; +import { getWMSLegendConfig, LEGEND_FORMAT } from './LegendUtils'; const defaultScales = getGoogleMercatorScales(0, 21); let PrintUtils; @@ -606,33 +607,30 @@ export const specCreators = { }) } ))}), - legend: (layer, spec) => ({ - "name": layer.title || layer.name, - "classes": [ - { - "name": "", - "icons": [ - PrintUtils.normalizeUrl(layer.url) + url.format({ - query: addAuthenticationParameter(PrintUtils.normalizeUrl(layer.url), { - TRANSPARENT: true, - EXCEPTIONS: "application/vnd.ogc.se_xml", - VERSION: "1.1.1", - SERVICE: "WMS", - REQUEST: "GetLegendGraphic", - LAYER: layer.name, - STYLE: layer.style || '', - SCALE: spec.scale, - ...getLegendIconsSize(spec, layer), - LEGEND_OPTIONS: "forceLabels:" + (spec.forceLabels ? "on" : "") + ";fontAntialiasing:" + spec.antiAliasing + ";dpi:" + spec.legendDpi + ";fontStyle:" + (spec.bold && "bold" || (spec.italic && "italic") || '') + ";fontName:" + spec.fontFamily + ";fontSize:" + spec.fontSize, - format: "image/png", - ...(spec.language ? {LANGUAGE: spec.language} : {}), - ...layer?.params + legend: (layer, spec) => { + const legendOptions = "forceLabels:" + (spec.forceLabels ? "on" : "") + ";fontAntialiasing:" + spec.antiAliasing + ";dpi:" + spec.legendDpi + ";fontStyle:" + (spec.bold && "bold" || (spec.italic && "italic") || '') + ";fontName:" + spec.fontFamily + ";fontSize:" + spec.fontSize; + return { + "name": layer.title || layer.name, + "classes": [ + { + "name": "", + "icons": [ + PrintUtils.normalizeUrl(layer.url) + url.format({ + query: addAuthenticationParameter(PrintUtils.normalizeUrl(layer.url), { + ...getWMSLegendConfig({layer, legendOptions, mapBbox: spec.bbox, mapSize: spec.size, projection: spec.projection, format: LEGEND_FORMAT.IMAGE}), + TRANSPARENT: true, + EXCEPTIONS: "application/vnd.ogc.se_xml", + VERSION: "1.1.1", + SCALE: spec.scale, + ...getLegendIconsSize(spec, layer), + ...(spec.language ? {LANGUAGE: spec.language} : {}) + }) }) - }) - ] - } - ] - }) + ] + } + ] + }; + } }, vector: { map: (layer, spec) => ({ diff --git a/web/client/utils/__tests__/FilterUtils-test.js b/web/client/utils/__tests__/FilterUtils-test.js index 7787c74b7f..7433c3dae2 100644 --- a/web/client/utils/__tests__/FilterUtils-test.js +++ b/web/client/utils/__tests__/FilterUtils-test.js @@ -32,6 +32,7 @@ import { isFilterEmpty, updateLayerLegendFilter, resetLayerLegendFilter } from '../FilterUtils'; +import { INTERACTIVE_LEGEND_ID } from '../LegendUtils'; describe('FilterUtils', () => { @@ -2337,8 +2338,8 @@ describe('FilterUtils', () => { const updatedFilterObj = updateLayerLegendFilter(layerFilterObj, lgegendFilter); expect(updatedFilterObj).toBeTruthy(); expect(updatedFilterObj.filters.length).toEqual(1); - expect(updatedFilterObj.filters.filter(i => i.id === 'interactiveLegend')?.length).toEqual(1); - expect(updatedFilterObj.filters.find(i => i.id === 'interactiveLegend').filters.length).toEqual(1); + expect(updatedFilterObj.filters.filter(i => i.id === INTERACTIVE_LEGEND_ID)?.length).toEqual(1); + expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID).filters.length).toEqual(1); }); it('test updateLayerLegendFilter for wms, apply multi legend filter', () => { const layerFilterObj = { @@ -2364,7 +2365,7 @@ describe('FilterUtils', () => { }, "filters": [ { - "id": "interactiveLegend", + "id": INTERACTIVE_LEGEND_ID, "format": "logic", "version": "1.0.0", "logic": "OR", @@ -2383,8 +2384,8 @@ describe('FilterUtils', () => { const updatedFilterObj = updateLayerLegendFilter(layerFilterObj, lgegendFilter); expect(updatedFilterObj).toBeTruthy(); expect(updatedFilterObj.filters.length).toEqual(1); - expect(updatedFilterObj.filters.filter(i => i.id === 'interactiveLegend')?.length).toEqual(1); - expect(updatedFilterObj.filters.find(i => i.id === 'interactiveLegend').filters.length).toEqual(2); + expect(updatedFilterObj.filters.filter(i => i.id === INTERACTIVE_LEGEND_ID)?.length).toEqual(1); + expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID).filters.length).toEqual(2); }); it('test reset legend filter using updateLayerLegendFilter', () => { const layerFilterObj = { @@ -2410,7 +2411,7 @@ describe('FilterUtils', () => { }, "filters": [ { - "id": "interactiveLegend", + "id": INTERACTIVE_LEGEND_ID, "format": "logic", "version": "1.0.0", "logic": "OR", @@ -2434,7 +2435,7 @@ describe('FilterUtils', () => { const updatedFilterObj = updateLayerLegendFilter(layerFilterObj); expect(updatedFilterObj).toBeTruthy(); expect(updatedFilterObj.filters.length).toEqual(0); - expect(updatedFilterObj.filters.find(i => i.id === 'interactiveLegend')).toBeFalsy(); + expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID)).toBeFalsy(); }); it('test resetLayerLegendFilter in case change wms style', () => { const layerFilterObj = { @@ -2460,7 +2461,7 @@ describe('FilterUtils', () => { }, "filters": [ { - "id": "interactiveLegend", + "id": INTERACTIVE_LEGEND_ID, "format": "logic", "version": "1.0.0", "logic": "OR", @@ -2489,6 +2490,6 @@ describe('FilterUtils', () => { const updatedFilterObj = resetLayerLegendFilter(layer, 'style', 'style_02'); expect(updatedFilterObj).toBeTruthy(); expect(updatedFilterObj.filters.length).toEqual(0); - expect(updatedFilterObj.filters.find(i => i.id === 'interactiveLegend')).toBeFalsy(); + expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID)).toBeFalsy(); }); }); diff --git a/web/client/utils/__tests__/LegendUtils-test.js b/web/client/utils/__tests__/LegendUtils-test.js new file mode 100644 index 0000000000..7083d6a4d6 --- /dev/null +++ b/web/client/utils/__tests__/LegendUtils-test.js @@ -0,0 +1,164 @@ +/* + * Copyright 2024, 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 expect from 'expect'; +import { + getWMSLegendConfig, + getLayerFilterByLegendFormat, + INTERACTIVE_LEGEND_ID, + LEGEND_FORMAT +} from '../LegendUtils'; +import { ServerTypes } from '../LayersUtils'; + +describe('LegendUtils', () => { + describe('getLayerFilterByLegendFormat', () => { + it('should return layer filter without interactive legend filter for JSON format', () => { + const layer = { + type: 'wms', + url: 'http://example.com', + layerFilter: { + filters: [{ id: INTERACTIVE_LEGEND_ID }, { id: 'otherFilter' }] + } + }; + const format = LEGEND_FORMAT.JSON; + const result = getLayerFilterByLegendFormat(layer, format); + expect(result.filters).toEqual([{ id: 'otherFilter' }]); + }); + + it('should return original layer filter for non-JSON format', () => { + const layer = { + type: 'wms', + url: 'http://example.com', + layerFilter: { + filters: [{ id: INTERACTIVE_LEGEND_ID }, { id: 'otherFilter' }] + } + }; + const format = LEGEND_FORMAT.IMAGE; + const result = getLayerFilterByLegendFormat(layer, format); + expect(result.filters).toEqual([{ id: INTERACTIVE_LEGEND_ID }, { id: 'otherFilter' }]); + }); + + it('should return empty filter if layerFilter is undefined', () => { + const layer = { + type: 'wms', + url: 'http://example.com' + }; + const format = LEGEND_FORMAT.JSON; + const result = getLayerFilterByLegendFormat(layer, format); + expect(result).toBe(undefined); + }); + }); + + describe('getWMSLegendConfig', () => { + it('should return correct WMS legend config for non-vendor server type', () => { + const layer = { + name: 'testLayer', + type: 'wms', + url: 'http://example.com', + serverType: ServerTypes.NO_VENDOR, + params: { customParam: 'value' } + }; + const config = getWMSLegendConfig({ + format: LEGEND_FORMAT.IMAGE, + legendHeight: 20, + legendWidth: 20, + layer, + mapSize: { width: 800, height: 600 }, + projection: 'EPSG:4326', + mapBbox: { bounds: { minx: -30, miny: 20, maxx: 50, maxy: 60 } }, + legendOptions: 'fontSize:10' + }); + expect(config).toEqual({ + service: 'WMS', + request: 'GetLegendGraphic', + format: LEGEND_FORMAT.IMAGE, + height: 20, + width: 20, + layer: 'testLayer', + style: null, + version: '1.3.0', + SLD_VERSION: '1.1.0', + LEGEND_OPTIONS: 'fontSize:10', + customParam: 'value' + }); + }); + + it('should return correct WMS legend config for vendor server type', () => { + const layer = { + name: 'testLayer', + type: 'wms', + url: 'http://example.com', + serverType: 'VENDOR', + group: 'foreground' + }; + const config = getWMSLegendConfig({ + format: LEGEND_FORMAT.IMAGE, + legendHeight: 20, + legendWidth: 20, + layer, + mapSize: { width: 800, height: 600 }, + projection: 'EPSG:4326', + mapBbox: { bounds: {minx: -30, miny: 20, maxx: 50, maxy: 60}, crs: "EPSG:4326" }, + legendOptions: 'fontSize:10' + }); + expect(config).toEqual({ + service: 'WMS', + request: 'GetLegendGraphic', + format: LEGEND_FORMAT.IMAGE, + height: 20, + width: 20, + layer: 'testLayer', + style: null, + version: '1.3.0', + SLD_VERSION: '1.1.0', + LEGEND_OPTIONS: 'hideEmptyRules:true;fontSize:10', + SRCWIDTH: 800, + SRCHEIGHT: 600, + SRS: 'EPSG:4326', + CRS: 'EPSG:4326', + BBOX: '-30,20,50,60' + }); + }); + it('should return correct WMS legend config for vendor server type with background group', () => { + const layer = { + name: 'testLayer', + type: 'wms', + url: 'http://example.com', + serverType: 'VENDOR', + group: 'background' + }; + const config = getWMSLegendConfig({ + format: LEGEND_FORMAT.IMAGE, + legendHeight: 20, + legendWidth: 20, + layer, + mapSize: { width: 800, height: 600 }, + projection: 'EPSG:4326', + mapBbox: { bounds: { minx: -30, miny: 20, maxx: 50, maxy: 60 }, crs: "EPSG:4326" }, + legendOptions: 'fontSize:10' + }); + expect(config).toEqual({ + service: 'WMS', + request: 'GetLegendGraphic', + format: LEGEND_FORMAT.IMAGE, + height: 20, + width: 20, + layer: 'testLayer', + style: null, + version: '1.3.0', + SLD_VERSION: '1.1.0', + LEGEND_OPTIONS: 'hideEmptyRules:false;fontSize:10', + SRCWIDTH: 800, + SRCHEIGHT: 600, + SRS: 'EPSG:4326', + CRS: 'EPSG:4326', + BBOX: '-30,20,50,60' + }); + }); + }); +}); From 2f3ebbcfe6ff6ef2550873b705bc18d8432d44bb Mon Sep 17 00:00:00 2001 From: Suren Date: Fri, 6 Dec 2024 11:17:48 +0530 Subject: [PATCH 2/7] unit test updated --- .../plugins/TOC/components/__tests__/WMSLegend-test.jsx | 8 ++++---- web/client/utils/LegendUtils.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/web/client/plugins/TOC/components/__tests__/WMSLegend-test.jsx b/web/client/plugins/TOC/components/__tests__/WMSLegend-test.jsx index 878a63eb87..a2bc66f5c1 100644 --- a/web/client/plugins/TOC/components/__tests__/WMSLegend-test.jsx +++ b/web/client/plugins/TOC/components/__tests__/WMSLegend-test.jsx @@ -88,7 +88,7 @@ describe('test WMSLegend module component', () => { const params = new URLSearchParams(image[0].src); expect(params.get("width")).toBe('12'); expect(params.get("height")).toBe('12'); - expect(params.get("LEGEND_OPTIONS")).toBe('forceLabels:on'); + expect(params.get("LEGEND_OPTIONS")).toBe('hideEmptyRules:true;forceLabels:on'); }); it('tests WMSLegend component legendOptions with one or all values missing', () => { @@ -112,7 +112,7 @@ describe('test WMSLegend module component', () => { const params = new URLSearchParams(image[0].src); expect(params.get("width")).toBe('12'); expect(params.get("height")).toBe('12'); - expect(params.get("LEGEND_OPTIONS")).toBe('forceLabels:on'); + expect(params.get("LEGEND_OPTIONS")).toBe('hideEmptyRules:true;forceLabels:on'); }); it('tests WMSLegend component legendOptions with values', () => { @@ -136,7 +136,7 @@ describe('test WMSLegend module component', () => { const params = new URLSearchParams(image[0].src); expect(params.get("width")).toBe('20'); expect(params.get("height")).toBe('40'); - expect(params.get("LEGEND_OPTIONS")).toBe('forceLabels:on'); + expect(params.get("LEGEND_OPTIONS")).toBe('hideEmptyRules:true;forceLabels:on'); }); it('tests WMSLegend component legendOptions from cfg', () => { @@ -159,7 +159,7 @@ describe('test WMSLegend module component', () => { const params = new URLSearchParams(image[0].src); expect(params.get("width")).toBe('20'); expect(params.get("height")).toBe('40'); - expect(params.get("LEGEND_OPTIONS")).toBe('forceLabels:on'); + expect(params.get("LEGEND_OPTIONS")).toBe('hideEmptyRules:true;forceLabels:on'); }); it('tests WMSLegend component language property with value', () => { diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js index 53f798f94d..392f820516 100644 --- a/web/client/utils/LegendUtils.js +++ b/web/client/utils/LegendUtils.js @@ -57,6 +57,7 @@ export const getWMSLegendConfig = ({ if (layer.serverType !== ServerTypes.NO_VENDOR) { return { ...baseParams, + // hideEmptyRules is applied for all layers except background layers LEGEND_OPTIONS: `hideEmptyRules:${layer.group !== "background"};${legendOptions}`, SRCWIDTH: mapSize?.width ?? 512, SRCHEIGHT: mapSize?.height ?? 512, From b9f27fcbe8701ee549d35e145e7a0d376eb1cf72 Mon Sep 17 00:00:00 2001 From: Suren Date: Wed, 11 Dec 2024 18:21:14 +0530 Subject: [PATCH 3/7] code refactor --- .../components/widgets/enhancers/legendWidget.js | 1 + .../TOC/components/StyleBasedWMSJsonLegend.jsx | 12 +++++++----- web/client/themes/default/less/toc.less | 9 +++------ web/client/translations/data.de-DE.json | 4 ++-- web/client/translations/data.en-US.json | 4 ++-- web/client/translations/data.es-ES.json | 4 ++-- web/client/translations/data.fr-FR.json | 4 ++-- web/client/translations/data.it-IT.json | 7 ++++--- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/web/client/components/widgets/enhancers/legendWidget.js b/web/client/components/widgets/enhancers/legendWidget.js index 8336438b0f..0fa8b68bde 100644 --- a/web/client/components/widgets/enhancers/legendWidget.js +++ b/web/client/components/widgets/enhancers/legendWidget.js @@ -40,6 +40,7 @@ export default compose( let layersUpdatedWithCql = {}; let cqlFilter = undefined; + // update the layer filters to apply filtering to the legend if (mapSync && !isEmpty(layerInCommon) && (filterObj.featureTypeName ? filterObj.featureTypeName === targetLayerName : true)) { if (dependencies.quickFilters) { filterObjCollection = {...filterObjCollection, ...composeFilterObject(filterObj, dependencies.quickFilters, dependencies.options)}; diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index 89dc75b544..a22a52b846 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -152,14 +152,13 @@ class StyleBasedWMSJsonLegend extends React.Component { const isPreviousFilterValid = this.checkPreviousFiltersAreValid(rules, legendFilters); return ( <> - {!isPreviousFilterValid ? + {!isPreviousFilterValid ? - + : null} {(rules || []).map((rule) => { @@ -182,7 +181,10 @@ class StyleBasedWMSJsonLegend extends React.Component { if (!this.state.error && this.props.layer && this.props.layer.type === "wms" && this.props.layer.url) { return <>
- { this.state.loading ? : this.renderRules(this.state.jsonLegend?.rules || [])} + { this.state.loading && !this.state?.jsonLegend?.rules?.length + ? + : this.renderRules(this.state.jsonLegend?.rules || []) + }
; } diff --git a/web/client/themes/default/less/toc.less b/web/client/themes/default/less/toc.less index 5ce4de78e1..191d1fd1af 100644 --- a/web/client/themes/default/less/toc.less +++ b/web/client/themes/default/less/toc.less @@ -403,13 +403,10 @@ margin-right: 8px; } } - .legend-filter-warning { + .reset-legend-filter { display: flex; - align-items: center; - margin: 8px 0; - .reset-legend-filter { - width: 45px; - } + font-size: @font-size-small; + margin-top: 2px; } } diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 57e8411159..1ce244e627 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -242,8 +242,8 @@ }, "disableFeaturesEditing": "Deaktivieren Sie die Bearbeitung der Attributtabelle", "interactiveLegend": { - "incompatibleFilterWarning": "Angewendete Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel", - "resetLegendFilterTooltip": "Setzen Sie den Legendenfilter auf den Anfangszustand zurück" + "incompatibleFilterWarning": "Angewendete Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel. Klicken Sie auf Zurücksetzen, um die Legendenfilter zu entfernen", + "resetLegendFilter": "Zurücksetzen" } }, "localizedInput": { diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index ae572a18f2..2baeefa050 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -242,8 +242,8 @@ }, "disableFeaturesEditing": "Disable editing on Attribute table", "interactiveLegend": { - "incompatibleFilterWarning": "Applied legend filters are incompatible with the active layer filter", - "resetLegendFilterTooltip": "Reset the legend filter to the initial state" + "incompatibleFilterWarning": "Applied legend filters are incompatible with the active layer filter. Click on reset to remove legend filters", + "resetLegendFilter": "Reset" } }, "localizedInput": { diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 7e36fcf102..6c37273a28 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -242,8 +242,8 @@ }, "disableFeaturesEditing": "Deshabilitar la edición en la tabla de atributos", "interactiveLegend": { - "incompatibleFilterWarning": "Los filtros de leyenda aplicados son incompatibles con el filtro de capa activo", - "resetLegendFilterTooltip": "Restablecer el filtro de leyenda al estado inicial" + "incompatibleFilterWarning": "Los filtros de leyenda aplicados son incompatibles con el filtro de capa activo. Haga clic en restablecer para eliminar los filtros de leyenda", + "resetLegendFilter": "Restablecer" } }, "localizedInput": { diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 7b0aa69b5d..64947572f9 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -242,8 +242,8 @@ }, "disableFeaturesEditing": "Désactiver la modification sur la table attributaire", "interactiveLegend": { - "incompatibleFilterWarning": "Les filtres de légende appliqués sont incompatibles avec le filtre de couche actif", - "resetLegendFilterTooltip": "Réinitialiser le filtre de légende à l'état initial" + "incompatibleFilterWarning": "Les filtres de légende appliqués sont incompatibles avec le filtre de couche actif. Cliquez sur réinitialiser pour supprimer les filtres de légende", + "resetLegendFilter": "Réinitialiser" } }, "localizedInput": { diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 67be004473..57709d4601 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -242,9 +242,10 @@ }, "disableFeaturesEditing": "Disabilita la modifica sulla tabella degli attributi", "interactiveLegend": { - "incompatibleFilterWarning": "I filtri della legenda applicati non sono compatibili con il filtro del livello attivo", - "resetLegendFilterTooltip": "Reimposta il filtro della legenda allo stato iniziale" - } }, + "incompatibleFilterWarning": "I filtri della legenda applicati sono incompatibili con il filtro del livello attivo. Clicca su reset per rimuovere i filtri della legenda", + "resetLegendFilter": "Reset" + } + }, "localizedInput": { "localize": "Traduci questo testo...", "title": "Traduci testo", From e3a7348249fd1b8c9858c9026a64473cf56d59f3 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 12 Dec 2024 14:43:49 +0530 Subject: [PATCH 4/7] Code refactor and add unit test for utils --- .../widgets/enhancers/legendWidget.js | 34 +---- .../components/StyleBasedWMSJsonLegend.jsx | 8 +- web/client/utils/LegendUtils.js | 43 ++++++- .../utils/__tests__/LegendUtils-test.js | 118 +++++++++++++++++- 4 files changed, 168 insertions(+), 35 deletions(-) diff --git a/web/client/components/widgets/enhancers/legendWidget.js b/web/client/components/widgets/enhancers/legendWidget.js index 0fa8b68bde..dc28131d24 100644 --- a/web/client/components/widgets/enhancers/legendWidget.js +++ b/web/client/components/widgets/enhancers/legendWidget.js @@ -7,23 +7,20 @@ */ import {compose, withHandlers, withProps} from 'recompose'; -import { castArray, get, isEmpty, find } from 'lodash'; +import { castArray, get } from 'lodash'; import deleteWidget from './deleteWidget'; import { editableWidget, defaultIcons, withHeaderTools } from './tools'; import { getScales } from '../../../utils/MapUtils'; import { WIDGETS_MAPS_REGEX } from "../../../actions/widgets"; import { getInactiveNode, DEFAULT_GROUP_ID } from '../../../utils/LayersUtils'; -import { composeFilterObject } from './utils'; -import { toCQLFilter } from '../../../utils/FilterUtils'; -import { arrayUpdate } from '../../../utils/ImmutableUtils'; -import { optionsToVendorParams } from '../../../utils/VendorParamsUtils'; +import { updateLayerWithLegendFilters } from '../../../utils/LegendUtils'; /** * map dependencies to layers, scales and current zoom level to show legend items for current zoom. * Add also base tools and menu to the widget */ export default compose( - withProps(({ dependencies = {}, dependenciesMap = {}, mapSync }) => { + withProps(({ dependencies = {}, dependenciesMap = {} }) => { const allLayers = dependencies[dependenciesMap.layers] || dependencies.layers || []; const groups = castArray(dependencies[dependenciesMap.groups] || dependencies.groups || []); let layers = allLayers @@ -33,30 +30,7 @@ export default compose( layer.group !== 'background' && !getInactiveNode(layer?.group || DEFAULT_GROUP_ID, groups) ) .map(({ group, ...layer }) => layer); - const targetLayerName = dependencies && dependencies.layer && dependencies.layer.name; - const filterObj = dependencies.filter || {}; - const layerInCommon = find(layers, {name: targetLayerName}) || {}; - let filterObjCollection = {}; - let layersUpdatedWithCql = {}; - let cqlFilter = undefined; - - // update the layer filters to apply filtering to the legend - if (mapSync && !isEmpty(layerInCommon) && (filterObj.featureTypeName ? filterObj.featureTypeName === targetLayerName : true)) { - if (dependencies.quickFilters) { - filterObjCollection = {...filterObjCollection, ...composeFilterObject(filterObj, dependencies.quickFilters, dependencies.options)}; - } - cqlFilter = toCQLFilter(filterObjCollection); - if (!isEmpty(filterObjCollection) && cqlFilter) { - layersUpdatedWithCql = arrayUpdate(false, - {...layerInCommon, params: optionsToVendorParams({ params: {CQL_FILTER: cqlFilter}})}, - {name: targetLayerName}, - layers - ); - } - } else { - layersUpdatedWithCql = layers.map(l => ({...l, params: {...l.params, CQL_FILTER: undefined}})); - } - layers = layersUpdatedWithCql; + layers = updateLayerWithLegendFilters(layers, dependencies); return { allLayers, map: { diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index a22a52b846..774d3dedf4 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -14,6 +14,7 @@ import isNil from 'lodash/isNil'; import pick from 'lodash/pick'; import get from 'lodash/get'; import isEqual from 'lodash/isEqual'; +import isEmpty from 'lodash/isEmpty'; import { Alert, Tooltip, Glyphicon } from 'react-bootstrap'; import { ButtonWithTooltip } from '../../../components/misc/Button'; @@ -153,10 +154,11 @@ class StyleBasedWMSJsonLegend extends React.Component { return ( <> {!isPreviousFilterValid ? - +
@@ -181,7 +183,7 @@ class StyleBasedWMSJsonLegend extends React.Component { if (!this.state.error && this.props.layer && this.props.layer.type === "wms" && this.props.layer.url) { return <>
- { this.state.loading && !this.state?.jsonLegend?.rules?.length + { this.state.loading && isEmpty(this.state?.jsonLegend?.rules) ? : this.renderRules(this.state.jsonLegend?.rules || []) } diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js index 392f820516..b7246f2e52 100644 --- a/web/client/utils/LegendUtils.js +++ b/web/client/utils/LegendUtils.js @@ -10,6 +10,9 @@ import { isEmpty } from "lodash"; import { getExtentFromViewport } from "./CoordinatesUtils"; import { ServerTypes } from "./LayersUtils"; import { optionsToVendorParams } from "./VendorParamsUtils"; +import { composeFilterObject } from "../components/widgets/enhancers/utils"; +import { toCQLFilter } from "./FilterUtils"; +import { arrayUpdate } from "./ImmutableUtils"; export const INTERACTIVE_LEGEND_ID = "interactiveLegend"; export const LEGEND_FORMAT = { @@ -74,9 +77,47 @@ export const getWMSLegendConfig = ({ }; }; +/** + * Updates the layers with the filters from dependencies + * to perform legend filtering in the legend widget + */ +export const updateLayerWithLegendFilters = (layers, dependencies) => { + const targetLayerName = dependencies?.layer?.name; + const filterObj = dependencies?.filter || {}; + const layerInCommon = layers?.find(l => l.name === targetLayerName) || {}; + let filterObjCollection = {}; + let layersUpdatedWithCql = {}; + let cqlFilter = undefined; + + if (dependencies?.mapSync && !isEmpty(layerInCommon) + && (filterObj.featureTypeName ? filterObj.featureTypeName === targetLayerName : true)) { + if (dependencies?.quickFilters) { + filterObjCollection = { + ...filterObjCollection, + ...composeFilterObject(filterObj, dependencies?.quickFilters, dependencies?.options) + }; + } + cqlFilter = toCQLFilter(filterObjCollection); + if (!isEmpty(filterObjCollection) && cqlFilter) { + layersUpdatedWithCql = arrayUpdate(false, + { + ...layerInCommon, + params: optionsToVendorParams({ params: {CQL_FILTER: cqlFilter}}) + }, + {name: targetLayerName}, + layers + ); + } + } else { + layersUpdatedWithCql = layers.map(l => ({...l, params: {...l.params, CQL_FILTER: undefined}})); + } + return layersUpdatedWithCql; +}; + export default { INTERACTIVE_LEGEND_ID, getLayerFilterByLegendFormat, - getWMSLegendConfig + getWMSLegendConfig, + updateLayerWithLegendFilters }; diff --git a/web/client/utils/__tests__/LegendUtils-test.js b/web/client/utils/__tests__/LegendUtils-test.js index 7083d6a4d6..0813b9176e 100644 --- a/web/client/utils/__tests__/LegendUtils-test.js +++ b/web/client/utils/__tests__/LegendUtils-test.js @@ -11,7 +11,8 @@ import { getWMSLegendConfig, getLayerFilterByLegendFormat, INTERACTIVE_LEGEND_ID, - LEGEND_FORMAT + LEGEND_FORMAT, + updateLayerWithLegendFilters } from '../LegendUtils'; import { ServerTypes } from '../LayersUtils'; @@ -161,4 +162,119 @@ describe('LegendUtils', () => { }); }); }); + describe('updateLayerWithLegendFilters', () => { + const filter = { + "featureTypeName": "layer1", + "groupFields": [{"id": 1, "logic": "OR", "index": 0}], + "filterFields": [], + "spatialField": { + "method": "BBOX", + "attribute": "the_geom", + "operation": "INTERSECTS", + "geometry": { + "id": "2", + "type": "Polygon", + "extent": [-12039795.482942028, 4384116.951814341, -9045909.959068244, 6702910.641873448], + "center": [-10542852.721005136, 5543513.796843895], + "coordinates": [[[-12039795.482942028, 6702910.641873448], [-12039795.482942028, 4384116.951814341], [-9045909.959068244, 4384116.951814341], [-9045909.959068244, 6702910.641873448], [-12039795.482942028, 6702910.641873448]]], + "style": {}, + "projection": "EPSG:3857" + } + }, + "pagination": null, + "filterType": "OGC", + "ogcVersion": "1.1.0", + "sortOptions": null, + "crossLayerFilter": null, + "hits": false, + "filters": [] + }; + const quickFilters = { + "STATE_NAME": {"rawValue": "mi", "value": "mi", "operator": "ilike", "type": "string", "attribute": "STATE_NAME"} + }; + it('should return layers with updated CQL_FILTER when mapSync is true and filter matches', () => { + const layers = [ + { name: 'layer1', params: {} }, + { name: 'layer2', params: {} } + ]; + const dependencies = { + layer: { name: 'layer1' }, + filter, + mapSync: true, + quickFilters: {}, + options: {} + }; + + const result = updateLayerWithLegendFilters(layers, dependencies); + + expect(result).toBeTruthy(); + expect(result.length).toBe(2); + const layer = {"name": "layer1", "params": {"CQL_FILTER": "(INTERSECTS(\"the_geom\",SRID=3857;Polygon((-12039795.482942028 6702910.641873448, -12039795.482942028 4384116.951814341, -9045909.959068244 4384116.951814341, -9045909.959068244 6702910.641873448, -12039795.482942028 6702910.641873448))))"}}; + expect(result[0]).toEqual(layer); + expect(result[1].params).toEqual({}); + }); + + it('should return layers with undefined CQL_FILTER when mapSync is false', () => { + const layers = [ + { name: 'layer1', params: { CQL_FILTER: 'some_filter' } }, + { name: 'layer2', params: { CQL_FILTER: 'some_filter' } } + ]; + const dependencies = { + layer: { name: 'layer1' }, + filter, + mapSync: false, + quickFilters, + options: {} + }; + + const result = updateLayerWithLegendFilters(layers, dependencies); + expect(result).toBeTruthy(); + expect(result).toEqual([ + { name: 'layer1', params: {CQL_FILTER: undefined} }, + { name: 'layer2', params: {CQL_FILTER: undefined} } + ]); + }); + + it('should return layers with undefined CQL_FILTER when no matching layer is found', () => { + const layers = [ + { name: 'layer1', params: { CQL_FILTER: 'some_filter' } }, + { name: 'layer2', params: { CQL_FILTER: 'some_filter' } } + ]; + const dependencies = { + layer: { name: 'layer3' }, + filter: { featureTypeName: 'layer3' }, + mapSync: true, + quickFilters: {}, + options: {} + }; + + const result = updateLayerWithLegendFilters(layers, dependencies); + expect(result).toBeTruthy(); + expect(result).toEqual([ + { name: 'layer1', params: {CQL_FILTER: undefined} }, + { name: 'layer2', params: {CQL_FILTER: undefined} } + ]); + }); + + it('should return layers with updated CQL_FILTER when quickFilters are provided', () => { + const layers = [ + { name: 'layer1', params: {} }, + { name: 'layer2', params: {} } + ]; + const dependencies = { + layer: { name: 'layer1' }, + filter, + mapSync: true, + quickFilters, + options: {propertyName: ['STATE_NAME']} + }; + + const result = updateLayerWithLegendFilters(layers, dependencies); + expect(result).toBeTruthy(); + expect(result.length).toBe(2); + const CQL_FILTER = "((strToLowerCase(\"STATE_NAME\") LIKE '%mi%')) AND (INTERSECTS(\"the_geom\",SRID=3857;Polygon((-12039795.482942028 6702910.641873448, -12039795.482942028 4384116.951814341, -9045909.959068244 4384116.951814341, -9045909.959068244 6702910.641873448, -12039795.482942028 6702910.641873448))))"; + expect(result[0].name).toBe("layer1"); + expect(result[0].params.CQL_FILTER).toBe(CQL_FILTER); + }); + }); }); From e0cbee59db2ba079a47db66dacc41b1671a44ffb Mon Sep 17 00:00:00 2001 From: Suren Date: Mon, 16 Dec 2024 13:06:10 +0530 Subject: [PATCH 5/7] #10236: Fix - Legend filter persisting issue when editing style --- web/client/api/WMS.js | 20 ++- .../components/StyleBasedWMSJsonLegend.jsx | 45 +++--- .../StyleBasedWMSJsonLegend-test.jsx | 131 ++++++++++++++---- web/client/translations/data.de-DE.json | 3 +- web/client/translations/data.en-US.json | 3 +- web/client/translations/data.es-ES.json | 3 +- web/client/translations/data.fr-FR.json | 3 +- web/client/translations/data.it-IT.json | 4 +- web/client/utils/LegendUtils.js | 2 +- 9 files changed, 155 insertions(+), 59 deletions(-) diff --git a/web/client/api/WMS.js b/web/client/api/WMS.js index 6f680ff988..94f3e05aac 100644 --- a/web/client/api/WMS.js +++ b/web/client/api/WMS.js @@ -10,7 +10,7 @@ import urlUtil from 'url'; import { isArray, castArray, get } from 'lodash'; import xml2js from 'xml2js'; import axios from '../libs/ajax'; -import { getConfigProp } from '../utils/ConfigUtils'; +import ConfigUtils, { getConfigProp } from '../utils/ConfigUtils'; import { getWMSBoundingBox } from '../utils/CoordinatesUtils'; import { isValidGetMapFormat, isValidGetFeatureInfoFormat } from '../utils/WMSUtils'; const capabilitiesCache = {}; @@ -323,15 +323,25 @@ export const getSupportedFormat = (url, includeGFIFormats = false) => { let layerLegendJsonData = {}; export const getJsonWMSLegend = (url) => { - const request = layerLegendJsonData[url] - ? () => Promise.resolve(layerLegendJsonData[url]) - : () => axios.get(url).then((response) => { + let request; + + // enables caching of the JSON legend for a specified duration, + // while providing the possibility of re-fetching the legend data in case of external modifications + const cached = layerLegendJsonData[url]; + if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) { + request = () => Promise.resolve(cached.data); + } else { + request = () => axios.get(url).then((response) => { if (typeof response?.data === 'string' && response.data.includes("Exception")) { throw new Error("Faild to get json legend"); } - layerLegendJsonData[url] = response?.data?.Legend; + layerLegendJsonData[url] = { + timestamp: new Date().getTime(), + data: response?.data?.Legend + }; return response?.data?.Legend || []; }); + } return request().then((data) => data).catch(err => { throw err; }); diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index 774d3dedf4..2e5e62c338 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -71,11 +71,11 @@ class StyleBasedWMSJsonLegend extends React.Component { const prevLayerStyle = prevProps?.layer?.style; const currentLayerStyle = this.props?.layer?.style; - const prevFilter = getLayerFilterByLegendFormat(prevProps?.layer, LEGEND_FORMAT.JSON); - const currFilter = getLayerFilterByLegendFormat(this.props?.layer, LEGEND_FORMAT.JSON); + const [prevFilter, currFilter] = [prevProps?.layer, this.props?.layer] + .map(_layer => getLayerFilterByLegendFormat(_layer, LEGEND_FORMAT.JSON)); // get the new json legend and rerender in case of change in style or layer filter - if (currentLayerStyle !== prevLayerStyle + if (!isEqual(prevLayerStyle, currentLayerStyle) || !isEqual(prevFilter, currFilter) || !isEqual(prevProps.mapBbox, this.props.mapBbox) ) { @@ -87,6 +87,7 @@ class StyleBasedWMSJsonLegend extends React.Component { const newLayerFilter = updateLayerLegendFilter(this.props?.layer?.layerFilter); this.props.onChange({ layerFilter: newLayerFilter }); } + getLegendData() { let jsonLegendUrl = this.getUrl(this.props); if (!jsonLegendUrl) { @@ -109,6 +110,7 @@ class StyleBasedWMSJsonLegend extends React.Component { } return null; }; + getUrl = (props, urlIdx) => { if (props.layer && props.layer.type === "wms" && props.layer.url) { const layer = props.layer; @@ -147,9 +149,10 @@ class StyleBasedWMSJsonLegend extends React.Component { } return ''; } + renderRules = (rules) => { - const isLegendFilterIncluded = get(this.props, 'layer.layerFilter.filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); - const legendFilters = isLegendFilterIncluded ? isLegendFilterIncluded?.filters : []; + const interactiveLegendFilters = get(this.props, 'layer.layerFilter.filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); + const legendFilters = get(interactiveLegendFilters, 'filters', []); const isPreviousFilterValid = this.checkPreviousFiltersAreValid(rules, legendFilters); return ( <> @@ -163,22 +166,26 @@ class StyleBasedWMSJsonLegend extends React.Component { : null} - {(rules || []).map((rule) => { - const activeFilter = legendFilters?.some(f => f.id === rule.filter); - const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; - return ( -
this.filterWMSLayerHandler(rule.filter)}> - - {rule.name || rule.title || ''} -
- ); - })} + {isEmpty(rules) + ? + : rules.map((rule, idx) => { + const activeFilter = legendFilters?.some(f => f.id === rule.filter); + const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; + return ( +
this.filterWMSLayerHandler(rule.filter)}> + + {rule.name || rule.title || ''} +
+ ); + }) + } ); }; + render() { if (!this.state.error && this.props.layer && this.props.layer.type === "wms" && this.props.layer.url) { return <> @@ -202,12 +209,14 @@ class StyleBasedWMSJsonLegend extends React.Component {
); } + filterWMSLayerHandler = (filter) => { const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; if (!filter || isFilterDisabled) return; const newLayerFilter = updateLayerLegendFilter(this.props?.layer?.layerFilter, filter); this.props.onChange({ layerFilter: newLayerFilter }); }; + checkPreviousFiltersAreValid = (rules, prevLegendFilters) => { const rulesFilters = rules.map(rule => rule.filter); return prevLegendFilters?.every(f => rulesFilters.includes(f.id)); diff --git a/web/client/plugins/TOC/components/__tests__/StyleBasedWMSJsonLegend-test.jsx b/web/client/plugins/TOC/components/__tests__/StyleBasedWMSJsonLegend-test.jsx index c648c63694..1c3b91ce8e 100644 --- a/web/client/plugins/TOC/components/__tests__/StyleBasedWMSJsonLegend-test.jsx +++ b/web/client/plugins/TOC/components/__tests__/StyleBasedWMSJsonLegend-test.jsx @@ -14,8 +14,39 @@ import axios from '../../../../libs/ajax'; import StyleBasedWMSJsonLegend from '../StyleBasedWMSJsonLegend'; import expect from 'expect'; import TestUtils from 'react-dom/test-utils'; +import { INTERACTIVE_LEGEND_ID } from '../../../../utils/LegendUtils'; let mockAxios; +const rules = [ + { + "name": ">= 159.05 and < 5062.5", + "filter": "[field >= '159.05' AND field < '5062.5']", + "symbolizers": [{"Polygon": { + "uom": "in/72", + "stroke": "#ffffff", + "stroke-width": "1.0", + "stroke-opacity": "0.35", + "stroke-linecap": "butt", + "stroke-linejoin": "miter", + "fill": "#8DD3C7", + "fill-opacity": "0.75" + }}] + }, + { + "name": ">= 5062.5 and < 20300.35", + "filter": "[field >= '5062.5' AND field < '20300.35']", + "symbolizers": [{"Polygon": { + "uom": "in/72", + "stroke": "#ffffff", + "stroke-width": "1.0", + "stroke-opacity": "0.35", + "stroke-linecap": "butt", + "stroke-linejoin": "miter", + "fill": "#ABD9C5", + "fill-opacity": "0.75" + }}] + } +]; describe('test StyleBasedWMSJsonLegend module component', () => { beforeEach((done) => { @@ -45,35 +76,7 @@ describe('test StyleBasedWMSJsonLegend module component', () => { "Legend": [{ "layerName": "layer00", "title": "Layer", - "rules": [ - { - "name": ">= 159.05 and < 5062.5", - "filter": "[field >= '159.05' AND field < '5062.5']", - "symbolizers": [{"Polygon": { - "uom": "in/72", - "stroke": "#ffffff", - "stroke-width": "1.0", - "stroke-opacity": "0.35", - "stroke-linecap": "butt", - "stroke-linejoin": "miter", - "fill": "#8DD3C7", - "fill-opacity": "0.75" - }}] - }, - { - "name": ">= 5062.5 and < 20300.35", - "filter": "[field >= '5062.5' AND field < '20300.35']", - "symbolizers": [{"Polygon": { - "uom": "in/72", - "stroke": "#ffffff", - "stroke-width": "1.0", - "stroke-opacity": "0.35", - "stroke-linecap": "butt", - "stroke-linejoin": "miter", - "fill": "#ABD9C5", - "fill-opacity": "0.75" - }}] - }] + rules }] }]; }); @@ -89,4 +92,74 @@ describe('test StyleBasedWMSJsonLegend module component', () => { expect(legendRuleElem).toBeTruthy(); expect(legendRuleElem.length).toEqual(2); }); + it('tests legend with empty rules', async() => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wms', + url: 'http://localhost:8080/geoserver1/wms' + }; + mockAxios.onGet(/geoserver1/).reply(() => { + return [200, { + "Legend": [{ + "layerName": "layer01", + "title": "Layer1", + "rules": [] + }] + }]; + }); + const comp = ReactDOM.render(, document.getElementById("container")); + await TestUtils.act(async() => comp); + + const domNode = ReactDOM.findDOMNode(comp); + expect(domNode).toBeTruthy(); + + const legendElem = document.querySelector('.wms-legend'); + expect(legendElem).toBeTruthy(); + expect(legendElem.innerText).toBe('layerProperties.interactiveLegend.noLegendData'); + const legendRuleElem = domNode.querySelectorAll('.wms-json-legend-rule'); + expect(legendRuleElem.length).toBe(0); + }); + it('tests legend with incompatible filter rules', async() => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wms', + url: 'http://localhost:8080/geoserver2/wms', + layerFilter: { + filters: [{ + id: INTERACTIVE_LEGEND_ID, + filters: [{ + id: 'filter1' + }] + }] + } + }; + mockAxios.onGet(/geoserver2/).reply(() => { + return [200, { + "Legend": [{ + "layerName": "layer01", + "title": "Layer1", + rules + }] + }]; + }); + const comp = ReactDOM.render(, document.getElementById("container")); + await TestUtils.act(async() => comp); + + const domNode = ReactDOM.findDOMNode(comp); + expect(domNode).toBeTruthy(); + + const legendElem = document.querySelector('.wms-legend'); + expect(legendElem).toBeTruthy(); + const legendRuleElem = domNode.querySelector('.wms-legend .alert-warning'); + expect(legendRuleElem).toBeTruthy(); + expect(legendRuleElem.innerText).toContain('layerProperties.interactiveLegend.incompatibleFilterWarning'); + const resetLegendFilter = domNode.querySelector('.wms-legend .alert-warning button'); + expect(resetLegendFilter).toBeTruthy(); + }); }); diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 1ce244e627..a10ad99a65 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -243,7 +243,8 @@ "disableFeaturesEditing": "Deaktivieren Sie die Bearbeitung der Attributtabelle", "interactiveLegend": { "incompatibleFilterWarning": "Angewendete Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel. Klicken Sie auf Zurücksetzen, um die Legendenfilter zu entfernen", - "resetLegendFilter": "Zurücksetzen" + "resetLegendFilter": "Zurücksetzen", + "noLegendData": "Keine Legenden Elemente zum Anzeigen" } }, "localizedInput": { diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 2baeefa050..86c5aa1f2a 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -243,7 +243,8 @@ "disableFeaturesEditing": "Disable editing on Attribute table", "interactiveLegend": { "incompatibleFilterWarning": "Applied legend filters are incompatible with the active layer filter. Click on reset to remove legend filters", - "resetLegendFilter": "Reset" + "resetLegendFilter": "Reset", + "noLegendData": "No legend items to show" } }, "localizedInput": { diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 6c37273a28..d7d4fb5825 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -243,7 +243,8 @@ "disableFeaturesEditing": "Deshabilitar la edición en la tabla de atributos", "interactiveLegend": { "incompatibleFilterWarning": "Los filtros de leyenda aplicados son incompatibles con el filtro de capa activo. Haga clic en restablecer para eliminar los filtros de leyenda", - "resetLegendFilter": "Restablecer" + "resetLegendFilter": "Restablecer", + "noLegendData": "No hay elementos de leyenda para mostrar" } }, "localizedInput": { diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 64947572f9..0dd0188023 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -243,7 +243,8 @@ "disableFeaturesEditing": "Désactiver la modification sur la table attributaire", "interactiveLegend": { "incompatibleFilterWarning": "Les filtres de légende appliqués sont incompatibles avec le filtre de couche actif. Cliquez sur réinitialiser pour supprimer les filtres de légende", - "resetLegendFilter": "Réinitialiser" + "resetLegendFilter": "Réinitialiser", + "noLegendData": "Aucun élément de légende à afficher" } }, "localizedInput": { diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 57709d4601..81ab902518 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -243,8 +243,8 @@ "disableFeaturesEditing": "Disabilita la modifica sulla tabella degli attributi", "interactiveLegend": { "incompatibleFilterWarning": "I filtri della legenda applicati sono incompatibili con il filtro del livello attivo. Clicca su reset per rimuovere i filtri della legenda", - "resetLegendFilter": "Reset" - } + "resetLegendFilter": "Reset", + "noLegendData": "Nessun elemento della legenda da mostrare" } }, "localizedInput": { "localize": "Traduci questo testo...", diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js index b7246f2e52..18405b938e 100644 --- a/web/client/utils/LegendUtils.js +++ b/web/client/utils/LegendUtils.js @@ -20,7 +20,7 @@ export const LEGEND_FORMAT = { JSON: "application/json" }; -export const getLayerFilterByLegendFormat = (layer, format) => { +export const getLayerFilterByLegendFormat = (layer, format = LEGEND_FORMAT.JSON) => { const layerFilter = layer?.layerFilter; if (layer && layer.type === "wms" && layer.url) { if (format === LEGEND_FORMAT.JSON && !isEmpty(layerFilter)) { From b487aa97d46ab9a4b261ce3229b67859d5535e56 Mon Sep 17 00:00:00 2001 From: Suren Date: Wed, 18 Dec 2024 12:16:19 +0530 Subject: [PATCH 6/7] Update filter show warning case --- .../components/StyleBasedWMSJsonLegend.jsx | 7 ++-- .../StyleBasedWMSJsonLegend-test.jsx | 41 ++++++++++++++++++- web/client/themes/default/less/toc.less | 5 --- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index 2e5e62c338..0bac5e6f36 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -151,12 +151,13 @@ class StyleBasedWMSJsonLegend extends React.Component { } renderRules = (rules) => { - const interactiveLegendFilters = get(this.props, 'layer.layerFilter.filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); + const layerFilter = get(this.props, 'layer.layerFilter', {}); + const interactiveLegendFilters = get(layerFilter, 'filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); const legendFilters = get(interactiveLegendFilters, 'filters', []); - const isPreviousFilterValid = this.checkPreviousFiltersAreValid(rules, legendFilters); + const showResetWarning = !this.checkPreviousFiltersAreValid(rules, legendFilters) && !layerFilter.disabled; return ( <> - {!isPreviousFilterValid ? + {showResetWarning ?
{ filters: [{ id: 'filter1' }] - }] + }], + disabled: false } }; mockAxios.onGet(/geoserver2/).reply(() => { @@ -162,4 +163,42 @@ describe('test StyleBasedWMSJsonLegend module component', () => { const resetLegendFilter = domNode.querySelector('.wms-legend .alert-warning button'); expect(resetLegendFilter).toBeTruthy(); }); + it('tests hide warning when layer filter is disabled', async() => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wms', + url: 'http://localhost:8080/geoserver3/wms', + layerFilter: { + filters: [{ + id: INTERACTIVE_LEGEND_ID, + filters: [{ + id: 'filter1' + }] + }], + disabled: true + } + }; + mockAxios.onGet(/geoserver3/).reply(() => { + return [200, { + "Legend": [{ + "layerName": "layer01", + "title": "Layer1", + rules + }] + }]; + }); + const comp = ReactDOM.render(, document.getElementById("container")); + await TestUtils.act(async() => comp); + + const domNode = ReactDOM.findDOMNode(comp); + expect(domNode).toBeTruthy(); + + const legendElem = document.querySelector('.wms-legend'); + expect(legendElem).toBeTruthy(); + const legendRuleElem = domNode.querySelector('.wms-legend .alert-warning'); + expect(legendRuleElem).toBeFalsy(); + }); }); diff --git a/web/client/themes/default/less/toc.less b/web/client/themes/default/less/toc.less index 191d1fd1af..74c2487c92 100644 --- a/web/client/themes/default/less/toc.less +++ b/web/client/themes/default/less/toc.less @@ -403,11 +403,6 @@ margin-right: 8px; } } - .reset-legend-filter { - display: flex; - font-size: @font-size-small; - margin-top: 2px; - } } // legend style From 3bc8b0d138aec65a42fca3494c2e48861805c8a6 Mon Sep 17 00:00:00 2001 From: Suren Date: Wed, 18 Dec 2024 14:18:39 +0530 Subject: [PATCH 7/7] Translation updated --- 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 | 3 +-- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index a10ad99a65..df6b185b0e 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -242,7 +242,7 @@ }, "disableFeaturesEditing": "Deaktivieren Sie die Bearbeitung der Attributtabelle", "interactiveLegend": { - "incompatibleFilterWarning": "Angewendete Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel. Klicken Sie auf Zurücksetzen, um die Legendenfilter zu entfernen", + "incompatibleFilterWarning": "Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel oder es sind keine sichtbaren Features im Kartenansichtsfenster vorhanden. Klicken Sie auf Zurücksetzen, um die Legendenfilter zu löschen", "resetLegendFilter": "Zurücksetzen", "noLegendData": "Keine Legenden Elemente zum Anzeigen" } diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 86c5aa1f2a..d0d7e09dd9 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -242,7 +242,7 @@ }, "disableFeaturesEditing": "Disable editing on Attribute table", "interactiveLegend": { - "incompatibleFilterWarning": "Applied legend filters are incompatible with the active layer filter. Click on reset to remove legend filters", + "incompatibleFilterWarning": "Legend filters are incompatible with the active layer filter, or no visible features are within the map view. Click reset to clear legend filters", "resetLegendFilter": "Reset", "noLegendData": "No legend items to show" } diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index d7d4fb5825..05ee0381eb 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -242,7 +242,7 @@ }, "disableFeaturesEditing": "Deshabilitar la edición en la tabla de atributos", "interactiveLegend": { - "incompatibleFilterWarning": "Los filtros de leyenda aplicados son incompatibles con el filtro de capa activo. Haga clic en restablecer para eliminar los filtros de leyenda", + "incompatibleFilterWarning": "Los filtros de leyenda son incompatibles con el filtro de capa activo, o no hay características visibles dentro de la vista del mapa. Haga clic en restablecer para borrar los filtros de leyenda", "resetLegendFilter": "Restablecer", "noLegendData": "No hay elementos de leyenda para mostrar" } diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 0dd0188023..61c8d8f522 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -242,7 +242,7 @@ }, "disableFeaturesEditing": "Désactiver la modification sur la table attributaire", "interactiveLegend": { - "incompatibleFilterWarning": "Les filtres de légende appliqués sont incompatibles avec le filtre de couche actif. Cliquez sur réinitialiser pour supprimer les filtres de légende", + "incompatibleFilterWarning": "Les filtres de légende sont incompatibles avec le filtre de couche actif, ou aucune fonctionnalité visible n'est dans la vue de la carte. Cliquez sur réinitialiser pour effacer les filtres de légende", "resetLegendFilter": "Réinitialiser", "noLegendData": "Aucun élément de légende à afficher" } diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 81ab902518..9c2e32872f 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -242,8 +242,7 @@ }, "disableFeaturesEditing": "Disabilita la modifica sulla tabella degli attributi", "interactiveLegend": { - "incompatibleFilterWarning": "I filtri della legenda applicati sono incompatibili con il filtro del livello attivo. Clicca su reset per rimuovere i filtri della legenda", - "resetLegendFilter": "Reset", + "incompatibleFilterWarning": "I filtri della legenda sono incompatibili con il filtro del layer attivo, oppure non ci sono feature visibili all'interno della vista della mappa. Clicca su reset per cancellare i filtri della legenda", "resetLegendFilter": "Reset", "noLegendData": "Nessun elemento della legenda da mostrare" } }, "localizedInput": {