From 3925cf0ced0a15f86463c4f2194e7fa0df9e2a6f Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 21 Aug 2024 10:01:10 +0200 Subject: [PATCH] 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; +};