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/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; +};