{
+ // don't call filter handler if it is not interactive legend or filter is disabled or the filter rule is not truthy value
+ if (isLegendFilterNotApplicable) return;
+ filterLayerHandler(rule.filter);
+ }}
+ className={`ms-legend-rule ${isLegendFilterNotApplicable ? "" : "filter-enabled "} ${activeFilter && interactive ? 'active' : ''}`}>
+
+ {rule.name || ''}
+
);
+ })}
+ >);
};
return <>
@@ -33,7 +87,10 @@ function VectorLegend({ style }) {
}
VectorLegend.propTypes = {
- style: PropTypes.object
+ style: PropTypes.object,
+ layer: PropTypes.object,
+ interactive: PropTypes.bool,
+ onChange: PropTypes.func
};
export default VectorLegend;
diff --git a/web/client/plugins/TOC/components/WMSLegend.jsx b/web/client/plugins/TOC/components/WMSLegend.jsx
index cd8db22cea..9164cc0173 100644
--- a/web/client/plugins/TOC/components/WMSLegend.jsx
+++ b/web/client/plugins/TOC/components/WMSLegend.jsx
@@ -126,6 +126,7 @@ class WMSLegend extends React.Component {
legendOptions={this.props.WMSLegendOptions}
onChange={this.props.onChange}
{...this.getLegendProps()}
+ interactive
/>
);
diff --git a/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx b/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx
index 118cff9406..546f26e03a 100644
--- a/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx
+++ b/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx
@@ -10,16 +10,157 @@ import React from 'react';
import ReactDOM from 'react-dom';
import expect from 'expect';
import VectorLegend from '../VectorLegend';
+import { INTERACTIVE_LEGEND_ID } from '../../../../utils/LegendUtils';
+import { setConfigProp } from '../../../../utils/ConfigUtils';
+const rules = [
+ {
+ "name": ">= 0 and < 0.6",
+ "filter": [
+ "&&",
+ [
+ ">=",
+ "priority",
+ 0
+ ],
+ [
+ "<",
+ "priority",
+ 0.6
+ ]
+ ],
+ "symbolizers": [
+ {
+ "kind": "Fill",
+ "color": "#fff7ec",
+ "fillOpacity": 1,
+ "outlineColor": "#777777",
+ "outlineWidth": 1,
+ "msClassificationType": "both",
+ "msClampToGround": true
+ }
+ ]
+ },
+ {
+ "name": ">= 0.6 and < 1.2",
+ "filter": [
+ "&&",
+ [
+ ">=",
+ "priority",
+ 0.6
+ ],
+ [
+ "<",
+ "priority",
+ 1.2
+ ]
+ ],
+ "symbolizers": [
+ {
+ "kind": "Fill",
+ "color": "#fdd49e",
+ "fillOpacity": 1,
+ "outlineColor": "#777777",
+ "outlineWidth": 1,
+ "msClassificationType": "both",
+ "msClampToGround": true
+ }
+ ]
+ },
+ {
+ "name": ">= 1.2 and < 1.7999999999999998",
+ "filter": [
+ "&&",
+ [
+ ">=",
+ "priority",
+ 1.2
+ ],
+ [
+ "<",
+ "priority",
+ 1.7999999999999998
+ ]
+ ],
+ "symbolizers": [
+ {
+ "kind": "Fill",
+ "color": "#fc8d59",
+ "fillOpacity": 1,
+ "outlineColor": "#777777",
+ "outlineWidth": 1,
+ "msClassificationType": "both",
+ "msClampToGround": true
+ }
+ ]
+ },
+ {
+ "name": ">= 1.7999999999999998 and < 2.4",
+ "filter": [
+ "&&",
+ [
+ ">=",
+ "priority",
+ 1.7999999999999998
+ ],
+ [
+ "<",
+ "priority",
+ 2.4
+ ]
+ ],
+ "symbolizers": [
+ {
+ "kind": "Fill",
+ "color": "#d7301f",
+ "fillOpacity": 1,
+ "outlineColor": "#777777",
+ "outlineWidth": 1,
+ "msClassificationType": "both",
+ "msClampToGround": true
+ }
+ ]
+ },
+ {
+ "name": ">= 2.4 and <= 3",
+ "filter": [
+ "&&",
+ [
+ ">=",
+ "priority",
+ 2.4
+ ],
+ [
+ "<=",
+ "priority",
+ 3
+ ]
+ ],
+ "symbolizers": [
+ {
+ "kind": "Fill",
+ "color": "#7f0000",
+ "fillOpacity": 1,
+ "outlineColor": "#777777",
+ "outlineWidth": 1,
+ "msClassificationType": "both",
+ "msClampToGround": true
+ }
+ ]
+ }
+];
describe('VectorLegend module component', () => {
beforeEach((done) => {
document.body.innerHTML = '';
+ setConfigProp('miscSettings', { experimentalInteractiveLegend: true });
setTimeout(done);
});
afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById('container'));
document.body.innerHTML = '';
+ setConfigProp('miscSettings', { });
setTimeout(done);
});
@@ -314,4 +455,74 @@ describe('VectorLegend module component', () => {
const textElement = ruleElements[0].getElementsByTagName('span');
expect(textElement[0].innerHTML).toBe('');
});
+ it('tests legend with empty rules', () => {
+ const l = {
+ name: 'layer00',
+ title: 'Layer',
+ visibility: true,
+ storeIndex: 9,
+ type: 'wfs',
+ url: 'http://localhost:8080/geoserver1/wfs',
+ style: {format: 'geostyler', body: {rules: []}}
+ };
+
+ ReactDOM.render(, document.getElementById("container"));
+ const legendElem = document.querySelector('.ms-legend');
+ expect(legendElem).toBeTruthy();
+ expect(legendElem.innerText).toBe('layerProperties.interactiveLegend.noLegendData');
+ });
+ it('tests legend with incompatible filter rules', () => {
+ const l = {
+ name: 'layer00',
+ title: 'Layer',
+ visibility: true,
+ storeIndex: 9,
+ type: 'wfs',
+ url: 'http://localhost:8080/geoserver2/wfs',
+ enableInteractiveLegend: true,
+ layerFilter: {
+ filters: [{
+ id: INTERACTIVE_LEGEND_ID,
+ filters: [{
+ id: 'filter1'
+ }]
+ }],
+ disabled: false
+ }
+ };
+ ReactDOM.render(, document.getElementById("container"));
+ const legendElem = document.querySelector('.ms-legend');
+ expect(legendElem).toBeTruthy();
+ const legendRuleElem = document.querySelector('.ms-legend .alert-warning');
+ expect(legendRuleElem).toBeTruthy();
+ expect(legendRuleElem.innerText).toContain('layerProperties.interactiveLegend.incompatibleWFSFilterWarning');
+ const resetLegendFilter = document.querySelector('.ms-legend .alert-warning button');
+ expect(resetLegendFilter).toBeTruthy();
+ });
+ it('tests hide warning when layer filter is disabled', () => {
+ const l = {
+ name: 'layer00',
+ title: 'Layer',
+ visibility: true,
+ storeIndex: 9,
+ type: 'wfs',
+ url: 'http://localhost:8080/geoserver3/wfs',
+ layerFilter: {
+ filters: [{
+ id: INTERACTIVE_LEGEND_ID,
+ filters: [{
+ id: 'filter1'
+ }]
+ }],
+ disabled: true
+ }
+ };
+
+ ReactDOM.render(, document.getElementById("container"));
+ const legendElem = document.querySelector('.ms-legend');
+ expect(legendElem).toBeTruthy();
+ const legendRuleElem = document.querySelector('.ms-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 74c2487c92..31b386c78d 100644
--- a/web/client/themes/default/less/toc.less
+++ b/web/client/themes/default/less/toc.less
@@ -88,10 +88,10 @@
.background-color-var(@theme-vars[selected-bg]);
.outline-color-var(@theme-vars[focus-color]);
}
- .wms-json-legend-rule.filter-enabled:hover {
+ .ms-legend-rule.filter-enabled:hover,.wms-json-legend-rule.filter-enabled:hover {
.background-color-var(@theme-vars[selected-hover-bg]);
}
- .wms-json-legend-rule.filter-enabled.active {
+ .ms-legend-rule.filter-enabled.active, .wms-json-legend-rule.filter-enabled.active {
.background-color-var(@theme-vars[selected-bg]);
}
}
@@ -426,6 +426,6 @@
align-items: normal;
}
}
-.wms-json-legend-rule.filter-enabled:hover {
+.ms-legend-rule.filter-enabled:hover, .wms-json-legend-rule.filter-enabled:hover {
cursor: pointer;
}
\ No newline at end of file
diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json
index 5c61e664ab..84e74eadc0 100644
--- a/web/client/translations/data.de-DE.json
+++ b/web/client/translations/data.de-DE.json
@@ -206,6 +206,7 @@
"enableInteractiveLegendInfo": {
"label": "Aktivieren Sie die interaktive Legende",
"info": "Wenn diese Option aktiviert ist, kann der Filter nach Legende angewendet werden, indem im Inhaltsverzeichnis auf Legendenelemente dieser Ebene geklickt wird. Hinweis: Diese Einstellung benötigt spezifische Konfigurationen im GeoServer",
+ "infoWithoutGSNote": "Wenn diese Option aktiviert ist, kann der Filter nach Legende angewendet werden, indem im Inhaltsverzeichnis auf Legendenelemente dieser Ebene geklickt wird.",
"fetchError": "Die Legenden Informationen konnten nicht vom Dienst abgerufen werden"
},
"enableLocalizedLayerStyles": {
@@ -243,6 +244,7 @@
"disableFeaturesEditing": "Deaktivieren Sie die Bearbeitung der Attributtabelle",
"interactiveLegend": {
"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",
+ "incompatibleWFSFilterWarning": "Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel. 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 02595d66d6..4a11155b3b 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -206,6 +206,7 @@
"enableInteractiveLegendInfo": {
"label": "Enable interactive legend",
"info": "If this option is enabled, filter by legend can be applied by clicking on legend items of this layer from the TOC. Note: This parameter requires specific configurations on GeoServer",
+ "infoWithoutGSNote": "If this option is enabled, filter by legend can be applied by clicking on legend items of this layer from the TOC.",
"fetchError": "Failed to retrieve the legend info from the service"
},
"enableLocalizedLayerStyles": {
@@ -243,6 +244,7 @@
"disableFeaturesEditing": "Disable editing on Attribute table",
"interactiveLegend": {
"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",
+ "incompatibleWFSFilterWarning": "Legend filters are incompatible with the active layer filter. 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 95460de5c8..ad5afc4284 100644
--- a/web/client/translations/data.es-ES.json
+++ b/web/client/translations/data.es-ES.json
@@ -203,6 +203,7 @@
"enableInteractiveLegendInfo": {
"label": "Habilitar una leyenda interactiva",
"info": "Si esta opción está habilitada, se puede aplicar el filtro por leyenda haciendo clic en los elementos de leyenda de esta capa desde el TOC. Nota: este parámetro requiere configuraciones específicas en GeoServer",
+ "infoWithoutGSNote": "Si esta opción está habilitada, se puede aplicar el filtro por leyenda haciendo clic en los elementos de leyenda de esta capa desde el TOC.",
"fetchError": "No se pudo obtener la información de la leyenda del servicio"
},
"templateFormatInfoAlertExample": "La identificación de la característica es ${ properties.datos } ora ${ properties['datos-valor'] }",
@@ -243,6 +244,7 @@
"disableFeaturesEditing": "Deshabilitar la edición en la tabla de atributos",
"interactiveLegend": {
"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",
+ "incompatibleWFSFilterWarning": "Los filtros de leyenda son incompatibles con el filtro de capa activo. 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 f1cbd8864b..471ea7191a 100644
--- a/web/client/translations/data.fr-FR.json
+++ b/web/client/translations/data.fr-FR.json
@@ -206,6 +206,7 @@
"enableInteractiveLegendInfo": {
"label": "Activer la légende interactive",
"info": "Si cette option est activée, le filtre par légende peut être appliqué en cliquant sur les éléments de légende de cette couche depuis la table des matières. Remarque: ce paramètre nécessite des configurations spécifiques sur GeoServer",
+ "infoWithoutGSNote": "Si cette option est activée, le filtre par légende peut être appliqué en cliquant sur les éléments de légende de cette couche depuis la table des matières.",
"fetchError": "Impossible d'obtenir les informations de légende du service"
},
"enableLocalizedLayerStyles": {
@@ -243,6 +244,7 @@
"disableFeaturesEditing": "Désactiver la modification sur la table attributaire",
"interactiveLegend": {
"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",
+ "incompatibleWFSFilterWarning": "Les filtres de légende sont incompatibles avec le filtre de couche actif. 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 8d2905dbc3..fa65152b4a 100644
--- a/web/client/translations/data.it-IT.json
+++ b/web/client/translations/data.it-IT.json
@@ -206,6 +206,7 @@
"enableInteractiveLegendInfo": {
"label": "Abilita leggenda interattiva",
"info": "Se questa opzione è abilitata, è possibile applicare il filtro per legenda facendo clic sugli elementi della legenda di questo layer dal sommario. Nota: questo parametro richiede configurazioni specifiche su GeoServer",
+ "infoWithoutGSNote": "Se questa opzione è abilitata, è possibile applicare il filtro per legenda facendo clic sugli elementi della legenda di questo layer dal sommario.",
"fetchError": "Impossibile ottenere le informazioni sulla leggenda dal servizio"
},
"enableLocalizedLayerStyles": {
@@ -243,6 +244,7 @@
"disableFeaturesEditing": "Disabilita la modifica sulla tabella degli attributi",
"interactiveLegend": {
"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",
+ "incompatibleWFSFilterWarning": "I filtri della legenda sono incompatibili con il filtro del layer attivo. Clicca su reset per cancellare i filtri della legenda", "resetLegendFilter": "Reset",
"noLegendData": "Nessun elemento della legenda da mostrare" }
},
"localizedInput": {
diff --git a/web/client/utils/FilterUtils.js b/web/client/utils/FilterUtils.js
index ce9297b606..e08dbd5ea4 100644
--- a/web/client/utils/FilterUtils.js
+++ b/web/client/utils/FilterUtils.js
@@ -1369,6 +1369,76 @@ export const updateLayerLegendFilter = (layerFilterObj, legendFilter) => {
return newFilter;
};
+/**
+ * Merges legend geostyler filter, with mapstore filter objects to filters
+ * @param {object} layerFilterObj previous layer filter object includes all filters
+ * @param {array} legendGeostylerFilter geostyler filter
+ * @return {object} layerFilterObj updated the layer filter object
+ */
+export const updateLayerWFSVectorLegendFilter = (layerFilterObj, legendGeostylerFilter) => {
+ const defaultLayerFilter = {
+ groupFields: [
+ {
+ id: 1,
+ logic: 'OR',
+ index: 0
+ }
+ ],
+ filterFields: [],
+ attributePanelExpanded: true,
+ spatialPanelExpanded: true,
+ crossLayerExpanded: true,
+ crossLayerFilter: {
+ attribute: 'the_geom'
+ },
+ spatialField: {
+ method: null,
+ operation: 'INTERSECTS',
+ geometry: null,
+ attribute: 'the_geom'
+ }
+ };
+ let filterObj = {...defaultLayerFilter, ...layerFilterObj};
+ const isLegendFilterExist = filterObj?.filters?.find(f => f.id === INTERACTIVE_LEGEND_ID);
+ if (!legendGeostylerFilter) {
+ // clear legend filter with id = 'interactiveLegend'
+ if (isLegendFilterExist) {
+ filterObj = {
+ ...filterObj, filters: filterObj?.filters?.filter(f => f.id !== INTERACTIVE_LEGEND_ID)
+ };
+ }
+ let newFilter = filterObj ? filterObj : undefined;
+ return newFilter;
+ }
+ let interactiveLegendFilters = isLegendFilterExist ? isLegendFilterExist.filters || [] : [];
+
+ const isSelectedFilterExist = interactiveLegendFilters.find(legFilter => legFilter.body?.toString() === legendGeostylerFilter?.toString());
+ if (isSelectedFilterExist) {
+ interactiveLegendFilters = interactiveLegendFilters.filter(legFilter => legFilter.body?.toString() !== legendGeostylerFilter?.toString());
+ } else {
+ interactiveLegendFilters = [...interactiveLegendFilters, {
+ "format": "geostyler",
+ "version": "1.0.0",
+ "body": legendGeostylerFilter,
+ "id": `${legendGeostylerFilter?.toString()}`
+ }];
+ }
+ let newFilter = {
+ ...(filterObj || {}), filters: [
+ ...(filterObj?.filters?.filter(f => f.id !== INTERACTIVE_LEGEND_ID) || []), ...[
+ {
+ "id": INTERACTIVE_LEGEND_ID,
+ "format": "logic",
+ "version": "1.0.0",
+ "logic": "OR",
+ "filters": [...interactiveLegendFilters]
+ }
+ ]
+ ]
+ };
+ return newFilter;
+};
+
export function resetLayerLegendFilter(layer, reason, value) {
const isResetForStyle = reason === 'style'; // here the reason for reset is change 'style' or change the enable/disable interactive legend config 'disableEnableInteractiveLegend'
let needReset = false;
@@ -1415,6 +1485,5 @@ FilterUtils = {
processOGCSpatialFilter,
createFeatureFilter,
mergeFiltersToOGC,
- convertFiltersToOGC,
- INTERACTIVE_LEGEND_ID
+ convertFiltersToOGC
};
diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js
index 18405b938e..0ccd4169a8 100644
--- a/web/client/utils/LegendUtils.js
+++ b/web/client/utils/LegendUtils.js
@@ -115,7 +115,6 @@ export const updateLayerWithLegendFilters = (layers, dependencies) => {
};
export default {
- INTERACTIVE_LEGEND_ID,
getLayerFilterByLegendFormat,
getWMSLegendConfig,
updateLayerWithLegendFilters
diff --git a/web/client/utils/__tests__/FilterUtils-test.js b/web/client/utils/__tests__/FilterUtils-test.js
index 7433c3dae2..019bb0d779 100644
--- a/web/client/utils/__tests__/FilterUtils-test.js
+++ b/web/client/utils/__tests__/FilterUtils-test.js
@@ -30,7 +30,9 @@ import {
convertFiltersToOGC,
convertFiltersToCQL,
isFilterEmpty,
- updateLayerLegendFilter, resetLayerLegendFilter
+ updateLayerLegendFilter,
+ resetLayerLegendFilter,
+ updateLayerWFSVectorLegendFilter
} from '../FilterUtils';
import { INTERACTIVE_LEGEND_ID } from '../LegendUtils';
@@ -2332,6 +2334,7 @@ describe('FilterUtils', () => {
})).toBe(false);
});
+ // for wms
it('test updateLayerLegendFilter for wms, simple filter', () => {
const layerFilterObj = {};
const lgegendFilter = "[FIELD1 = 'Value' AND FIELD2 > '1256']";
@@ -2492,4 +2495,109 @@ describe('FilterUtils', () => {
expect(updatedFilterObj.filters.length).toEqual(0);
expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID)).toBeFalsy();
});
+ // for WFS
+ it('test updateLayerWFSVectorLegendFilter for wfs, simple filter', () => {
+ const layerFilterObj = {};
+ const lgegendFilter = [
+ "&&",
+ ["==", "FIELD1", 'Value'],
+ ["==", "FIELD2", '1256']
+ ];
+ const updatedFilterObj = updateLayerWFSVectorLegendFilter(layerFilterObj, lgegendFilter);
+ expect(updatedFilterObj).toBeTruthy();
+ expect(updatedFilterObj.filters.length).toEqual(1);
+ expect(updatedFilterObj.filters.filter(i => i.id === INTERACTIVE_LEGEND_ID)?.length).toEqual(1);
+ expect(updatedFilterObj.filters[0].filters[0].format).toEqual("geostyler");
+ expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID).filters.length).toEqual(1);
+ });
+ it('test updateLayerWFSVectorLegendFilter for wfs, apply multi legend filter', () => {
+ const layerFilterObj = {
+ "groupFields": [
+ {
+ "id": 1,
+ "logic": "OR",
+ "index": 0
+ }
+ ],
+ "filterFields": [],
+ "attributePanelExpanded": true,
+ "spatialPanelExpanded": true,
+ "crossLayerExpanded": true,
+ "crossLayerFilter": {
+ "attribute": "the_geom"
+ },
+ "spatialField": {
+ "method": null,
+ "operation": "INTERSECTS",
+ "geometry": null,
+ "attribute": "the_geom"
+ },
+ "filters": [
+ {
+ "id": INTERACTIVE_LEGEND_ID,
+ "format": "logic",
+ "version": "1.0.0",
+ "logic": "OR",
+ "filters": [
+ {
+ "format": "geostyler",
+ "version": "1.0.0",
+ "body": ["&&", ['>=', 'FIELD_01', '2500'], ['<', 'FIELD_01', '7000']],
+ "id": "&&,>=,FIELD_01,2500,<,FIELD_01,7000"
+ }
+ ]
+ }
+ ]
+ };
+ const lgegendFilter = ["&&", ['>=', 'FIELD_01', '13000'], ['<', 'FIELD_01', '14500']];
+ const updatedFilterObj = updateLayerLegendFilter(layerFilterObj, lgegendFilter);
+ expect(updatedFilterObj).toBeTruthy();
+ expect(updatedFilterObj.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(2);
+ });
+ it('test reset legend filter using updateLayerWFSVectorLegendFilter', () => {
+ const layerFilterObj = {
+ "groupFields": [
+ {
+ "id": 1,
+ "logic": "OR",
+ "index": 0
+ }
+ ],
+ "filterFields": [],
+ "attributePanelExpanded": true,
+ "spatialPanelExpanded": true,
+ "crossLayerExpanded": true,
+ "crossLayerFilter": {
+ "attribute": "the_geom"
+ },
+ "spatialField": {
+ "method": null,
+ "operation": "INTERSECTS",
+ "geometry": null,
+ "attribute": "the_geom"
+ },
+ "filters": [
+ {
+ "id": INTERACTIVE_LEGEND_ID,
+ "format": "logic",
+ "version": "1.0.0",
+ "logic": "OR",
+ "filters": [
+ {
+ "format": "geostyler",
+ "version": "1.0.0",
+ "body": ["&&", ['>=', 'FIELD_01', '2500'], ['<', 'FIELD_01', '7000']],
+ "id": "&&,>=,FIELD_01,2500,<,FIELD_01,7000"
+ }
+ ]
+ }
+ ]
+ };
+ const updatedFilterObj = updateLayerWFSVectorLegendFilter(layerFilterObj);
+ expect(updatedFilterObj).toBeTruthy();
+ expect(updatedFilterObj.filters.length).toEqual(0);
+ expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID)).toBeFalsy();
+ });
});
diff --git a/web/client/utils/filter/converters/__tests__/geostyler-test.js b/web/client/utils/filter/converters/__tests__/geostyler-test.js
new file mode 100644
index 0000000000..9b9e18069e
--- /dev/null
+++ b/web/client/utils/filter/converters/__tests__/geostyler-test.js
@@ -0,0 +1,54 @@
+/*
+ * 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 expect from 'expect';
+import geostyler from '../geostyler';
+describe('GeoStyler converter', () => {
+ const SAMPLES = [
+ // logic operators
+ {
+ cql: '(prop1 = 1 AND prop2 = 2)',
+ geostyler: {
+ body: ['&&', ['==', 'prop1', 1], ['==', 'prop2', 2]]
+ }
+ },
+ {
+ cql: '(prop1 = 1 OR prop2 = 2)',
+ geostyler: {body: ['||', ['==', 'prop1', 1], ['==', 'prop2', 2]]}
+ },
+ {
+ cql: '(prop1 = 1 AND (prop2 = 2 OR prop3 = 3))',
+ geostyler: {body: ['&&', ['==', 'prop1', 1], ['||', ['==', 'prop2', 2], ['==', 'prop3', 3]]]}
+ },
+ {
+ cql: '(prop1 = 1 OR (prop2 = 2 AND prop3 = 3))',
+ geostyler: {body: ['||', ['==', 'prop1', 1], ['&&', ['==', 'prop2', 2], ['==', 'prop3', 3]]]}
+ },
+ // comparison operators
+ {
+ cql: 'prop1 = 1',
+ geostyler: {body: ['==', 'prop1', 1]}
+ },
+ {
+ cql: 'prop1 <> 1',
+ geostyler: {body: ['!=', 'prop1', 1]}
+ },
+ {
+ cql: 'prop1 < 1',
+ geostyler: {body: ['<', 'prop1', 1]}
+ },
+ {
+ cql: 'prop1 <= 1',
+ geostyler: {body: ['<=', 'prop1', 1]}
+ }
+ ];
+ it('test geostyler to cql', () => {
+ SAMPLES.forEach((sample) => {
+ expect(geostyler.cql(sample.geostyler)).toBe(sample.cql);
+ });
+ });
+});
diff --git a/web/client/utils/filter/converters/__tests__/index-test.js b/web/client/utils/filter/converters/__tests__/index-test.js
index 68674f52c4..9ed7f208bb 100644
--- a/web/client/utils/filter/converters/__tests__/index-test.js
+++ b/web/client/utils/filter/converters/__tests__/index-test.js
@@ -15,6 +15,7 @@ describe('Filter converters', () => {
expect(canConvert('logic', 'cql')).toBe(true, "logic to cql conversion not allowed");
expect(canConvert('ogc', 'logic')).toBe(false, "ogc to logic conversion allowed, but it shouldn't");
expect(canConvert('cql', 'logic')).toBe(false, "cql to logic conversion allowed, but it shouldn't");
+ expect(canConvert('geostyler', 'cql')).toBe(true, "geostyler to cql conversion allowed");
});
const SAMPLES = [
{
diff --git a/web/client/utils/filter/converters/geostyler.js b/web/client/utils/filter/converters/geostyler.js
new file mode 100644
index 0000000000..2debbf666f
--- /dev/null
+++ b/web/client/utils/filter/converters/geostyler.js
@@ -0,0 +1,58 @@
+/*
+ * 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 { isArray } from "lodash";
+
+const operatorMap = {
+ '==': '=',
+ '!=': '<>',
+ '>': '>',
+ '<': '<',
+ '>=': '>=',
+ '<=': '<=',
+ '&&': 'AND',
+ '||': 'OR'
+};
+
+/**
+ * Converts a Geostyler filter to CQL filter
+ * @param {object|string} geostylerFilter geostyler filter rules array
+ * @returns {string} CQL filter
+ */
+
+export const cql = (geostylerFilter) => {
+ function parseCondition(filter) {
+ if (Array.isArray(filter)) {
+ let [operator, ...filterRules] = filter;
+ if (operatorMap[operator]) {
+ if (operator === '&&' || operator === '||') {
+ return `(${filterRules.map(parseCondition).join(` ${operatorMap[operator]} `)})`;
+ }
+ let field = filter[1];
+ let value = filter[2];
+ if (typeof value === 'string') {
+ value = `'${value}'`;
+ } else if (typeof value === 'boolean') {
+ value = value ? 'TRUE' : 'FALSE';
+ }
+ return `${field} ${operatorMap[operator]} ${value}`;
+
+ }
+ }
+ return '';
+ }
+ const geostylerRules = isArray(geostylerFilter) ? geostylerFilter : geostylerFilter?.body;
+ return parseCondition(geostylerRules);
+};
+
+// TODO: create a converter from cql to geostyler rules
+
+export default {
+ cql
+ // geostyler
+};
diff --git a/web/client/utils/filter/converters/index.js b/web/client/utils/filter/converters/index.js
index 11b43216b1..73ae202c48 100644
--- a/web/client/utils/filter/converters/index.js
+++ b/web/client/utils/filter/converters/index.js
@@ -9,6 +9,7 @@
* @prop {function} toOgc
*/
import cql from './cql';
+import geostyler from './geostyler';
const converters = {
};
@@ -33,6 +34,7 @@ export const canConvert = (from, to) => {
};
converters.cql = cql;
+converters.geostyler = geostyler;
converters.logic = {
cql: (filter) => {
@@ -83,5 +85,25 @@ converters.logic = {
}
return null;
+ },
+ geostyler: (filter) => {
+ if (filter.logic) {
+ const convertFilter = (f) => {
+ if (canConvert(f.format, 'geostyler')) {
+ return getConverter(f.format, 'geostyler')(f);
+ }
+ return null;
+ };
+ if (!filter.filters || filter.filters.length === 0) {
+ return [];
+ } else if (filter.filters.length === 1) {
+ if (filter.logic.toUpperCase() === 'NOT') {
+ return ['!', convertFilter(filter.filters[0])];
+ }
+ return convertFilter(filter.filters[0]);
+ }
+ return [ filter.logic.toUpperCase(), ...filter.filters.map(convertFilter) ];
+ }
+ return null;
}
};