diff --git a/web/client/components/TOC/fragments/settings/Display.jsx b/web/client/components/TOC/fragments/settings/Display.jsx index 293f07808f..fb1dae9a67 100644 --- a/web/client/components/TOC/fragments/settings/Display.jsx +++ b/web/client/components/TOC/fragments/settings/Display.jsx @@ -351,7 +351,7 @@ export default class extends React.Component { } - {this.props.element.type === "wfs" && + {['wfs', 'vector'].includes(this.props.element.type) &&
{experimentalInteractiveLegend && diff --git a/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx b/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx index 119f84a315..435a23422c 100644 --- a/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx +++ b/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx @@ -407,5 +407,43 @@ describe('test Layer Properties Display module component', () => { expect(spy.calls[0].arguments[0]).toEqual("enableInteractiveLegend"); expect(spy.calls[0].arguments[1]).toEqual(true); }); + it('tests vector Layer Properties Legend component events', () => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'vector', + url: 'fakeurl', + legendOptions: { + legendWidth: 15, + legendHeight: 15 + }, + enableInteractiveLegend: false + }; + const settings = { + options: { + opacity: 1 + } + }; + const handlers = { + onChange() {} + }; + let spy = expect.spyOn(handlers, "onChange"); + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toBeTruthy(); + const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" ); + const legendPreview = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "legend-preview" ); + expect(legendPreview).toBeTruthy(); + expect(inputs).toBeTruthy(); + expect(inputs.length).toBe(6); + let interactiveLegendConfig = document.querySelector(".legend-options input[data-qa='display-interactive-legend-option']"); + // change enableInteractiveLegend to enable interactive legend + interactiveLegendConfig.checked = true; + ReactTestUtils.Simulate.change(interactiveLegendConfig); + expect(spy).toHaveBeenCalled(); + expect(spy.calls[0].arguments[0]).toEqual("enableInteractiveLegend"); + expect(spy.calls[0].arguments[1]).toEqual(true); + }); }); diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx index 8158645754..68d996e173 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx @@ -51,7 +51,7 @@ export default ({  } /> } - {experimentalInteractiveLegend && ['wfs'].includes(service.type) && + {experimentalInteractiveLegend && ['wfs', 'vector'].includes(service.type) && onChangeServiceProperty("layerOptions", { ...service.layerOptions, enableInteractiveLegend: e.target.checked})} checked={!isNil(service.layerOptions?.enableInteractiveLegend) ? service.layerOptions?.enableInteractiveLegend : false}> diff --git a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js index c4a4d73a3d..013c5a0d44 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js +++ b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js @@ -88,13 +88,23 @@ describe('Test common advanced settings', () => { expect(spyOn).toHaveBeenCalled(); expect(spyOn.calls[1].arguments).toEqual([ 'fetchMetadata', false ]); }); - it('test showing/hiding interactive legend checkbox', () => { + it('test showing/hiding interactive legend checkbox for WFS', () => { ReactDOM.render(, document.getElementById("container")); - const interactiveLegendCheckboxInput = document.querySelector(".wfs-interactive-legend .checkbox input[data-qa='display-interactive-legend-option']"); + const interactiveLegendCheckboxInput = document.querySelector(".wfs-vector-interactive-legend .checkbox input[data-qa='display-interactive-legend-option']"); expect(interactiveLegendCheckboxInput).toBeTruthy(); - const interactiveLegendLabel = document.querySelector(".wfs-interactive-legend .checkbox span"); + const interactiveLegendLabel = document.querySelector(".wfs-vector-interactive-legend .checkbox span"); + expect(interactiveLegendLabel).toBeTruthy(); + expect(interactiveLegendLabel.innerHTML).toEqual('layerProperties.enableInteractiveLegendInfo.label'); + }); + it('test showing/hiding interactive legend checkbox for vector', () => { + ReactDOM.render(, document.getElementById("container")); + const interactiveLegendCheckboxInput = document.querySelector(".wfs-vector-interactive-legend .checkbox input[data-qa='display-interactive-legend-option']"); + expect(interactiveLegendCheckboxInput).toBeTruthy(); + const interactiveLegendLabel = document.querySelector(".wfs-vector-interactive-legend .checkbox span"); expect(interactiveLegendLabel).toBeTruthy(); expect(interactiveLegendLabel.innerHTML).toEqual('layerProperties.enableInteractiveLegendInfo.label'); }); diff --git a/web/client/components/map/BaseMap.jsx b/web/client/components/map/BaseMap.jsx index ca728de42e..a1df5cf232 100644 --- a/web/client/components/map/BaseMap.jsx +++ b/web/client/components/map/BaseMap.jsx @@ -9,6 +9,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { isString } from 'lodash'; +import { createVectorFeatureFilter } from '../../utils/FilterUtils'; /** * Base map component that renders a map. @@ -104,7 +105,8 @@ class BaseMap extends React.Component { if (layer.features && layer.type === "vector") { const { plugins } = this.props; const { Feature } = plugins; - return layer.features.map((feature) => { + const vectorFeatureFilter = createVectorFeatureFilter(layer); + return layer.features.filter(vectorFeatureFilter).map((feature) => { return ( { document.body.innerHTML = '
'; map = new Cesium.Viewer("map"); map.imageryLayers.removeAll(); + setConfigProp('miscSettings', { experimentalInteractiveLegend: true }); setTimeout(done); }); @@ -55,6 +56,7 @@ describe('Cesium layer', () => { } catch(e) {} /* eslint-enable */ document.body.innerHTML = ''; + setConfigProp('miscSettings', { }); setTimeout(done); }); it('missing layer', () => { @@ -1491,6 +1493,69 @@ describe('Cesium layer', () => { expect(cmp.layer.styledFeatures._queryable).toBe(false); expect(cmp.layer.styledFeatures._features.length).toBe(1); }); + it('should create a vector layer with interactive legend filter', () => { + const options = { + type: 'vector', + features: [ + { type: 'Feature', properties: { "prop1": 0 }, geometry: { type: 'Point', coordinates: [0, 0] } }, + { type: 'Feature', properties: { "prop1": 2 }, geometry: { type: 'Point', coordinates: [1, 0] } }, + { type: 'Feature', properties: { "prop1": 5 }, geometry: { type: 'Point', coordinates: [2, 0] } } + ], + title: 'Title', + visibility: true, + bbox: { + crs: 'EPSG:4326', + bounds: { + minx: -180, + miny: -90, + maxx: 180, + maxy: 90 + } + }, + enableInteractiveLegend: true, + layerFilter: { + filters: [{ + "id": "interactiveLegend", + "format": "logic", + "version": "1.0.0", + "logic": "OR", + "filters": [ + { + "format": "geostyler", + "version": "1.0.0", + "body": [ + "&&", + [ + ">", + "prop1", + "0" + ], [ + "<", + "prop1", + "3" + ] + ], + "id": "&&,>,prop1,0,<,prop1,3" + } + ] + }] + } + }; + // create layers + const cmp = ReactDOM.render( + , document.getElementById('container')); + expect(cmp).toBeTruthy(); + expect(cmp.layer).toBeTruthy(); + expect(cmp.layer.styledFeatures).toBeTruthy(); + expect(cmp.layer.detached).toBe(true); + const renderedFeatsNum = cmp.layer.styledFeatures._features.filter(cmp.layer.styledFeatures._featureFilter).length; + const filteredFeatsNum = 1; + expect(renderedFeatsNum).toEqual(filteredFeatsNum); + }); it('should create a wfs layer', () => { const options = { type: 'wfs', diff --git a/web/client/components/map/cesium/plugins/VectorLayer.js b/web/client/components/map/cesium/plugins/VectorLayer.js index 34fccd432b..86c2aba45f 100644 --- a/web/client/components/map/cesium/plugins/VectorLayer.js +++ b/web/client/components/map/cesium/plugins/VectorLayer.js @@ -15,6 +15,7 @@ import { } from '../../../../utils/VectorStyleUtils'; import { applyDefaultStyleToVectorLayer } from '../../../../utils/StyleUtils'; import GeoJSONStyledFeatures from '../../../../utils/cesium/GeoJSONStyledFeatures'; +import { createVectorFeatureFilter } from '../../../../utils/FilterUtils'; const createLayer = (options, map) => { @@ -27,13 +28,14 @@ const createLayer = (options, map) => { } const features = flattenFeatures(options?.features || [], ({ style, ...feature }) => feature); - + const vectorFeatureFilter = createVectorFeatureFilter(options); let styledFeatures = new GeoJSONStyledFeatures({ features, id: options?.id, map: map, opacity: options.opacity, - queryable: options.queryable === undefined || options.queryable + queryable: options.queryable === undefined || options.queryable, + featureFilter: vectorFeatureFilter // make filter for features if filter is existing }); layerToGeoStylerStyle(options) @@ -43,7 +45,6 @@ const createLayer = (options, map) => { styledFeatures.setStyleFunction(styleFunc); }); }); - return { detached: true, styledFeatures, @@ -62,6 +63,10 @@ Layers.registerType('vector', { if (!isEqual(newOptions.features, oldOptions.features)) { return createLayer(newOptions, map); } + if (layer?.styledFeatures && !isEqual(newOptions?.layerFilter, oldOptions?.layerFilter)) { + const vectorFeatureFilter = createVectorFeatureFilter(newOptions); + layer.styledFeatures.setFeatureFilter(vectorFeatureFilter); + } if (layer?.styledFeatures && !isEqual(newOptions.style, oldOptions.style)) { layerToGeoStylerStyle(newOptions) diff --git a/web/client/components/map/leaflet/__tests__/Layer-test.jsx b/web/client/components/map/leaflet/__tests__/Layer-test.jsx index 76035caac8..ab3bf6de42 100644 --- a/web/client/components/map/leaflet/__tests__/Layer-test.jsx +++ b/web/client/components/map/leaflet/__tests__/Layer-test.jsx @@ -42,6 +42,7 @@ describe('Leaflet layer', () => { let map; beforeEach((done) => { + setConfigProp('miscSettings', { experimentalInteractiveLegend: true }); mockAxios = new MockAdapter(axios); document.body.innerHTML = '
'; map = L.map('map'); @@ -49,6 +50,7 @@ describe('Leaflet layer', () => { }); afterEach((done) => { + setConfigProp('miscSettings', { }); mockAxios.restore(); ReactDOM.unmountComponentAtNode(document.getElementById("map")); ReactDOM.unmountComponentAtNode(document.getElementById("container")); @@ -525,6 +527,152 @@ describe('Leaflet layer', () => { />)}, document.getElementById("container")); expect(l2).toExist(); }); + it('creates a non legacy vector layer for leaflet map with interactive legend filter', () => { + var options = { + "type": "vector", + "visibility": true, + "name": "vector_sample", + "group": "sample", + "styleName": "marker", + "features": [ + { "type": "Feature", + "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, + "properties": { + "prop0": "value0", + "prop2": "value2" + } + }, + { "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0] + ] + }, + "properties": { + "prop0": "value0", + "prop2": "value2", + "prop1": 0.0 + } + }, + { "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], + [100.0, 1.0], [100.0, 0.0] ] + ] + }, + "properties": { + "prop0": "value0", + "prop2": "value1", + "prop1": {"this": "that"} + } + }, + { "type": "Feature", + "geometry": { "type": "MultiPoint", + "coordinates": [ [100.0, 0.0], [101.0, 1.0] ] + }, + "properties": { + "prop0": "value0", + "prop2": "value2", + "prop1": {"this": "that"} + } + }, + { "type": "Feature", + "geometry": { "type": "MultiLineString", + "coordinates": [ + [ [100.0, 0.0], [101.0, 1.0] ], + [ [102.0, 2.0], [103.0, 3.0] ] + ] + }, + "properties": { + "prop0": "value0", + "prop2": "value2", + "prop1": {"this": "that"} + } + }, + { "type": "Feature", + "geometry": { "type": "MultiPolygon", + "coordinates": [ + [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]], + [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], + [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]] + ] + }, + "properties": { + "prop0": "value0", + "prop2": "value1", + "prop1": {"this": "that"} + } + }, + { "type": "Feature", + "geometry": { "type": "GeometryCollection", + "geometries": [ + { "type": "Point", + "coordinates": [100.0, 0.0] + }, + { "type": "LineString", + "coordinates": [ [101.0, 0.0], [102.0, 1.0] ] + } + ] + }, + "properties": { + "prop0": "value0", + "prop2": "value2", + "prop1": {"this": "that"} + } + } + ] + }; + // create layers + let l2 = ReactDOM.render( + + {options.features.map((feature) => )}, document.getElementById("container")); + expect(l2).toExist(); + const renderedFeaturesNum = l2.layer.getLayers().length; + const filteredFeaturesNum = 2; + expect(renderedFeaturesNum).toEqual(filteredFeaturesNum); + }); it('creates a wms layer for leaflet map with custom tileSize', () => { var options = { diff --git a/web/client/components/map/leaflet/plugins/VectorLayer.jsx b/web/client/components/map/leaflet/plugins/VectorLayer.jsx index 2834bed845..0ab86451ad 100644 --- a/web/client/components/map/leaflet/plugins/VectorLayer.jsx +++ b/web/client/components/map/leaflet/plugins/VectorLayer.jsx @@ -13,6 +13,7 @@ import { getStyle } from '../../../../utils/VectorStyleUtils'; import { applyDefaultStyleToVectorLayer } from '../../../../utils/StyleUtils'; +import { createVectorFeatureFilter } from '../../../../utils/FilterUtils'; const setOpacity = (layer, opacity) => { if (layer.eachLayer) { @@ -54,13 +55,16 @@ const createLayerLegacy = (options) => { const createLayer = (options) => { const { hideLoading } = options; - const layer = L.geoJson(options.features, { + const vectorFeatureFilter = createVectorFeatureFilter(options); + const featuresToRender = options.features.filter(vectorFeatureFilter); // make filter for features if filter is existing + + const layer = L.geoJson(featuresToRender, { hideLoading: hideLoading }); getStyle(applyDefaultStyleToVectorLayer(options), 'leaflet') .then((styleUtils) => { - styleUtils({ opacity: options.opacity, layer, features: options.features }) + styleUtils({ opacity: options.opacity, layer, features: featuresToRender }) .then(({ style: styleFunc, pointToLayer = () => null, @@ -69,7 +73,7 @@ const createLayer = (options) => { layer.clearLayers(); layer.options.pointToLayer = pointToLayer; layer.options.filter = filterFunc; - layer.addData(options.features); + layer.addData(featuresToRender); layer.setStyle(styleFunc); }); }); @@ -89,6 +93,10 @@ const updateLayerLegacy = (layer, newOptions, oldOptions) => { }; const updateLayer = (layer, newOptions, oldOptions) => { + if (!isEqual(oldOptions.layerFilter, newOptions.layerFilter)) { + layer.remove(); + return createLayer(newOptions); + } if (!isEqual(oldOptions.style, newOptions.style) || newOptions.opacity !== oldOptions.opacity) { getStyle(applyDefaultStyleToVectorLayer(newOptions), 'leaflet') @@ -111,9 +119,11 @@ const updateLayer = (layer, newOptions, oldOptions) => { }; Layers.registerType('vector', { - create: (options) => !isNewStyle(options) - ? createLayerLegacy(options) - : createLayer(options), + create: (options) => { + return !isNewStyle(options) + ? createLayerLegacy(options) + : createLayer(options); + }, update: (layer, newOptions, oldOptions) => layer._msLegacyGeoJSON ? updateLayerLegacy(layer, newOptions, oldOptions) : updateLayer(layer, newOptions, oldOptions), diff --git a/web/client/components/map/openlayers/__tests__/Layer-test.jsx b/web/client/components/map/openlayers/__tests__/Layer-test.jsx index 2803d28e26..ea12eba56b 100644 --- a/web/client/components/map/openlayers/__tests__/Layer-test.jsx +++ b/web/client/components/map/openlayers/__tests__/Layer-test.jsx @@ -30,7 +30,7 @@ import { setStore, setCredentials } from '../../../../utils/SecurityUtils'; -import ConfigUtils from '../../../../utils/ConfigUtils'; +import ConfigUtils, { setConfigProp } from '../../../../utils/ConfigUtils'; import { ServerTypes } from '../../../../utils/LayersUtils'; @@ -169,6 +169,7 @@ describe('Openlayers layer', () => { let map; beforeEach(() => { + setConfigProp('miscSettings', { experimentalInteractiveLegend: true }); mockAxios = new MockAdapter(axios); document.body.innerHTML = '
'; map = new Map({ @@ -188,6 +189,7 @@ describe('Openlayers layer', () => { }); afterEach(() => { + setConfigProp('miscSettings', { }); mockAxios.restore(); map.setTarget(null); document.body.innerHTML = ''; @@ -1502,7 +1504,65 @@ describe('Openlayers layer', () => { // count layers expect(map.getLayers().getLength()).toBe(1); }); + it('creates a vector layer for openlayers map with interactive legend filter', () => { + const options = { + type: 'vector', + features: [ + { type: 'Feature', properties: { "prop1": 0 }, geometry: { type: 'Point', coordinates: [0, 0] } }, + { type: 'Feature', properties: { "prop1": 2 }, geometry: { type: 'Point', coordinates: [1, 0] } }, + { type: 'Feature', properties: { "prop1": 5 }, geometry: { type: 'Point', coordinates: [2, 0] } } + ], + title: 'Title', + visibility: true, + bbox: { + crs: 'EPSG:4326', + bounds: { + minx: -180, + miny: -90, + maxx: 180, + maxy: 90 + } + }, + enableInteractiveLegend: true, + layerFilter: { + filters: [{ + "id": "interactiveLegend", + "format": "logic", + "version": "1.0.0", + "logic": "OR", + "filters": [ + { + "format": "geostyler", + "version": "1.0.0", + "body": [ + "&&", + [ + ">", + "prop1", + "0" + ], [ + "<", + "prop1", + "3" + ] + ], + "id": "&&,>,prop1,0,<,prop1,3" + } + ] + }] + } + }; + // create layers + var layer = ReactDOM.render( + , document.getElementById("container")); + expect(layer).toBeTruthy(); + // count layers + const renderedFeatsNum = map.getLayers().getLength(); + const filteredFeatsNum = 1; + expect(renderedFeatsNum).toEqual(filteredFeatsNum); + }); it('change layer visibility for Google Layer', () => { var google = { maps: { diff --git a/web/client/plugins/Map.jsx b/web/client/plugins/Map.jsx index 31e5bae4fb..d66ecd7e27 100644 --- a/web/client/plugins/Map.jsx +++ b/web/client/plugins/Map.jsx @@ -27,7 +27,7 @@ import additionalLayersReducer from "../reducers/additionallayers"; import mapEpics from "../epics/map"; import pluginsCreator from "./map/index"; import withScalesDenominators from "../components/map/enhancers/withScalesDenominators"; -import { createFeatureFilter } from '../utils/FilterUtils'; +import { createVectorFeatureFilter } from '../utils/FilterUtils'; import ErrorPanel from '../components/map/ErrorPanel'; import catalog from "../epics/catalog"; import backgroundSelector from "../epics/backgroundselector"; @@ -343,7 +343,8 @@ class MapPlugin extends React.Component { renderLayerContent = (layer, projection) => { const plugins = this.state.plugins; if (layer.features) { - return layer.features.filter(createFeatureFilter(layer.filterObj)).map( (feature) => { + const vectorFeatureFilter = createVectorFeatureFilter(layer); + return layer.features.filter(vectorFeatureFilter).map((feature) => { return ( { import { get, isNil, isArray, find, findIndex, isString, flatten } from 'lodash'; import { INTERACTIVE_LEGEND_ID } from './LegendUtils'; +import { geoStylerStyleFilter } from './styleparser/StyleParserUtils'; import { getMiscSetting } from './ConfigUtils'; let FilterUtils; @@ -1461,7 +1462,43 @@ export function resetLayerLegendFilter(layer, reason, value) { } return !isFilterEmpty(filterObj) ? filterObj : undefined; } +/** + * filter vector layer features base on filter actions like interactive legend filters + * @param {object} layer layer object configuration + * @return {func} a filter function that accepts a GeoJSON feature as argument + */ +export const createVectorFeatureFilter = (layer) => { + return (feature) => { + let isLayerHasFilterObj = layer.filterObj; + // filter introduced as support for the feature grid editor + if (isLayerHasFilterObj) { + let isFeatureMatchFilter = createFeatureFilter(layer.filterObj)(feature); + if (!isFeatureMatchFilter) return false; + } + // Check for interactive egend filter + // TODO: we can add other filters here as well + const isLayerHasInteractiveLegend = layer?.enableInteractiveLegend; + const experimentalInteractiveLegend = getMiscSetting('experimentalInteractiveLegend', false); + + if ((isLayerHasInteractiveLegend && experimentalInteractiveLegend)) { + const isLegendFilterExist = layer?.layerFilter?.filters?.find(f => f.id === INTERACTIVE_LEGEND_ID); + const isInteractiveLegendFiltersExist = isLegendFilterExist?.filters?.length; + const isLayerFilterDisabled = layer?.layerFilter?.disabled; + + if (!isInteractiveLegendFiltersExist || isLayerFilterDisabled) { + return true; + } + let legendFilters = isLegendFilterExist; + if (legendFilters?.logic === 'OR') { + return legendFilters.filters.some(filterRule => geoStylerStyleFilter(feature, filterRule.body)); + } else if (legendFilters?.logic === 'AND') { + return legendFilters.filters.every(filterRule => geoStylerStyleFilter(feature, filterRule.body)); + } + } + return true; + }; +}; FilterUtils = { processOGCFilterGroup, processOGCFilterFields, diff --git a/web/client/utils/__tests__/FilterUtils-test.js b/web/client/utils/__tests__/FilterUtils-test.js index 170bd87f9d..ff8e0d0cc8 100644 --- a/web/client/utils/__tests__/FilterUtils-test.js +++ b/web/client/utils/__tests__/FilterUtils-test.js @@ -32,12 +32,22 @@ import { isFilterEmpty, updateLayerLegendFilter, resetLayerLegendFilter, - updateLayerWFSVectorLegendFilter + updateLayerWFSVectorLegendFilter, + createVectorFeatureFilter } from '../FilterUtils'; import { INTERACTIVE_LEGEND_ID } from '../LegendUtils'; +import { setConfigProp } from '../ConfigUtils'; describe('FilterUtils', () => { + beforeEach((done) => { + setConfigProp('miscSettings', { experimentalInteractiveLegend: true }); + setTimeout(done); + }); + afterEach((done) => { + setConfigProp('miscSettings', { }); + setTimeout(done); + }); it('Calculate OGC filter', () => { let filterObj = { filterFields: [{ @@ -2597,4 +2607,62 @@ describe('FilterUtils', () => { // check if there is no any filters --> updatedFilterObj will be undefined expect(updatedFilterObj).toBeFalsy(); }); + it('test createVectorFeatureFilter for vector layers', () => { + 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 layerOptions = { + enableInteractiveLegend: true, + layerFilter: layerFilterObj, + style: "style_01" + }; + const filterFunction = createVectorFeatureFilter(layerOptions); + const isFiltered1 = filterFunction({ + properties: { + "FIELD_01": 2550 // matched with the filter rule + }, geometry: {} + }); + const isFiltered2 = filterFunction({ + properties: { + "FIELD_01": 1500 // not matched the filter rule + }, geometry: {} + }); + expect(isFiltered1).toBeTruthy(); + expect(isFiltered2).toBeFalsy(); + }); }); diff --git a/web/client/utils/cesium/GeoJSONStyledFeatures.js b/web/client/utils/cesium/GeoJSONStyledFeatures.js index 541168c1bb..16eca71c07 100644 --- a/web/client/utils/cesium/GeoJSONStyledFeatures.js +++ b/web/client/utils/cesium/GeoJSONStyledFeatures.js @@ -65,6 +65,7 @@ const featureToCartesianPositions = (feature) => { * @param {boolean} options.queryable if false the features will not be queryable, default is true * @param {array} options.features array of valid geojson features * @param {boolean} options.mergePolygonFeatures if true will merge all polygons with similar styles in a single primitive. This could help to reduce the draw call to the render + * @param {func} featureFilter a function to filter feature, it receives a GeoJSON feature as argument and it must return a boolean */ class GeoJSONStyledFeatures { constructor(options = {}) { @@ -81,6 +82,7 @@ class GeoJSONStyledFeatures { this._opacity = options.opacity ?? 1; this._queryable = options.queryable === undefined ? true : !!options.queryable; this._mergePolygonFeatures = !!options?.mergePolygonFeatures; + this._featureFilter = options.featureFilter; this._dataSource.entities.collectionChanged.addEventListener(() => { setTimeout(() => this._map.scene.requestRender(), 300); }); @@ -365,7 +367,7 @@ class GeoJSONStyledFeatures { this._styleFunction({ map: this._map, opacity: this._opacity, - features: this._features, + features: this._featureFilter ? this._features.filter(this._featureFilter) : this._features, getPreviousStyledFeature: (styledFeature) => { const editingStyleFeature = this._styledFeatures.find(({ id }) => id === styledFeature.id); return editingStyleFeature; @@ -413,6 +415,10 @@ class GeoJSONStyledFeatures { this._styleFunction = styleFunction; this._update(); } + setFeatureFilter(featureFilter) { + this._featureFilter = featureFilter; + this._update(); + } destroy() { this._primitives.removeAll(); this._map.scene.primitives.remove(this._primitives);