From 464099dad3d66b47d6a36e3d7feb51aa4bc39f72 Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Tue, 6 Aug 2024 10:03:50 +0200 Subject: [PATCH 1/3] Config, button, reux, filter for restricted area --- web/client/actions/featuregrid.js | 6 ++ .../data/featuregrid/FeatureGrid.jsx | 6 ++ .../data/featuregrid/toolbars/Toolbar.jsx | 23 +++++++- web/client/epics/featuregrid.js | 58 +++++++++++++++++-- web/client/plugins/FeatureEditor.jsx | 4 +- .../plugins/featuregrid/panels/index.jsx | 4 +- web/client/reducers/featuregrid.js | 10 +++- web/client/selectors/featuregrid.js | 38 +++++++++++- 8 files changed, 137 insertions(+), 12 deletions(-) diff --git a/web/client/actions/featuregrid.js b/web/client/actions/featuregrid.js index ec62c14b6c..c6e7e10ede 100644 --- a/web/client/actions/featuregrid.js +++ b/web/client/actions/featuregrid.js @@ -7,6 +7,7 @@ */ export const SET_UP = 'FEATUREGRID:SET_UP'; +export const SET_RESTRICTED_AREA = "FEATUREGRID:SET_RESTRICTED_AREA"; export const SELECT_FEATURES = 'FEATUREGRID:SELECT_FEATURES'; export const DESELECT_FEATURES = 'FEATUREGRID:DESELECT_FEATURES'; export const CLEAR_SELECTION = 'FEATUREGRID:CLEAR_SELECTION'; @@ -404,3 +405,8 @@ export const setViewportFilter = (viewportFilter) => ({ type: SET_VIEWPORT_FILTER, value: viewportFilter }); + +export const setRestrictedArea = (area) => ({ + type: SET_RESTRICTED_AREA, + area: area +}); diff --git a/web/client/components/data/featuregrid/FeatureGrid.jsx b/web/client/components/data/featuregrid/FeatureGrid.jsx index 19168b7e20..4448462b22 100644 --- a/web/client/components/data/featuregrid/FeatureGrid.jsx +++ b/web/client/components/data/featuregrid/FeatureGrid.jsx @@ -33,6 +33,9 @@ class FeatureGrid extends React.PureComponent { static propTypes = { autocompleteEnabled: PropTypes.bool, editingAllowedRoles: PropTypes.array, + restrictedArea: PropTypes.object, + restrictedAreaUrl: PropTypes.string, + restrictedAreaOperator: PropTypes.string, gridOpts: PropTypes.object, changes: PropTypes.object, selectBy: PropTypes.object, @@ -56,6 +59,9 @@ class FeatureGrid extends React.PureComponent { }; static defaultProps = { editingAllowedRoles: ["ADMIN"], + restrictedArea: {}, + restrictedAreaUrl: "", + restrictedAreaOperator: "CONTAINS", autocompleteEnabled: false, gridComponent: AdaptiveGrid, changes: {}, diff --git a/web/client/components/data/featuregrid/toolbars/Toolbar.jsx b/web/client/components/data/featuregrid/toolbars/Toolbar.jsx index 47a5b15e19..dda9c9416c 100644 --- a/web/client/components/data/featuregrid/toolbars/Toolbar.jsx +++ b/web/client/components/data/featuregrid/toolbars/Toolbar.jsx @@ -1,7 +1,7 @@ import React from 'react'; import './toolbar.css'; -import { sortBy } from 'lodash'; -import {ButtonGroup, Checkbox, Glyphicon, FormControl, FormGroup, Col} from 'react-bootstrap'; +import { sortBy, isEmpty } from 'lodash'; +import {ButtonGroup, Checkbox, Glyphicon, FormControl, FormGroup, Col, Button} from 'react-bootstrap'; import Message from '../../../I18N/Message'; import withHint from '../enhancers/withHint'; @@ -35,7 +35,23 @@ const standardButtons = { disabled={disabled} visible={mode === "VIEW" && isEditingAllowed && areLayerFeaturesEditable(layer)} onClick={events.switchEditMode} - glyph="pencil"/>), + glyph="pencil" />), + isRestrictedByArea: ({ restrictedArea }) => { + return + }, filter: ({isFilterActive = false, viewportFilter, disabled, isSearchAllowed, mode, showAdvancedFilterButton = true, events = {}}) => ( { const defaultFeatureProj = getDefaultFeatureProjection(); @@ -205,7 +212,16 @@ const setupDrawSupport = (state, original) => { }); // Remove features with geometry null or id "empty_row" - const cleanFeatures = features.filter(ft => ft.geometry !== null || ft.id !== 'empty_row'); + const cleanFeatures = features.filter(ft => { + console.log("clean features"); + const restrictedArea = restrictedAreaSelector(state); + let isValidFeature = ft.geometry !== null || ft.id !== 'empty_row'; + if (isValidFeature && !isEmpty(restrictedArea)) { + // allow only feature inside restricted area + isValidFeature = booleanIntersects(restrictedArea, ft.geometry); + } + return isValidFeature + }); if (cleanFeatures.length > 0) { return Rx.Observable.from([ @@ -262,7 +278,7 @@ const createLoadPageFlow = (store) => ({page, size, reason} = {}) => { wfsURL(state), addPagination({ ...(wfsFilter(state)), - ...viewportFilter(state) + ...additionnalGridFilters(state) }, getPagination(state, {page, size}) ), @@ -487,6 +503,7 @@ export const enableGeometryFilterOnEditMode = (action$, store) => action$.ofType(TOGGLE_MODE) .filter(() => modeSelector(store.getState()) === MODES.EDIT) .switchMap(() => { + console.log("enableGeometryFilterOnEditMode") const currentFilter = find(getAttributeFilters(store.getState()), f => f.type === 'geometry') || {}; return currentFilter.value ? Rx.Observable.empty() : Rx.Observable.of(updateFilter({ attribute: findGeometryProperty(describeSelector(store.getState())).name, @@ -1283,3 +1300,36 @@ export const resetViewportFilter = (action$, store) => return viewportFilter(store.getState()) !== null ? Rx.Observable.of(setViewportFilter(null)) : Rx.Observable.empty(); }); + + export const requestRestrictedArea = (action$, store) => + action$.ofType(OPEN_FEATURE_GRID, LOGIN_SUCCESS) + .filter(() => + { + return !isAdminUserSelector(store.getState()) + && isLoggedIn(store.getState()) + && !isEmpty(restrictedAreaSrcSelector(store.getState()))} + ) + .switchMap((action) => { + const src = restrictedAreaSrcSelector(store.getState()); + if (src.url) { + return Rx.Observable.defer(() => fetch(src?.url).then(r => r?.json?.())) + .switchMap(result => { + return Rx.Observable.of( + setRestrictedArea(result), + changePage(0) + ) + }) + } else { + return Rx.Observable.of( + setRestrictedArea(src?.raw || {}), + changePage(0) + ) + } + }) + +export const resetRestrictedArea = (action$, store) => + action$.ofType(LOGOUT, CLOSE_FEATURE_GRID) + .filter((a) => !isEmpty(restrictedAreaSrcSelector(store.getState()))) + .switchMap(() => Rx.Observable.of( + setRestrictedArea({}) + )) diff --git a/web/client/plugins/FeatureEditor.jsx b/web/client/plugins/FeatureEditor.jsx index 0f141132e2..78ce960d7a 100644 --- a/web/client/plugins/FeatureEditor.jsx +++ b/web/client/plugins/FeatureEditor.jsx @@ -175,7 +175,9 @@ const EditorPlugin = connect( virtualScroll: this.props.virtualScroll ?? true, editingAllowedRoles: this.props.editingAllowedRoles, editingAllowedGroups: this.props.editingAllowedGroups, - maxStoredPages: this.props.maxStoredPages + maxStoredPages: this.props.maxStoredPages, + restrictedAreaUrl: this.props.restrictedAreaUrl, + restrictedArea: this.props.restrictedArea }); }, componentDidUpdate(prevProps) { diff --git a/web/client/plugins/featuregrid/panels/index.jsx b/web/client/plugins/featuregrid/panels/index.jsx index d07fc5af1b..8b5b2ef6a4 100644 --- a/web/client/plugins/featuregrid/panels/index.jsx +++ b/web/client/plugins/featuregrid/panels/index.jsx @@ -47,7 +47,8 @@ import { timeSyncActive, isViewportFilterActive, isFilterByViewportSupported, - selectedLayerSelector + selectedLayerSelector, + restrictedAreaSelector } from '../../../selectors/featuregrid'; import { mapLayoutValuesSelector } from '../../../selectors/maplayout'; import {isCesium, mapTypeSelector} from '../../../selectors/maptype'; @@ -95,6 +96,7 @@ const Toolbar = connect( disableZoomAll: (state) => state && state.featuregrid.virtualScroll || featureCollectionResultSelector(state).features.length === 0, isSearchAllowed: (state) => !isCesium(state), isEditingAllowed: isEditingAllowedSelector, + restrictedArea: restrictedAreaSelector, hasSupportedGeometry, isFilterActive, showTimeSyncButton: showTimeSync, diff --git a/web/client/reducers/featuregrid.js b/web/client/reducers/featuregrid.js index 4ecbd3438f..5d27311f5f 100644 --- a/web/client/reducers/featuregrid.js +++ b/web/client/reducers/featuregrid.js @@ -47,7 +47,8 @@ import { SET_TIME_SYNC, UPDATE_EDITORS_OPTIONS, SET_PAGINATION, - SET_VIEWPORT_FILTER + SET_VIEWPORT_FILTER, + SET_RESTRICTED_AREA, } from '../actions/featuregrid'; import { MAP_CONFIG_LOADED } from '../actions/config'; @@ -156,7 +157,9 @@ function featuregrid(state = emptyResultsState, action) { editingAllowedRoles: action.options.editingAllowedRoles || state.editingAllowedRoles || ["ADMIN"], editingAllowedGroups: action.options.editingAllowedGroups || state.editingAllowedGroups || [], virtualScroll: !!action.options.virtualScroll, - maxStoredPages: action.options.maxStoredPages || 5 + maxStoredPages: action.options.maxStoredPages || 5, + restrictedAreaUrl: action.options.restrictedAreaUrl || "", + restrictedArea: action.options.restrictedArea || {} }); } case LOAD_MORE_FEATURES: @@ -440,6 +443,9 @@ function featuregrid(state = emptyResultsState, action) { } case MAP_CONFIG_LOADED: { return {...state, ...get(action, 'config.featureGrid', {})}; + } + case SET_RESTRICTED_AREA: { + return { ...state, restrictedArea: { ...state.restrictedArea, geometry: action.area } }; } default: return state; diff --git a/web/client/selectors/featuregrid.js b/web/client/selectors/featuregrid.js index 8f460fa483..e7ba0342ea 100644 --- a/web/client/selectors/featuregrid.js +++ b/web/client/selectors/featuregrid.js @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -import { head, get, isObject } from 'lodash'; +import { head, get, isObject, isEmpty } from 'lodash'; import { getLayerFromId } from './layers'; import { findGeometryProperty } from '../utils/ogc/WFS/base'; @@ -202,6 +202,11 @@ export const isEditingAllowedSelector = (state) => { })(state); return (canEdit || isAllowed) && !isCesium(state); }; + +export const restrictedAreaSrcSelector = state => get(state, "featuregrid.restrictedArea"); +export const restrictedAreaOperatorSelector = state => get(state, "featuregrid.restrictedArea.operator"); +export const restrictedAreaSelector = state => get(state, "featuregrid.restrictedArea.geometry"); + export const paginationSelector = state => get(state, "featuregrid.pagination"); export const useLayerFilterSelector = state => get(state, "featuregrid.useLayerFilter", true); @@ -235,3 +240,34 @@ export const viewportFilter = createShallowSelectorCreator(isEqual)( } : {}; } ); + +export const restrictedAreaFilter = createShallowSelectorCreator(isEqual)( + restrictedAreaSelector, + projectionSelector, + describeSelector, + state => restrictedAreaOperatorSelector(state), + (restrictedArea, projection, describeLayer, operator) => { + const attribute = findGeometryProperty(describeLayer)?.name; + console.log(restrictedArea); + return !isEmpty(restrictedArea) ? { + spatialField: [ + { + geometry: { + ...restrictedArea, + projection: "EPSG:4326" + }, + attribute: attribute, + method: "Polygon", + operation: operator || "CONTAINS", + restrictedArea: true + } + ] + } : {}; + } +) + +export const additionnalGridFilters = (state) => { + const restrictedArea = restrictedAreaFilter(state)?.spatialField || []; + const viewport = viewportFilter(state)?.spatialField || []; + return {spatialField: [...restrictedArea, ...viewport]} +} From 9f2ad4bb48797354cb58011924b966dd29cd8f70 Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 21 Aug 2024 09:01:56 +0200 Subject: [PATCH 2/3] keep existing filters without viewport --- web/client/epics/featuregrid.js | 2 +- web/client/selectors/featuregrid.js | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/web/client/epics/featuregrid.js b/web/client/epics/featuregrid.js index 9b4500fc4d..10bb4acb8a 100644 --- a/web/client/epics/featuregrid.js +++ b/web/client/epics/featuregrid.js @@ -314,7 +314,7 @@ const updateFilterFunc = (store) => ({update = {}, append} = {}) => { // If an advanced filter is present it's filterFields should be composed with the action' const {id} = selectedLayerSelector(store.getState()); const filterObj = {...get(store.getState(), `featuregrid.advancedFilters["${id}"]`)}; - if (filterObj) { + if (filterObj && !isEmpty(filterObj)) { // TODO: make append with advanced filters work const attributesFilter = getAttributeFilters(store.getState()) || {}; const columnsFilters = reduce(attributesFilter, (cFilters, value, attribute) => { diff --git a/web/client/selectors/featuregrid.js b/web/client/selectors/featuregrid.js index e7ba0342ea..1596f11a67 100644 --- a/web/client/selectors/featuregrid.js +++ b/web/client/selectors/featuregrid.js @@ -214,11 +214,13 @@ export const isViewportFilterActive = state => get(state, 'featuregrid.viewportF export const isFilterByViewportSupported = state => mapTypeSelector(state) !== MapLibraries.CESIUM; +export const spatialFieldFilters = state => get(state, 'query.filterObj.spatialField'); + export const viewportFilter = createShallowSelectorCreator(isEqual)( isViewportFilterActive, mapBboxSelector, projectionSelector, - state => get(state, 'query.filterObj.spatialField'), + spatialFieldFilters, describeSelector, isFilterByViewportSupported, (viewportFilterIsActive, box, projection, spatialField = [], describeLayer, viewportFilterIsSupported) => { @@ -226,7 +228,8 @@ export const viewportFilter = createShallowSelectorCreator(isEqual)( const existingFilter = spatialField?.operation ? [spatialField] : spatialField; return viewportFilterIsActive && viewportFilterIsSupported ? { spatialField: [ - ...existingFilter, + // avoid restricted area filter dupplication + ...existingFilter.filter(f => !f.viewport && !f.restrictedArea), { geometry: { ...bboxToFeatureGeometry(box.bounds), @@ -234,7 +237,8 @@ export const viewportFilter = createShallowSelectorCreator(isEqual)( }, attribute: attribute, method: "Rectangle", - operation: "INTERSECTS" + operation: "INTERSECTS", + viewport: true } ] } : {}; @@ -243,14 +247,21 @@ export const viewportFilter = createShallowSelectorCreator(isEqual)( export const restrictedAreaFilter = createShallowSelectorCreator(isEqual)( restrictedAreaSelector, + spatialFieldFilters, + viewportFilter, projectionSelector, describeSelector, state => restrictedAreaOperatorSelector(state), - (restrictedArea, projection, describeLayer, operator) => { + (restrictedArea, spatialField = [], viewportFilter, projection, describeLayer, operator) => { const attribute = findGeometryProperty(describeLayer)?.name; - console.log(restrictedArea); + let existingFilter = []; + // if activate, viewportFilter already get existing filter + if(isEmpty(viewportFilter) && !isEmpty(spatialField)) { + existingFilter = spatialField?.operation ? [spatialField] : spatialField + } return !isEmpty(restrictedArea) ? { spatialField: [ + ...existingFilter, { geometry: { ...restrictedArea, @@ -266,6 +277,10 @@ export const restrictedAreaFilter = createShallowSelectorCreator(isEqual)( } ) +/** + * Create spatialField filters array. + * Contains filters from viewportFilter, restrictedArea, exsting WFS filter + */ export const additionnalGridFilters = (state) => { const restrictedArea = restrictedAreaFilter(state)?.spatialField || []; const viewport = viewportFilter(state)?.spatialField || []; From 3925cf0ced0a15f86463c4f2194e7fa0df9e2a6f Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 21 Aug 2024 10:01:10 +0200 Subject: [PATCH 3/3] Change restricted area setter restricted area documentation Lint and clean --- docs/user-guide/attributes-table.md | 8 ++++ .../attributes-table/restricted_area_icon.png | Bin 0 -> 4443 bytes .../data/featuregrid/toolbars/Toolbar.jsx | 16 +++---- web/client/epics/featuregrid.js | 44 ++++++++---------- web/client/plugins/FeatureEditor.jsx | 8 ++++ .../plugins/featuregrid/FeatureEditor.jsx | 8 ++++ web/client/reducers/featuregrid.js | 4 +- web/client/selectors/featuregrid.js | 12 ++--- web/client/utils/FeatureGridUtils.js | 37 +++++++++++++++ 9 files changed, 97 insertions(+), 40 deletions(-) create mode 100644 docs/user-guide/img/attributes-table/restricted_area_icon.png diff --git a/docs/user-guide/attributes-table.md b/docs/user-guide/attributes-table.md index de20b5df32..1c60c826f1 100644 --- a/docs/user-guide/attributes-table.md +++ b/docs/user-guide/attributes-table.md @@ -228,3 +228,11 @@ With a click on the button: + +## Restriction by area + +MapStore [allows to configure](https://mapstore.geosolutionsgroup.com/mapstore/docs/api/plugins#plugins.FeatureEditor) attribute table in order to limit features consultation by a geometric area. + +Note that this restriction is never active for adminstrators. If active, the user see an icon to the left of the attribute table toolbar : + + diff --git a/docs/user-guide/img/attributes-table/restricted_area_icon.png b/docs/user-guide/img/attributes-table/restricted_area_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..eff0e2207d433d36d867e284f14c48f1e8b8d14c GIT binary patch literal 4443 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 literal 0 HcmV?d00001 diff --git a/web/client/components/data/featuregrid/toolbars/Toolbar.jsx b/web/client/components/data/featuregrid/toolbars/Toolbar.jsx index dda9c9416c..bae1b34b51 100644 --- a/web/client/components/data/featuregrid/toolbars/Toolbar.jsx +++ b/web/client/components/data/featuregrid/toolbars/Toolbar.jsx @@ -37,7 +37,7 @@ const standardButtons = { onClick={events.switchEditMode} glyph="pencil" />), isRestrictedByArea: ({ restrictedArea }) => { - return - }, + width: 0, + padding: 0, + borderWidth: 0 + } : {}}> + + ); + }, filter: ({isFilterActive = false, viewportFilter, disabled, isSearchAllowed, mode, showAdvancedFilterButton = true, events = {}}) => ( { // Remove features with geometry null or id "empty_row" const cleanFeatures = features.filter(ft => { - console.log("clean features"); const restrictedArea = restrictedAreaSelector(state); let isValidFeature = ft.geometry !== null || ft.id !== 'empty_row'; if (isValidFeature && !isEmpty(restrictedArea)) { // allow only feature inside restricted area isValidFeature = booleanIntersects(restrictedArea, ft.geometry); } - return isValidFeature + return isValidFeature; }); if (cleanFeatures.length > 0) { @@ -503,7 +502,6 @@ export const enableGeometryFilterOnEditMode = (action$, store) => action$.ofType(TOGGLE_MODE) .filter(() => modeSelector(store.getState()) === MODES.EDIT) .switchMap(() => { - console.log("enableGeometryFilterOnEditMode") const currentFilter = find(getAttributeFilters(store.getState()), f => f.type === 'geometry') || {}; return currentFilter.value ? Rx.Observable.empty() : Rx.Observable.of(updateFilter({ attribute: findGeometryProperty(describeSelector(store.getState())).name, @@ -1301,35 +1299,33 @@ export const resetViewportFilter = (action$, store) => : Rx.Observable.empty(); }); - export const requestRestrictedArea = (action$, store) => +export const requestRestrictedArea = (action$, store) => action$.ofType(OPEN_FEATURE_GRID, LOGIN_SUCCESS) - .filter(() => - { + .filter(() => { return !isAdminUserSelector(store.getState()) - && isLoggedIn(store.getState()) - && !isEmpty(restrictedAreaSrcSelector(store.getState()))} - ) - .switchMap((action) => { + && isLoggedIn(store.getState()) + && !isEmpty(restrictedAreaSrcSelector(store.getState())); + }) + .switchMap(() => { const src = restrictedAreaSrcSelector(store.getState()); if (src.url) { return Rx.Observable.defer(() => fetch(src?.url).then(r => r?.json?.())) .switchMap(result => { return Rx.Observable.of( - setRestrictedArea(result), + setRestrictedArea(rawAsGeoJson(result)), changePage(0) - ) - }) - } else { - return Rx.Observable.of( - setRestrictedArea(src?.raw || {}), - changePage(0) - ) + ); + }); } - }) + return Rx.Observable.of( + setRestrictedArea(rawAsGeoJson(src.raw) || {}), + changePage(0) + ); + }); export const resetRestrictedArea = (action$, store) => action$.ofType(LOGOUT, CLOSE_FEATURE_GRID) - .filter((a) => !isEmpty(restrictedAreaSrcSelector(store.getState()))) + .filter(() => !isEmpty(restrictedAreaSrcSelector(store.getState()))) .switchMap(() => Rx.Observable.of( setRestrictedArea({}) - )) + )); diff --git a/web/client/plugins/FeatureEditor.jsx b/web/client/plugins/FeatureEditor.jsx index 78ce960d7a..4231efb330 100644 --- a/web/client/plugins/FeatureEditor.jsx +++ b/web/client/plugins/FeatureEditor.jsx @@ -79,6 +79,9 @@ import {isViewportFilterActive} from "../selectors/featuregrid"; * @prop {array} cfg.showFilterByViewportTool Show button to toggle filter by viewport in toolbar. * @prop {object} cfg.dateFormats Allows to specify custom date formats ( in [ISO_8601](https://en.wikipedia.org/wiki/ISO_8601) format) to use to display dates in the table. `date` `date-time` and `time` are the supported entries for the date format. Example: * @prop {boolean} cfg.showPopoverSync default false. Hide the popup of map sync if false, shows the popup of map sync if true + * @prop {string} cfg.restrictedArea.url Geometry definition as WKT or GeoJSON loaded from URL or path. + * @prop {string} cfg.restrictedArea.raw Geometry definition as WKT or GeoJSON. + * @prop {string} cfg.restrictedArea.operator Spatial operation to performed between features and the given geometry. * ``` * "dateFormats": { * "date-time": "MM DD YYYY - HH:mm:ss", @@ -114,6 +117,11 @@ import {isViewportFilterActive} from "../selectors/featuregrid"; * }, * "editingAllowedRoles": ["ADMIN"], * "snapTool": true, + * "restrictedArea": { + * "url": "/wkt_or_geojson_geometry", + * "raw": "POLYGON ((-64.8 32.3, -65.5 18.3, -80.3 25.2, -64.8 32.3))", + * "operator": "WITHIN" + * }, * "snapConfig": { * "vertex": true, * "edge": true, diff --git a/web/client/plugins/featuregrid/FeatureEditor.jsx b/web/client/plugins/featuregrid/FeatureEditor.jsx index 0873b815e6..2486f3aa0d 100644 --- a/web/client/plugins/featuregrid/FeatureEditor.jsx +++ b/web/client/plugins/featuregrid/FeatureEditor.jsx @@ -96,6 +96,9 @@ const Dock = connect(createSelector( * @prop {array} cfg.snapConfig.additionalLayers Array of additional layers to include into snapping layers list. Provides a way to include layers from "state.additionallayers" * @prop {object} cfg.dateFormats object containing custom formats for one of the date/time attribute types. Following keys are supported: "date-time", "date", "time" * @prop {boolean} cfg.showPopoverSync default false. Hide the popup of map sync if false, shows the popup of map sync if true + * @prop {string} cfg.restrictedArea.url Geometry definition as WKT or GeoJSON loaded from URL or path. + * @prop {string} cfg.restrictedArea.raw Geometry definition as WKT or GeoJSON. + * @prop {string} cfg.restrictedArea.operator Spatial operation to performed between features and the given geometry. * * @classdesc * `FeatureEditor` Plugin, also called *FeatureGrid*, provides functionalities to browse/edit data via WFS. The grid can be configured to use paging or @@ -124,6 +127,11 @@ const Dock = connect(createSelector( * }, * "editingAllowedRoles": ["ADMIN"], * "snapTool": true, + * "restrictedArea": { + * "url": "/wkt_or_geojson_geometry", + * "raw": "POLYGON ((-64.8 32.3, -65.5 18.3, -80.3 25.2, -64.8 32.3))", + * "operator": "WITHIN" + * }, * "snapConfig": { * "vertex": true, * "edge": true, diff --git a/web/client/reducers/featuregrid.js b/web/client/reducers/featuregrid.js index 5d27311f5f..fd3e671d91 100644 --- a/web/client/reducers/featuregrid.js +++ b/web/client/reducers/featuregrid.js @@ -48,7 +48,7 @@ import { UPDATE_EDITORS_OPTIONS, SET_PAGINATION, SET_VIEWPORT_FILTER, - SET_RESTRICTED_AREA, + SET_RESTRICTED_AREA } from '../actions/featuregrid'; import { MAP_CONFIG_LOADED } from '../actions/config'; @@ -443,7 +443,7 @@ function featuregrid(state = emptyResultsState, action) { } case MAP_CONFIG_LOADED: { return {...state, ...get(action, 'config.featureGrid', {})}; - } + } case SET_RESTRICTED_AREA: { return { ...state, restrictedArea: { ...state.restrictedArea, geometry: action.area } }; } diff --git a/web/client/selectors/featuregrid.js b/web/client/selectors/featuregrid.js index 1596f11a67..05c80bc6f7 100644 --- a/web/client/selectors/featuregrid.js +++ b/web/client/selectors/featuregrid.js @@ -252,12 +252,12 @@ export const restrictedAreaFilter = createShallowSelectorCreator(isEqual)( projectionSelector, describeSelector, state => restrictedAreaOperatorSelector(state), - (restrictedArea, spatialField = [], viewportFilter, projection, describeLayer, operator) => { + (restrictedArea, spatialField = [], viewPortFilter, projection, describeLayer, operator) => { const attribute = findGeometryProperty(describeLayer)?.name; let existingFilter = []; // if activate, viewportFilter already get existing filter - if(isEmpty(viewportFilter) && !isEmpty(spatialField)) { - existingFilter = spatialField?.operation ? [spatialField] : spatialField + if (isEmpty(viewPortFilter) && !isEmpty(spatialField)) { + existingFilter = spatialField?.operation ? [spatialField] : spatialField; } return !isEmpty(restrictedArea) ? { spatialField: [ @@ -275,7 +275,7 @@ export const restrictedAreaFilter = createShallowSelectorCreator(isEqual)( ] } : {}; } -) +); /** * Create spatialField filters array. @@ -284,5 +284,5 @@ export const restrictedAreaFilter = createShallowSelectorCreator(isEqual)( export const additionnalGridFilters = (state) => { const restrictedArea = restrictedAreaFilter(state)?.spatialField || []; const viewport = viewportFilter(state)?.spatialField || []; - return {spatialField: [...restrictedArea, ...viewport]} -} + return {spatialField: [...restrictedArea, ...viewport]}; +}; diff --git a/web/client/utils/FeatureGridUtils.js b/web/client/utils/FeatureGridUtils.js index b5406acd93..badabf7da6 100644 --- a/web/client/utils/FeatureGridUtils.js +++ b/web/client/utils/FeatureGridUtils.js @@ -17,6 +17,8 @@ import { isValidValueForPropertyName as isValidValueForPropertyNameBase } from './ogc/WFS/base'; +import { WKT } from 'ol/format'; + import { applyDefaultToLocalizedString } from '../components/I18N/LocalizedString'; const getGeometryName = (describe) => get(findGeometryProperty(describe), "name"); @@ -392,3 +394,38 @@ export const supportsFeatureEditing = (layer) => includes(supportedEditLayerType * @returns {boolean} flag */ export const areLayerFeaturesEditable = (layer) => !layer?.disableFeaturesEditing && supportsFeatureEditing(layer); + +export const isWKT = (wktString) => { + let isWKTGeom = false; + try { + const reader = new WKT(); + const feature = reader.readFeature(wktString); + if (feature) { + isWKTGeom = true; + } + } catch (e) { + isWKTGeom = false; + } + return isWKTGeom; +}; + +export const wktToGeoJson = (wktString) => { + const reader = new WKT(); + const feature = reader.readFeature(wktString); + return { + type: feature.getGeometry().getType(), + coordinates: feature.getGeometry().getCoordinates() + }; +}; + +/** + * Return GeoJSON geometry. Transform WKT to GeoJSON if necessary. + * @param {string} raw - geometry + * @returns geometry object + */ +export const rawAsGeoJson = (raw) => { + if (isWKT(raw)) { + return wktToGeoJson(raw); + } + return raw; +};