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 0000000000..eff0e2207d
Binary files /dev/null and b/docs/user-guide/img/attributes-table/restricted_area_icon.png differ
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..bae1b34b51 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,15 @@ 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 => {
+ 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 +277,7 @@ const createLoadPageFlow = (store) => ({page, size, reason} = {}) => {
wfsURL(state),
addPagination({
...(wfsFilter(state)),
- ...viewportFilter(state)
+ ...additionnalGridFilters(state)
},
getPagination(state, {page, size})
),
@@ -298,7 +313,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) => {
@@ -1283,3 +1298,34 @@ 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(() => {
+ 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(rawAsGeoJson(result)),
+ changePage(0)
+ );
+ });
+ }
+ return Rx.Observable.of(
+ setRestrictedArea(rawAsGeoJson(src.raw) || {}),
+ changePage(0)
+ );
+ });
+
+export const resetRestrictedArea = (action$, store) =>
+ action$.ofType(LOGOUT, CLOSE_FEATURE_GRID)
+ .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 0f141132e2..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,
@@ -175,7 +183,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/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/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..fd3e671d91 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:
@@ -441,6 +444,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..05c80bc6f7 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);
@@ -209,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) => {
@@ -221,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),
@@ -229,9 +237,52 @@ export const viewportFilter = createShallowSelectorCreator(isEqual)(
},
attribute: attribute,
method: "Rectangle",
- operation: "INTERSECTS"
+ operation: "INTERSECTS",
+ viewport: true
+ }
+ ]
+ } : {};
+ }
+);
+
+export const restrictedAreaFilter = createShallowSelectorCreator(isEqual)(
+ restrictedAreaSelector,
+ spatialFieldFilters,
+ viewportFilter,
+ projectionSelector,
+ describeSelector,
+ state => restrictedAreaOperatorSelector(state),
+ (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;
+ }
+ return !isEmpty(restrictedArea) ? {
+ spatialField: [
+ ...existingFilter,
+ {
+ geometry: {
+ ...restrictedArea,
+ projection: "EPSG:4326"
+ },
+ attribute: attribute,
+ method: "Polygon",
+ operation: operator || "CONTAINS",
+ restrictedArea: true
}
]
} : {};
}
);
+
+/**
+ * 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 || [];
+ 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;
+};