From 9849146f1040963b7bca560762749bcccf97c79f Mon Sep 17 00:00:00 2001 From: MV88 Date: Mon, 18 Sep 2023 14:12:00 +0200 Subject: [PATCH 01/21] 9028 add spatial filter to dashboard --- web/client/actions/widgets.js | 10 + .../components/data/query/QueryBuilder.jsx | 2 +- .../components/data/query/SpatialFilter.jsx | 16 +- .../widgets/builder/wizard/ChartWizard.jsx | 4 +- .../wizard/common/WPSWidgetOptions.jsx | 8 +- web/client/configs/localConfig.json | 22 +- web/client/epics/dashboard.js | 7 +- web/client/epics/widgets.js | 61 +++++- web/client/plugins/QueryPanel.jsx | 46 +++-- web/client/plugins/QueryPanelWithMap.jsx | 188 ++++++++++++++++++ .../plugins/queryPanelWithMap/MapComp.jsx | 32 +++ .../plugins/queryPanelWithMap/MapWithDraw.jsx | 58 ++++++ .../queryPanelWithMap/PanelWithMap.jsx | 180 +++++++++++++++++ .../queryPanelWithMap/SmartQueryForm.jsx | 148 ++++++++++++++ .../queryPanelWithMap/SpatialFilter.jsx | 52 +++++ .../enhancers/mapEnhancer.js | 80 ++++++++ web/client/product/plugins.js | 1 + web/client/reducers/widgets.js | 14 ++ web/client/selectors/controls.js | 1 + web/client/selectors/widgets.js | 1 + web/client/themes/default/less/dashboard.less | 25 +++ web/client/utils/CoordinatesUtils.js | 14 ++ web/client/utils/WidgetsUtils.js | 70 +++++++ 23 files changed, 994 insertions(+), 46 deletions(-) create mode 100644 web/client/plugins/QueryPanelWithMap.jsx create mode 100644 web/client/plugins/queryPanelWithMap/MapComp.jsx create mode 100644 web/client/plugins/queryPanelWithMap/MapWithDraw.jsx create mode 100644 web/client/plugins/queryPanelWithMap/PanelWithMap.jsx create mode 100644 web/client/plugins/queryPanelWithMap/SmartQueryForm.jsx create mode 100644 web/client/plugins/queryPanelWithMap/SpatialFilter.jsx create mode 100644 web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js diff --git a/web/client/actions/widgets.js b/web/client/actions/widgets.js index d214810a6b..91769914a0 100644 --- a/web/client/actions/widgets.js +++ b/web/client/actions/widgets.js @@ -12,6 +12,7 @@ export const NEW = "WIDGETS:NEW"; export const EDIT = "WIDGETS:EDIT"; export const EDIT_NEW = "WIDGETS:EDIT_NEW"; export const EDITOR_CHANGE = "WIDGETS:EDITOR_CHANGE"; +export const CHANGE_MAP_EDITOR = "WIDGETS:CHANGE_MAP_EDITOR"; export const EDITOR_SETTING_CHANGE = "WIDGETS:EDITOR_SETTING_CHANGE"; export const UPDATE = "WIDGETS:UPDATE"; export const UPDATE_PROPERTY = "WIDGETS:UPDATE_PROPERTY"; @@ -186,6 +187,15 @@ export const onEditorChange = (key, value) => ({ key, value }); +/** + * Changes the entry in the widget editor + * @param {object} mapData the new map data + * @return {object} The action of type `WIDGETS:EDITOR_CHANGE` with key and value + */ +export const changeMapEditor = (mapData) => ({ + type: CHANGE_MAP_EDITOR, + mapData +}); /** * Changes a setting of the editor (e.g. the page) diff --git a/web/client/components/data/query/QueryBuilder.jsx b/web/client/components/data/query/QueryBuilder.jsx index a316a026ee..39fb252666 100644 --- a/web/client/components/data/query/QueryBuilder.jsx +++ b/web/client/components/data/query/QueryBuilder.jsx @@ -216,7 +216,7 @@ class QueryBuilder extends React.Component { {this.renderItems('layers', { spatialOperations, spatialMethodOptions, ...toolsOptions })} {this.renderItems('end', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - :
; + :
; } filterItem = (target, layerName) => (el) => { diff --git a/web/client/components/data/query/SpatialFilter.jsx b/web/client/components/data/query/SpatialFilter.jsx index 18ae5a30ed..193576a510 100644 --- a/web/client/components/data/query/SpatialFilter.jsx +++ b/web/client/components/data/query/SpatialFilter.jsx @@ -211,15 +211,17 @@ class SpatialFilter extends React.Component { ); }; renderSpatialPanel = (operationRow, drawLabel, selectedOperation) => { + const zoneFields = this.renderZoneFields(); + const roiField = this.props.spatialField.method + && this.getMethodFromId(this.props.spatialField.method) + && this.getMethodFromId(this.props.spatialField.method).type === "wfsGeocoder" + ? this.renderRoiPanel() + : null; return ( - + {this.props.spatialMethodOptions.length > 1 ? this.renderSpatialHeader() : } - {this.renderZoneFields()} - {this.props.spatialField.method - && this.getMethodFromId(this.props.spatialField.method) - && this.getMethodFromId(this.props.spatialField.method).type === "wfsGeocoder" - ? this.renderRoiPanel() - : null} + {zoneFields} + {roiField} {this.props.spatialOperations.length > 1 ?
diff --git a/web/client/components/widgets/builder/wizard/ChartWizard.jsx b/web/client/components/widgets/builder/wizard/ChartWizard.jsx index 914a456eba..3495ea68df 100644 --- a/web/client/components/widgets/builder/wizard/ChartWizard.jsx +++ b/web/client/components/widgets/builder/wizard/ChartWizard.jsx @@ -230,7 +230,7 @@ const ChartWizard = ({ hideButtons className={"chart-options"}> {[ChartOptions, WidgetOptions].map(component => - <> + (<> {component} - + ) )} ); }; diff --git a/web/client/components/widgets/builder/wizard/common/WPSWidgetOptions.jsx b/web/client/components/widgets/builder/wizard/common/WPSWidgetOptions.jsx index cbd8ca4eba..df6a14518c 100644 --- a/web/client/components/widgets/builder/wizard/common/WPSWidgetOptions.jsx +++ b/web/client/components/widgets/builder/wizard/common/WPSWidgetOptions.jsx @@ -345,8 +345,12 @@ export default ({ /> : null} - {formOptions.advancedOptions && data.widgetType === "chart" && (data.type === "bar" || data.type === "line") - ? + {formOptions.advancedOptions && data.widgetType === "chart" + ? : null} diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 28387fa81d..6694d8af6f 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -15,6 +15,11 @@ "https://gs-stable.geosolutionsgroup.com:443/geoserver", "http://gs-stable.geosolutionsgroup.com/geoserver", "http://gs-stable.geosolutionsgroup.com:443/geoserver", + "http://gs-main.geosolutionsgroup.com/geoserver/", + "http://gs-main.geosolutionsgroup.com:443/geoserver/", + "https://gs-main.geosolutionsgroup.com/geoserver/", + "https://gs-main.geosolutionsgroup.com:443/geoserver/", + "https://geoqualif.cnr-france.com/geoserver/", "https://tile.googleapis.com" ] }, @@ -810,12 +815,23 @@ "containerPosition": "columns" } }, - { "name": "QueryPanel", + { "name": "QueryPanelWithMap", "cfg": { "toolsOptions": { - "hideCrossLayer": true, - "hideSpatialFilter": true + "hideCrossLayer": true }, + "spatialPanelExpanded": false, + "spatialOperations": [ + {"id": "INTERSECTS", "name": "queryform.spatialfilter.operations.intersects"}, + {"id": "CONTAINS", "name": "queryform.spatialfilter.operations.contains"}, + {"id": "WITHIN", "name": "queryform.spatialfilter.operations.within"} + ], + "spatialMethodOptions": [ + {"id": "Viewport", "name": "queryform.spatialfilter.methods.viewport"}, + {"id": "BBOX", "name": "queryform.spatialfilter.methods.box"}, + {"id": "Circle", "name": "queryform.spatialfilter.methods.circle"}, + {"id": "Polygon", "name": "queryform.spatialfilter.methods.poly"} + ], "containerPosition": "columns" } }, diff --git a/web/client/epics/dashboard.js b/web/client/epics/dashboard.js index eaf304b836..253d407e45 100644 --- a/web/client/epics/dashboard.js +++ b/web/client/epics/dashboard.js @@ -77,6 +77,7 @@ export const closeDashboardEditorOnExit = (action$, {getState = () => {}} = {}) /** * Manages interaction with QueryPanel and Dashboard + * [ ] EDIT ALSO HERE */ export const handleDashboardWidgetsFilterPanel = (action$, {getState = () => {}} = {}) => action$.ofType(OPEN_FILTER_EDITOR) .filter(() => isDashboardAvailable(getState())) @@ -85,12 +86,12 @@ export const handleDashboardWidgetsFilterPanel = (action$, {getState = () => {}} Rx.Observable.of( featureTypeSelected(...getFTSelectedArgs(getState())), loadFilter(getEditingWidgetFilter(getState())), - setControlProperty('queryPanel', "enabled", true) + setControlProperty('queryPanelWithMap', "enabled", true) // wait for any filter update(search) or query form close event ).concat( Rx.Observable.race( action$.ofType(QUERY_FORM_SEARCH).take(1), - action$.ofType(TOGGLE_CONTROL).filter(({control, property} = {}) => control === "queryPanel" && (!property || property === "enabled")).take(1) + action$.ofType(TOGGLE_CONTROL).filter(({control, property} = {}) => control === "queryPanelWithMap" && (!property || property === "enabled")).take(1) ) // then close the query panel, open widget form and update the current filter for the widget in editing .switchMap( action => @@ -108,7 +109,7 @@ export const handleDashboardWidgetsFilterPanel = (action$, {getState = () => {}} .merge(action$.ofType(TOGGLE_CONTROL).filter(({control, property} = {}) => control === "widgetBuilder" && (!property === false)))) .concat( Rx.Observable.of(// drawSupportReset(), - setControlProperty('queryPanel', "enabled", false) + setControlProperty('queryPanelWithMap', "enabled", false) ) ) ); diff --git a/web/client/epics/widgets.js b/web/client/epics/widgets.js index 4b6887d8b7..d9550f03be 100644 --- a/web/client/epics/widgets.js +++ b/web/client/epics/widgets.js @@ -1,5 +1,17 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ + + import Rx from 'rxjs'; import { endsWith, has, get, includes, isEqual, omit, omitBy } from 'lodash'; +import { LOCATION_CHANGE } from 'connected-react-router'; +import { saveAs } from 'file-saver'; +import converter from 'json-2-csv'; import { EXPORT_CSV, @@ -9,6 +21,7 @@ import { WIDGET_SELECTED, EDITOR_SETTING_CHANGE, onEditorChange, + changeMapEditor, updateWidgetLayer, clearWidgets, loadDependencies, @@ -22,6 +35,7 @@ import { } from '../actions/widgets'; import { MAP_CONFIG_LOADED } from '../actions/config'; +import { SET_CONTROL_PROPERTY } from '../actions/controls'; import { availableDependenciesSelector, @@ -30,18 +44,18 @@ import { getFloatingWidgets, getWidgetLayer } from '../selectors/widgets'; - import { CHANGE_LAYER_PROPERTIES, LAYER_LOAD, LAYER_ERROR } from '../actions/layers'; +import { zoomToExtent } from '../actions/map'; + import { getLayerFromId } from '../selectors/layers'; import { pathnameSelector } from '../selectors/router'; import { isDashboardEditing } from '../selectors/dashboard'; import { MAP_CREATED, SAVING_MAP, MAP_ERROR } from '../actions/maps'; import { DASHBOARD_LOADED } from '../actions/dashboard'; -import { LOCATION_CHANGE } from 'connected-react-router'; -import { saveAs } from 'file-saver'; import {downloadCanvasDataURL} from '../utils/FileUtils'; -import converter from 'json-2-csv'; -import { updateDependenciesMapOfMapList } from "../utils/WidgetsUtils"; + +import { updateDependenciesMapOfMapList, DEFAULT_MAP_SETTINGS } from "../utils/WidgetsUtils"; +import { transformExtentToArray } from "../utils/CoordinatesUtils"; const updateDependencyMap = (active, targetId, { dependenciesMap, mappings}) => { const tableDependencies = ["layer", "filter", "quickFilters", "options"]; @@ -301,7 +315,39 @@ export const onWidgetCreationFromMap = (action$, store) => const state = store.getState(); const layer = getWidgetLayer(state); if (layer) { - observable$ = Rx.Observable.of(onEditorChange('chart-layers', [layer])); + observable$ = Rx.Observable.of( + onEditorChange('chart-layers', [layer]) + ); + } + return observable$; + }); + + +export const onLayerSelectedEpic = (action$, store) => + action$.ofType(EDITOR_CHANGE) + .filter(({key}) => key === 'chart-layers' && isDashboardEditing(store.getState())) + .switchMap(() => { + let observable$ = Rx.Observable.empty(); + const state = store.getState(); + const layer = getWidgetLayer(state); + if (layer?.bbox) { + observable$ = Rx.Observable.of( + changeMapEditor({ + ...DEFAULT_MAP_SETTINGS, + bbox: layer.bbox, + center: { + crs: layer.bbox.crs, + x: (layer.bbox.bounds.maxx + layer.bbox.bounds.minx) / 2, + y: (layer.bbox.bounds.maxy + layer.bbox.bounds.miny) / 2 + } + }) + ).concat( + action$.ofType(SET_CONTROL_PROPERTY) + .filter(({control, property, value}) => control === "queryPanelWithMap" && property === "enabled" && value) + .switchMap(() => { + return Rx.Observable.of(zoomToExtent(transformExtentToArray(layer.bbox.bounds), layer.bbox.crs, 21)); + }) + ); } return observable$; }); @@ -315,5 +361,6 @@ export default { updateLayerOnLayerPropertiesChange, updateLayerOnLoadingErrorChange, updateDependenciesMapOnMapSwitch, - onWidgetCreationFromMap + onWidgetCreationFromMap, + onLayerSelectedEpic }; diff --git a/web/client/plugins/QueryPanel.jsx b/web/client/plugins/QueryPanel.jsx index c1f323b393..4ceecf74a8 100644 --- a/web/client/plugins/QueryPanel.jsx +++ b/web/client/plugins/QueryPanel.jsx @@ -76,6 +76,8 @@ import layerFilterReducers from '../reducers/layerFilter'; import queryReducers from '../reducers/query'; import queryformReducers from '../reducers/queryform'; import { isDashboardAvailable } from '../selectors/dashboard'; +// import BaseMap from '../components/map/BaseMap'; + import { groupsSelector, selectedLayerLoadingErrorSelector } from '../selectors/layers'; import { mapSelector } from '../selectors/map'; import { mapLayoutValuesSelector } from '../selectors/maplayout'; @@ -89,8 +91,6 @@ import { sortLayers, sortUsing, toggleByType } from '../utils/LayersUtils'; import Message from './locale/Message'; import {typeNameSelector} from "../selectors/query"; -// include application component - const onReset = reset.bind(null, "query"); // connecting a Dumb component to the store @@ -185,6 +185,7 @@ const SmartQueryForm = connect((state) => { const tocSelector = createSelector( [ + mapSelector, (state) => state.controls && state.controls.toolbar && state.controls.toolbar.active === 'toc', groupsSelector, (state) => state.layers && state.layers.settings, @@ -196,7 +197,8 @@ const tocSelector = createSelector( (state) => state && state.query && state.query.isLayerFilter, selectedLayerLoadingErrorSelector, typeNameSelector - ], (enabled, groups, settings, queryPanelEnabled, layoutHeight, dashboardAvailable, appliedFilter, storedFilter, advancedToolbar, loadingError, selectedLayer) => ({ + ], (map, enabled, groups, settings, queryPanelEnabled, layoutHeight, dashboardAvailable, appliedFilter, storedFilter, advancedToolbar, loadingError, selectedLayer) => ({ + map, enabled, groups, settings, @@ -214,6 +216,7 @@ class QueryPanel extends React.Component { static propTypes = { id: PropTypes.number, buttonContent: PropTypes.node, + map: PropTypes.object, groups: PropTypes.array, settings: PropTypes.object, queryPanelEnabled: PropTypes.bool, @@ -284,10 +287,16 @@ class QueryPanel extends React.Component { this.props.onInit(); } } + onToggle = () => { + if (this.props.advancedToolbar && !isEqual(this.props.appliedFilter, this.props.storedFilter)) { + this.setState(() => ({showModal: true})); + } else { + this.props.onToggleQuery(); + } + } getNoBackgroundLayers = (group) => { return group.name !== 'background'; }; - renderSidebar = () => { return ( ); }; - onToggle = () => { - if (this.props.advancedToolbar && !isEqual(this.props.appliedFilter, this.props.storedFilter)) { - this.setState(() => ({showModal: true})); - } else { - this.props.onToggleQuery(); - } - } - restoreAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onRestoreFilter(); - this.props.onToggleQuery(); - } - storeAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onSaveFilter(); - this.props.onToggleQuery(); - } renderQueryPanel = () => { return (
+
{ + this.setState(() => ({showModal: false})); + this.props.onRestoreFilter(); + this.props.onToggleQuery(); + } + storeAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onSaveFilter(); + this.props.onToggleQuery(); + } } /** diff --git a/web/client/plugins/QueryPanelWithMap.jsx b/web/client/plugins/QueryPanelWithMap.jsx new file mode 100644 index 0000000000..60e4605bd3 --- /dev/null +++ b/web/client/plugins/QueryPanelWithMap.jsx @@ -0,0 +1,188 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { toggleControl } from '../actions/controls'; +import { discardCurrentFilter, storeCurrentFilter } from '../actions/layerFilter'; +import { initQueryPanel } from '../actions/wfsquery'; +import PanelWithMap from './queryPanelWithMap/PanelWithMap'; +import autocompleteEpics from '../epics/autocomplete'; +import layerFilterEpics from '../epics/layerfilter'; +import queryFormEpics from '../epics/queryform'; +import { featureTypeSelectedEpic, redrawSpatialFilterEpic, viewportSelectedEpic, wfsQueryEpic } from '../epics/wfsquery'; +import layerFilterReducers from '../reducers/layerFilter'; +import queryReducers from '../reducers/query'; +import draw from '../reducers/draw'; +import mapReducers from '../reducers/map'; +import queryformReducers from '../reducers/queryform'; +import { queryPanelWithMapSelector } from '../selectors/controls'; +import { isDashboardAvailable } from '../selectors/dashboard'; +import { groupsSelector, selectedLayerLoadingErrorSelector } from '../selectors/layers'; +import { mapSelector } from '../selectors/map'; +import { mapLayoutValuesSelector } from '../selectors/maplayout'; +import { appliedFilterSelector, storedFilterSelector } from '../selectors/queryform'; +import { typeNameSelector } from "../selectors/query"; +import MapComp from './queryPanelWithMap/MapComp'; + + +/** + * @class + * @classdesc + * QueryPanelPlugin allow to query a layer in different ways, using attributes of that layer, spatial filters + * @name QueryPanel + * @memberof plugins + * @prop {boolean} cfg.activateQueryTool: Activate query tool options, default `false` + * @prop {object[]} cfg.spatialMethodOptions: The list of geometric methods use to create/draw the spatial filter
+ * Here you can configure a list of methods used to draw (BBOX, Circle, Polygon) or create (Viewport and wfsGeocoder types) regarding the wfsGeocoder.
The options for wfsGeocoder are: + * - id: id of the method + * - name: label used in the DropdownList + * - type: must be wfsGeocoder + * - customItemClassName: a custom class for used for this method in the DropdownList + * - geodesic: {bool} draw a geodesic geometry for filter (supported only for Circle) + * - filterProps: + * - blacklist {string[]} a list of banned words excluded from the wfs search + * - maxFeatures {number} the maximum features fetched per request + * - predicate {string} the cql predicate + * - querableAttributes {string[]} list of attributes to query on. + * - typeName {string} the workspace + layer name on geoserver + * - valueField {string} the attribute from features properties used as value/label in the autocomplete list + * - srsName {string} The projection of the requested features fetched via wfs + * Plugin acts as container and by default it have three panels: "AttributesFilter", "SpatialFilter" and "CrossLayerFilter" (see "standardItems" variable) + * Panels can be customized by injection from another plugins (see example below). + * Targets available for injection: "start", "attributes", "afterAttributes", "spatial", "afterSpatial", "layers", "end". + + * @prop {object[]} cfg.spatialOperations: The list of geometric operations use to create the spatial filter.
+ * @prop {boolean} cfg.toolsOptions.hideCrossLayer force cross layer filter panel to hide (when is not used or not usable) + * @prop {boolean} cfg.toolsOptions.hideAttributeFilter force attribute filter panel to hide (when is not used or not usable). In general any `hide${CapitailizedItemId}` works to hide a particular panel of the query panel. + * @prop {boolean} cfg.toolsOptions.hideSpatialFilter force spatial filter panel to hide (when is not used or not usable) + * + * @example + * // This example configure a layer with polygons geometry as spatial filter method + * "spatialOperations": [ + * {"id": "INTERSECTS", "name": "queryform.spatialfilter.operations.intersects"}, + * {"id": "BBOX", "name": "queryform.spatialfilter.operations.bbox"}, + * {"id": "CONTAINS", "name": "queryform.spatialfilter.operations.contains"}, + * {"id": "WITHIN", "name": "queryform.spatialfilter.operations.within"}, + * {"id": "DWITHIN", "name": "queryform.spatialfilter.operations.dwithin"} + * ], + * "spatialMethodOptions": [ + * {"id": "Viewport", "name": "queryform.spatialfilter.methods.viewport"}, + * {"id": "BBOX", "name": "queryform.spatialfilter.methods.box"}, + * {"id": "Circle", "name": "queryform.spatialfilter.methods.circle"}, + * {"id": "Polygon", "name": "queryform.spatialfilter.methods.poly"}, + * { + * "id": "methodId", + * "name": "methodName", + * "type": "wfsGeocoder", + * "url": "urlToGeoserver", + * "crossLayer": { // if this is present, allows to optimize the filter using crossLayerFilter functinalities instead of geometry. The server must support them + * "cqlTemplate": "ATTRIBUTE_Y = '${properties.ATTRIBUTE_Y}'", // a template to generate the filter from the feature properties + * "geometryName": "GEOMETRY", + * "typeName": "workspace:typeName" + * }, + * "filterProps": { + * "blacklist": [], + * "maxFeatures": 5, + * "predicate": "LIKE", + * "queriableAttributes": ["ATTRIBUTE_X"], + * "typeName": "workspace:typeName", + * "valueField": "ATTRIBUTE_Y", + * "srsName": "EPSG:3857" + * }, + * "customItemClassName": "customItemClassName" + * } + * + * @example + * // customize the QueryPanels UI via plugin(s) + * import {createPlugin} from "../utils/PluginsUtils"; + * + * export default createPlugin('QueryPanelCustomizations', { + * component: () => null, + * containers: { + * QueryPanel: [ + * // Hide the attribute filter by injecting a `component: () => null` for one of the default panels, e.g. `attributeFilter`. + * { + * id: 'attributeFilter', + * component: () => null, + * target: 'attributes', + * position: 0, + * layerNameRegex: "^gs:us_states__[0-9]*" + * }, + * // adds a panel after the attribute panel (if present) at position `0` + * { + * id: 'attributeFilterNew', + * component: () => 'Sample text', + * target: 'attributes', + * position: 0 + * }, + * { + * id: 'customPanel', + * component: () => 'Panel content; Added to attributes target', + * target: 'attributes', + * position: 3 + * }, + * { + * id: 'customPanel2', + * component: () => 'Another panel added to start', + * target: 'start', + * position: 3 + * } + * ] + * } + * }); + */ + + +const QueryPanelWithMapPlugin = connect( + createSelector( + [ + mapSelector, + (state) => state.controls && state.controls.toolbar && state.controls.toolbar.active === 'toc', + groupsSelector, + queryPanelWithMapSelector, + state => mapLayoutValuesSelector(state, {height: true}), + isDashboardAvailable, + appliedFilterSelector, + storedFilterSelector, + (state) => state && state.query && state.query.isLayerFilter, + selectedLayerLoadingErrorSelector, + typeNameSelector + ], (map, enabled, groups, queryPanelEnabled, layoutHeight, dashboardAvailable, appliedFilter, storedFilter, advancedToolbar, loadingError, selectedLayer) => ({ + map, + enabled, + groups, + queryPanelEnabled, + layout: !dashboardAvailable ? layoutHeight : {}, + mapComp: MapComp, + appliedFilter, + storedFilter, + advancedToolbar, + loadingError, + selectedLayer + }) + ), { + onToggleQuery: toggleControl.bind(null, 'queryPanelWithMap', null), + onInit: initQueryPanel, + onSaveFilter: storeCurrentFilter, + onRestoreFilter: discardCurrentFilter + })(PanelWithMap); + + +export default { + QueryPanelWithMapPlugin, + reducers: { + draw, + queryform: queryformReducers, + map: mapReducers, + query: queryReducers, + layerFilter: layerFilterReducers + }, + epics: {featureTypeSelectedEpic, wfsQueryEpic, viewportSelectedEpic, redrawSpatialFilterEpic, ...autocompleteEpics, ...queryFormEpics, ...layerFilterEpics} +}; diff --git a/web/client/plugins/queryPanelWithMap/MapComp.jsx b/web/client/plugins/queryPanelWithMap/MapComp.jsx new file mode 100644 index 0000000000..fa97667062 --- /dev/null +++ b/web/client/plugins/queryPanelWithMap/MapComp.jsx @@ -0,0 +1,32 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import MapWithDraw from './MapWithDraw'; +import { + getWidgetLayer, + getMapConfigSelector +} from '../../selectors/widgets'; + +const MapComp = connect( + createSelector([ + getWidgetLayer, + getMapConfigSelector + ], (layer, map) => { + return { + layer, + map, + mapStateSource: "wizardMap", + containerSelector: ".mapstore-query-map" + }; + } + ), {} )(MapWithDraw); + +export default MapComp; diff --git a/web/client/plugins/queryPanelWithMap/MapWithDraw.jsx b/web/client/plugins/queryPanelWithMap/MapWithDraw.jsx new file mode 100644 index 0000000000..44b801fecb --- /dev/null +++ b/web/client/plugins/queryPanelWithMap/MapWithDraw.jsx @@ -0,0 +1,58 @@ +/** +* Copyright 2023, GeoSolutions Sas. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ +import React from 'react'; +import { Portal } from 'react-overlays'; +import { compose } from 'recompose'; +import PropTypes from 'prop-types'; + +import BaseMapComp from '../../components/map/BaseMap'; +import autoMapType from '../../components/map/enhancers/autoMapType'; +import autoResize from '../../components/map/enhancers/autoResize'; +import mapType from '../../components/map/enhancers/mapType'; +import onMapViewChanges from '../../components/map/enhancers/onMapViewChanges'; +import withDraw from '../../components/map/enhancers/withDraw'; +import mapEnhancer from './enhancers/mapEnhancer'; + +const MapWitDrawComp = compose( + mapEnhancer, + onMapViewChanges, + autoResize(0), + autoMapType, + mapType, + withDraw() +)(BaseMapComp); + + +const MapWithDraw = ({ + containerSelector, + map, + mapStateSource, + layer = {}, + onMapReady = () => {} +}) => { + return ( + + + ); +}; + +MapWithDraw.propTypes = { + containerSelector: PropTypes.string, + map: PropTypes.object, + mapStateSource: PropTypes.string, + onMapReady: PropTypes.bool, + layer: PropTypes.object +}; + +export default MapWithDraw; diff --git a/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx b/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx new file mode 100644 index 0000000000..69ca6748de --- /dev/null +++ b/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx @@ -0,0 +1,180 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { isEqual } from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Sidebar from 'react-sidebar'; + +import SpatialFilterCustom from './SpatialFilter'; +import standardItems from '../querypanel/index'; +import QueryPanelHeader from '../../components/data/query/QueryPanelHeader'; +import Portal from '../../components/misc/Portal'; +import ResizableModal from '../../components/misc/ResizableModal'; +import Message from '../locale/Message'; +import SmartQueryForm from './SmartQueryForm'; + +class PanelWithMap extends React.Component { + static propTypes = { + advancedToolbar: PropTypes.bool, + appliedFilter: PropTypes.object, + items: PropTypes.array, + layout: PropTypes.object, + loadingError: PropTypes.bool, + map: PropTypes.object, + mapComp: PropTypes.node, + onInit: PropTypes.func, + onRestoreFilter: PropTypes.func, + onSaveFilter: PropTypes.func, + onToggleQuery: PropTypes.func, + queryPanelEnabled: PropTypes.bool, + selectedLayer: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + spatialMethodOptions: PropTypes.array, + spatialOperations: PropTypes.array, + storedFilter: PropTypes.object, + toolsOptions: PropTypes.object + }; + + static defaultProps = { + items: [], + layout: {}, + loadingError: false, + onInit: () => {}, + onRestoreFilter: () => {}, + onSaveFilter: () => {}, + onToggleQuery: () => {}, + queryPanelEnabled: false, + selectedLayer: false, + toolsOptions: {} + }; + constructor(props) { + super(props); + this.state = {showModal: false}; + } + UNSAFE_componentWillReceiveProps(newProps) { + if (newProps.queryPanelEnabled === true && this.props.queryPanelEnabled === false) { + this.props.onInit(); + } + } + onToggle = () => { + if (this.props.advancedToolbar && !isEqual(this.props.appliedFilter, this.props.storedFilter)) { + this.setState(() => ({showModal: true})); + } else { + this.props.onToggleQuery(); + } + } + getNoBackgroundLayers = (group) => { + return group.name !== 'background'; + }; + renderSidebar = () => { + return ( + +
+ + ); + }; + renderQueryPanel = () => { + const MapComponent = this.props.mapComp; + standardItems.spatial = [{ + id: "spatialFilter", + plugin: SpatialFilterCustom, + cfg: {}, + position: 1 + }]; + return (
+ } + header={} + items={this.props.items} + loadingError={this.props.loadingError} + queryPanelEnabled={this.props.queryPanelEnabled} + selectedLayer={this.props.selectedLayer} + spatialMethodOptions={this.props.spatialMethodOptions} + spatialOperations={this.props.spatialOperations} + standardItems={standardItems} + storedFilter={this.props.storedFilter} + toolsOptions={this.props.toolsOptions} + /> +
+ +
+ + } + size="xs" + onClose={() => this.setState(() => ({showModal: false}))} + buttons={[ + { + bsStyle: 'primary', + text: , + onClick: this.storeAndClose + }, + { + bsStyle: 'primary', + text: , + onClick: this.restoreAndClose + } + ]}> +
+
+ +
+
+
+
+
); + }; + + + render() { + return this.renderSidebar(); + } + restoreAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onRestoreFilter(); + this.props.onToggleQuery(); + } + storeAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onSaveFilter(); + this.props.onToggleQuery(); + } +} + +export default PanelWithMap; diff --git a/web/client/plugins/queryPanelWithMap/SmartQueryForm.jsx b/web/client/plugins/queryPanelWithMap/SmartQueryForm.jsx new file mode 100644 index 0000000000..0e98e03db3 --- /dev/null +++ b/web/client/plugins/queryPanelWithMap/SmartQueryForm.jsx @@ -0,0 +1,148 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + + +import { toggleControl } from '../../actions/controls'; +import { changeDrawingStatus } from '../../actions/draw'; +import { applyFilter, discardCurrentFilter, storeCurrentFilter } from '../../actions/layerFilter'; +import { + addCrossLayerFilterField, + addFilterField, + addGroupField, + changeCascadingValue, + changeDwithinValue, + changeSpatialFilterValue, + expandAttributeFilterPanel, + expandCrossLayerFilterPanel, + expandSpatialFilterPanel, + removeCrossLayerFilterField, + removeFilterField, + removeGroupField, + removeSpatialSelection, + reset, + resetCrossLayerFilter, + search, + selectSpatialMethod, + selectSpatialOperation, + selectViewportSpatialMethod, + setCrossLayerFilterParameter, + showSpatialSelectionDetails, + toggleMenu, + updateCrossLayerFilterField, + updateExceptionField, + updateFilterField, + updateLogicCombo, + zoneChange, + zoneGetValues, + zoneSearch +} from '../../actions/queryform'; +import QueryBuilder from '../../components/data/query/QueryBuilder'; + +import { mapSelector } from '../../selectors/map'; +import { + availableCrossLayerFilterLayersSelector, + crossLayerFilterSelector +} from '../../selectors/queryform'; + + +const onReset = reset.bind(null, "query"); +// connecting a Dumb component to the store +// makes it a smart component +// we both connect state => props +// and actions to event handlers + +const SmartQueryForm = connect((state) => { + return { + // QueryBuilder props + useMapProjection: state.queryform.useMapProjection, + groupLevels: state.queryform.groupLevels, + groupFields: state.queryform.groupFields, + filterFields: state.queryform.filterFields, + attributes: state.query && state.query.typeName && state.query.featureTypes && state.query.featureTypes[state.query.typeName] && state.query.featureTypes[state.query.typeName].attributes, + featureTypeError: state.query && state.query.typeName && state.query.featureTypes && state.query.featureTypes[state.query.typeName] && state.query.featureTypes[state.query.typeName].error, + spatialField: state.queryform.spatialField, + filters: state.queryform.filters, + showDetailsPanel: state.queryform.showDetailsPanel, + toolbarEnabled: state.queryform.toolbarEnabled, + attributePanelExpanded: state.queryform.attributePanelExpanded, + autocompleteEnabled: state.queryform.autocompleteEnabled, + crossLayerExpanded: state.queryform.crossLayerExpanded, + crossLayerFilterOptions: { + layers: availableCrossLayerFilterLayersSelector(state), + crossLayerFilter: crossLayerFilterSelector(state), + ...(state.queryform.crossLayerFilterOptions || {}) + }, + maxFeaturesWPS: state.queryform.maxFeaturesWPS, + spatialPanelExpanded: state.queryform.spatialPanelExpanded, + featureTypeConfigUrl: state.query && state.query.url, + searchUrl: state.query && state.query.url, + featureTypeName: state.query && state.query.typeName, + ogcVersion: "1.1.0", + params: {typeName: state.query && state.query.typeName}, + resultTitle: "Query Result", + showGeneratedFilter: false, + allowEmptyFilter: true, + emptyFilterWarning: true, + maxHeight: state.map && state.map.present && state.map.present.size && state.map.present.size.height, + zoom: (mapSelector(state) || {}).zoom, + projection: (mapSelector(state) || {}).projection + }; +}, dispatch => { + return { + attributeFilterActions: bindActionCreators({ + onAddGroupField: addGroupField, + onAddFilterField: addFilterField, + onRemoveFilterField: removeFilterField, + onUpdateFilterField: updateFilterField, + onUpdateExceptionField: updateExceptionField, + onUpdateLogicCombo: updateLogicCombo, + onRemoveGroupField: removeGroupField, + onChangeCascadingValue: changeCascadingValue, + toggleMenu: toggleMenu, + onExpandAttributeFilterPanel: expandAttributeFilterPanel + }, dispatch), + spatialFilterActions: bindActionCreators({ + onChangeSpatialFilterValue: changeSpatialFilterValue, + onExpandSpatialFilterPanel: expandSpatialFilterPanel, + onSelectSpatialMethod: selectSpatialMethod, + onSelectViewportSpatialMethod: selectViewportSpatialMethod, + onSelectSpatialOperation: selectSpatialOperation, + onChangeDrawingStatus: changeDrawingStatus, + onRemoveSpatialSelection: removeSpatialSelection, + onShowSpatialSelectionDetails: showSpatialSelectionDetails, + onChangeDwithinValue: changeDwithinValue, + zoneFilter: zoneGetValues, + zoneSearch, + zoneChange + }, dispatch), + queryToolbarActions: bindActionCreators({ + onQuery: search, + onReset, + onSaveFilter: storeCurrentFilter, + onRestoreFilter: discardCurrentFilter, + storeAppliedFilter: applyFilter, + onChangeDrawingStatus: changeDrawingStatus + + }, dispatch), + crossLayerFilterActions: bindActionCreators({ + expandCrossLayerFilterPanel, + setCrossLayerFilterParameter, + addCrossLayerFilterField, + updateCrossLayerFilterField, + removeCrossLayerFilterField, + resetCrossLayerFilter, + toggleMenu: (rowId, status) => toggleMenu(rowId, status, "crossLayer") + }, dispatch), + controlActions: bindActionCreators({onToggleQuery: toggleControl.bind(null, 'queryPanelWithMap', null)}, dispatch) + }; +})(QueryBuilder); + +export default SmartQueryForm; diff --git a/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx b/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx new file mode 100644 index 0000000000..8f3201e686 --- /dev/null +++ b/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx @@ -0,0 +1,52 @@ +/** + * Copyright 2022, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +import {connect} from "react-redux"; +import {getMapConfigSelector} from "../../selectors/widgets"; +import {bindActionCreators} from "redux"; +import { + changeDwithinValue, + changeSpatialFilterValue, + expandSpatialFilterPanel, + removeSpatialSelection, + selectSpatialMethod, + selectSpatialOperation, + selectViewportSpatialMethod, showSpatialSelectionDetails, + zoneChange, zoneGetValues, zoneSearch +} from "../../actions/queryform"; +import {changeDrawingStatus} from "../../actions/draw"; +import SpatialFilterComponent from "../../components/data/query/SpatialFilter"; + +const SpatialFilter = connect((state) => { + return { + useMapProjection: state.queryform.useMapProjection, + spatialField: state.queryform.spatialField, + showDetailsPanel: state.queryform.showDetailsPanel, + spatialPanelExpanded: state.queryform.spatialPanelExpanded, + zoom: (getMapConfigSelector(state) || {}).zoom, + projection: (getMapConfigSelector(state) || {}).projection + }; +}, dispatch => { + return { + actions: bindActionCreators({ + onChangeSpatialFilterValue: changeSpatialFilterValue, + onExpandSpatialFilterPanel: expandSpatialFilterPanel, + onSelectSpatialMethod: selectSpatialMethod, + onSelectViewportSpatialMethod: selectViewportSpatialMethod, + onSelectSpatialOperation: selectSpatialOperation, + onChangeDrawingStatus: changeDrawingStatus, + onRemoveSpatialSelection: removeSpatialSelection, + onShowSpatialSelectionDetails: showSpatialSelectionDetails, + onChangeDwithinValue: changeDwithinValue, + zoneFilter: zoneGetValues, + zoneSearch, + zoneChange + }, dispatch) + }; +})(SpatialFilterComponent); + +export default SpatialFilter; diff --git a/web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js b/web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js new file mode 100644 index 0000000000..1faa6af18e --- /dev/null +++ b/web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js @@ -0,0 +1,80 @@ +/** +* Copyright 2018, GeoSolutions Sas. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ +import { compose, withStateHandlers, defaultProps, withPropsOnChange, withProps } from 'recompose'; +import { isEmpty } from 'lodash'; + +import { getCenterForExtent, getZoomForExtent } from '../../../utils/MapUtils'; +import { reprojectBbox, getExtentFromViewport } from '../../../utils/CoordinatesUtils'; + +const defaultBaseLayer = { + group: "background", + id: "mapnik__0", + loading: false, + loadingError: false, + name: "mapnik", + source: "osm", + title: "Open Street Map", + type: "osm", + visibility: true +}; + + +const mapEnhancer = compose( + defaultProps({ + onMapReady: () => {}, + baseLayer: defaultBaseLayer + + }), + withPropsOnChange("baseLayer", props => { + return {layers: [props.baseLayer]}; + }), + withStateHandlers(() => ({ + initialized: false + }), + { + onMapViewChanges: () => (map) => ( {map}), + onLayerLoad: ({map, initialized}, {onMapReady, baseLayer}) => (layerId) => { + // Map is ready when background is loaded just first load + if (!initialized && layerId === baseLayer.id) { + onMapReady(map); + return {initialized: true}; + } + return {}; + }, + centerLayer: (state, {layer: l}) => (map) => { + if (isEmpty(l)) { + return {}; + } + const newBbox = getExtentFromViewport(l.bbox, l.bbox.crs); + const center = getCenterForExtent(newBbox, l.bbox.crs); + const extent = l.bbox.crs !== (map.projection || "EPSG:3857") ? reprojectBbox(l.bbox.bounds, l.bbox.crs, map.projection || "EPSG:3857") : l.bbox.extent; + let zoom = 1; + if (map?.size) { + zoom = getZoomForExtent(extent, map.size, 0, 21); + } + const {bbox: omit, ...om} = map; + return {map: {...om, zoom, center, extent, mapStateSource: "mapModal"}}; + } + } + ), + withProps(props => { + return {layers: props.layers.concat(props.layer || [])}; + }), + withPropsOnChange(({map = {}}, {map: nM}) => { + return (!map?.size && nM?.size); + }, + ({ centerLayer, map}) => { + if (map?.size) { + centerLayer(map); + } + return {}; + }), + withProps(({onLayerLoad}) => ({eventHandlers: {onLayerLoad}})) +); +mapEnhancer.displayName = 'mapEnhancer'; +export default mapEnhancer; diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index ceef399b86..3fb9081df8 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -125,6 +125,7 @@ export const plugins = { OmniBarPlugin: toModulePlugin('OmniBar', () => import(/* webpackChunkName: 'plugins/omniBar' */ '../plugins/OmniBar')), PlaybackPlugin: toModulePlugin('Playback', () => import(/* webpackChunkName: 'plugins/playback' */ '../plugins/Playback')), QueryPanelPlugin: toModulePlugin('QueryPanel', () => import(/* webpackChunkName: 'plugins/queryPanel' */ '../plugins/QueryPanel')), + QueryPanelWithMapPlugin: toModulePlugin('QueryPanelWithMap', () => import(/* webpackChunkName: 'plugins/queryPanelWithMap' */ '../plugins/QueryPanelWithMap')), RedirectPlugin: toModulePlugin('Redirect', () => import(/* webpackChunkName: 'plugins/redirect' */ '../plugins/Redirect')), RedoPlugin: toModulePlugin('Redo', () => import(/* webpackChunkName: 'plugins/history' */ '../plugins/History')), SavePlugin: toModulePlugin('Save', () => import(/* webpackChunkName: 'plugins/save' */ '../plugins/Save')), diff --git a/web/client/reducers/widgets.js b/web/client/reducers/widgets.js index 38f31236ab..aea5faed4f 100644 --- a/web/client/reducers/widgets.js +++ b/web/client/reducers/widgets.js @@ -14,6 +14,7 @@ import { UPDATE_LAYER, DELETE, EDITOR_CHANGE, + CHANGE_MAP_EDITOR, EDITOR_SETTING_CHANGE, CHANGE_LAYOUT, CLEAR_WIDGETS, @@ -54,6 +55,7 @@ const emptyState = { } }, builder: { + map: null, settings: { step: 0 } @@ -98,6 +100,18 @@ function widgetsReducer(state = emptyState, action) { case EDITOR_CHANGE: { return editorChange(action, state); } + case CHANGE_MAP_EDITOR: { + return { + ...state, + builder: { + ...state.builder, + map: { + ...state.builder.map, + ...action.mapData + } + } + }; + } case INSERT: { let widget = {...action.widget}; if (widget.widgetType === 'chart') { diff --git a/web/client/selectors/controls.js b/web/client/selectors/controls.js index 5e9f45ec6a..c5e7192b96 100644 --- a/web/client/selectors/controls.js +++ b/web/client/selectors/controls.js @@ -24,6 +24,7 @@ export const shareSelector = (state) => get(state, "controls.share.enabled"); export const measureSelector = (state) => get(state, "controls.measure.enabled"); export const versionInfoSelector = (state) => get(state, "controls.version.enabled"); export const queryPanelSelector = (state) => get(state, "controls.queryPanel.enabled"); +export const queryPanelWithMapSelector = (state) => get(state, "controls.queryPanelWithMap.enabled"); export const printSelector = (state) => get(state, "controls.print.enabled"); export const wfsDownloadSelector = state => !!get(state, "controls.layerdownload.enabled"); export const widgetBuilderAvailable = state => get(state, "controls.widgetBuilder.available", false); diff --git a/web/client/selectors/widgets.js b/web/client/selectors/widgets.js index d4bba4b428..03b9a57304 100644 --- a/web/client/selectors/widgets.js +++ b/web/client/selectors/widgets.js @@ -18,6 +18,7 @@ import { createSelector, createStructuredSelector } from 'reselect'; import { createShallowSelector } from '../utils/ReselectUtils'; import { getAttributesNames } from "../utils/FeatureGridUtils"; +export const getMapConfigSelector = state => get(state, "widgets.builder.map"); export const getEditorSettings = state => get(state, "widgets.builder.settings"); export const getDependenciesMap = s => get(s, "widgets.dependencies") || {}; export const getDependenciesKeys = s => Object.keys(getDependenciesMap(s)).map(k => getDependenciesMap(s)[k]); diff --git a/web/client/themes/default/less/dashboard.less b/web/client/themes/default/less/dashboard.less index e35a1ff7f3..6a46f8216f 100644 --- a/web/client/themes/default/less/dashboard.less +++ b/web/client/themes/default/less/dashboard.less @@ -35,6 +35,31 @@ position: relative; overflow: auto; } + .query-form-root { + width: 100% !important; + .query-form-panel-container { + .spinner-panel{ + width: 600px !important; + } + width: 100% !important; + transition: unset !important; + .mapstore-query-builder { + position: unset; + display: flex; + #query-form-panel { + width: 600px !important; + } + .mapstore-query-map { + #__base_map__ { + margin: 0 !important; + height: 100% !important; + } + flex: 1 + } + + } + } + } } .dashboard-editor { diff --git a/web/client/utils/CoordinatesUtils.js b/web/client/utils/CoordinatesUtils.js index 7d1e8d5f9e..37d6314e34 100644 --- a/web/client/utils/CoordinatesUtils.js +++ b/web/client/utils/CoordinatesUtils.js @@ -1035,6 +1035,20 @@ export const transformExtentToObj = (extent) => { }; }; +/** + * helper use to transform the extent object to array { minx, miny, maxx, maxy } + * if there is no provided param extent it will return the default bound object of wgs84 + * @param {object} bounds is an object in the shape {minx, miny, maxx, maxy} + * @return {number[]} extent is an array of 4 ordered coordinates [minx, miny, maxx, maxy] + */ +export const transformExtentToArray = (bounds) => { + return [ + bounds.minx, + bounds.miny, + bounds.maxx, + bounds.maxy + ]; +}; /** diff --git a/web/client/utils/WidgetsUtils.js b/web/client/utils/WidgetsUtils.js index 53242cd8d7..8abb91155c 100644 --- a/web/client/utils/WidgetsUtils.js +++ b/web/client/utils/WidgetsUtils.js @@ -331,3 +331,73 @@ export const getSelectedWidgetData = (widget = {}) => { } return widget; }; + +export const DEFAULT_MAP_SETTINGS = { + projection: 'EPSG:900913', + units: 'm', + center: { + x: 11.22894105149402, + y: 43.380053862794, + crs: 'EPSG:4326' + }, + zoom: 5, + maxExtent: [ + -20037508.34, + -20037508.34, + 20037508.34, + 20037508.34 + ], + mapId: null, + size: { + width: 1300, + height: 920 + }, + bbox: { + bounds: { + minx: -3446291.017841229, + miny: 3136815.7816202897, + maxx: 5946291.017841229, + maxy: 7603184.218379708 + }, + crs: 'EPSG:3857', + rotation: 0 + }, + version: 2, + limits: {}, + mousePointer: 'pointer', + mapStateSource: 'mapWizard', + resolution: 4891.96981025128, + resolutions: [ + 156543.03392804097, + 78271.51696402048, + 39135.75848201024, + 19567.87924100512, + 9783.93962050256, + 4891.96981025128, + 2445.98490512564, + 1222.99245256282, + 611.49622628141, + 305.748113140705, + 152.8740565703525, + 76.43702828517625, + 38.21851414258813, + 19.109257071294063, + 9.554628535647032, + 4.777314267823516, + 2.388657133911758, + 1.194328566955879, + 0.5971642834779395, + 0.29858214173896974, + 0.14929107086948487, + 0.07464553543474244, + 0.03732276771737122, + 0.01866138385868561, + 0.009330691929342804, + 0.004665345964671402, + 0.002332672982335701, + 0.0011663364911678506, + 0.0005831682455839253, + 0.00029158412279196264, + 0.00014579206139598132 + ] +}; From 2edfd9c3526f3d1d35c21a00f5b5d50973b4b119 Mon Sep 17 00:00:00 2001 From: MV88 Date: Thu, 28 Sep 2023 14:39:55 +0200 Subject: [PATCH 02/21] Fix #9098 fix dashboard spatial filter capabilities --- web/client/actions/__tests__/widgets-test.js | 11 ++- web/client/actions/widgets.js | 6 +- .../components/data/query/SpatialFilter.jsx | 16 ++--- .../wizard/common/WPSWidgetOptions.jsx | 8 +-- web/client/configs/localConfig.json | 5 -- web/client/epics/__tests__/dashboard-test.js | 2 +- web/client/epics/__tests__/widgets-test.js | 67 ++++++++++++++++++- web/client/epics/dashboard.js | 1 - web/client/epics/widgets.js | 15 +---- web/client/plugins/QueryPanel.jsx | 46 ++++++------- .../queryPanelWithMap/PanelWithMap.jsx | 4 +- .../queryPanelWithMap/SpatialFilter.jsx | 2 +- .../enhancers/mapEnhancer.js | 2 +- web/client/reducers/__tests__/widgets-test.js | 5 ++ web/client/reducers/widgets.js | 5 +- .../selectors/__tests__/controls-test.js | 9 ++- .../selectors/__tests__/widgets-test.js | 5 ++ .../utils/__tests__/CoordinatesUtils-test.js | 11 ++- 18 files changed, 145 insertions(+), 75 deletions(-) diff --git a/web/client/actions/__tests__/widgets-test.js b/web/client/actions/__tests__/widgets-test.js index 02ca5741bc..cd80ea9035 100644 --- a/web/client/actions/__tests__/widgets-test.js +++ b/web/client/actions/__tests__/widgets-test.js @@ -9,6 +9,7 @@ import expect from 'expect'; import { + CHANGE_MAP_EDITOR, NEW, INSERT, UPDATE, @@ -27,8 +28,9 @@ import { DEPENDENCY_SELECTOR_KEY, TOGGLE_TRAY, TOGGLE_MAXIMIZE, - createChart, NEW_CHART, + changeMapEditor, + createChart, exportCSV, exportImage, openFilterEditor, @@ -54,6 +56,13 @@ import { describe('Test correctness of the widgets actions', () => { + it('changeMapEditor', () => { + const map = {id: "map-id"}; + const retval = changeMapEditor(map); + expect(retval).toExist(); + expect(retval.type).toBe(CHANGE_MAP_EDITOR); + expect(retval.mapData).toBe(map); + }); it('exportCSV', () => { const data = [{a: "a"}]; const retval = exportCSV({data, title: "TITLE"}); diff --git a/web/client/actions/widgets.js b/web/client/actions/widgets.js index 91769914a0..397c062a34 100644 --- a/web/client/actions/widgets.js +++ b/web/client/actions/widgets.js @@ -188,9 +188,9 @@ export const onEditorChange = (key, value) => ({ value }); /** - * Changes the entry in the widget editor - * @param {object} mapData the new map data - * @return {object} The action of type `WIDGETS:EDITOR_CHANGE` with key and value + * Changes the map config in the widget editor to be used by query form for creating spatial filters + * @param {object} mapData the new map data + * @return {object} The action of type `WIDGETS:CHANGE_MAP_EDITOR` with map data */ export const changeMapEditor = (mapData) => ({ type: CHANGE_MAP_EDITOR, diff --git a/web/client/components/data/query/SpatialFilter.jsx b/web/client/components/data/query/SpatialFilter.jsx index 193576a510..143c386431 100644 --- a/web/client/components/data/query/SpatialFilter.jsx +++ b/web/client/components/data/query/SpatialFilter.jsx @@ -211,17 +211,15 @@ class SpatialFilter extends React.Component { ); }; renderSpatialPanel = (operationRow, drawLabel, selectedOperation) => { - const zoneFields = this.renderZoneFields(); - const roiField = this.props.spatialField.method - && this.getMethodFromId(this.props.spatialField.method) - && this.getMethodFromId(this.props.spatialField.method).type === "wfsGeocoder" - ? this.renderRoiPanel() - : null; return ( - + {this.props.spatialMethodOptions.length > 1 ? this.renderSpatialHeader() : } - {zoneFields} - {roiField} + {this.renderZoneFields()} + {this.props.spatialField.method + && this.getMethodFromId(this.props.spatialField.method) + && this.getMethodFromId(this.props.spatialField.method).type === "wfsGeocoder" + ? this.renderRoiPanel() + : null} {this.props.spatialOperations.length > 1 ?
diff --git a/web/client/components/widgets/builder/wizard/common/WPSWidgetOptions.jsx b/web/client/components/widgets/builder/wizard/common/WPSWidgetOptions.jsx index df6a14518c..cbd8ca4eba 100644 --- a/web/client/components/widgets/builder/wizard/common/WPSWidgetOptions.jsx +++ b/web/client/components/widgets/builder/wizard/common/WPSWidgetOptions.jsx @@ -345,12 +345,8 @@ export default ({ /> : null} - {formOptions.advancedOptions && data.widgetType === "chart" - ? + {formOptions.advancedOptions && data.widgetType === "chart" && (data.type === "bar" || data.type === "line") + ? : null} diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 6694d8af6f..fe97c51fb3 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -15,11 +15,6 @@ "https://gs-stable.geosolutionsgroup.com:443/geoserver", "http://gs-stable.geosolutionsgroup.com/geoserver", "http://gs-stable.geosolutionsgroup.com:443/geoserver", - "http://gs-main.geosolutionsgroup.com/geoserver/", - "http://gs-main.geosolutionsgroup.com:443/geoserver/", - "https://gs-main.geosolutionsgroup.com/geoserver/", - "https://gs-main.geosolutionsgroup.com:443/geoserver/", - "https://geoqualif.cnr-france.com/geoserver/", "https://tile.googleapis.com" ] }, diff --git a/web/client/epics/__tests__/dashboard-test.js b/web/client/epics/__tests__/dashboard-test.js index 1d67d3beb8..1035810cce 100644 --- a/web/client/epics/__tests__/dashboard-test.js +++ b/web/client/epics/__tests__/dashboard-test.js @@ -245,7 +245,7 @@ describe('openDashboardWidgetEditor epic', () => { actions.map((action) => { switch (action.type) { case SET_CONTROL_PROPERTY: - if (action.control === "queryPanel") { + if (action.control === "queryPanelWithMap") { expect(action.property).toBe("enabled"); } break; diff --git a/web/client/epics/__tests__/widgets-test.js b/web/client/epics/__tests__/widgets-test.js index a676e303d5..aa90953b1e 100644 --- a/web/client/epics/__tests__/widgets-test.js +++ b/web/client/epics/__tests__/widgets-test.js @@ -16,10 +16,12 @@ import { updateLayerOnLayerPropertiesChange, updateLayerOnLoadingErrorChange, updateDependenciesMapOnMapSwitch, - onWidgetCreationFromMap + onWidgetCreationFromMap, + onLayerSelectedEpic } from '../widgets'; import { + CHANGE_MAP_EDITOR, CLEAR_WIDGETS, insertWidget, toggleConnection, @@ -41,6 +43,9 @@ import { onLocationChanged } from 'connected-react-router'; import { ActionsObservable } from 'redux-observable'; import Rx from 'rxjs'; +import { DEFAULT_MAP_SETTINGS } from '../../utils/WidgetsUtils'; + + describe('widgets Epics', () => { it('clearWidgetsOnLocationChange triggers CLEAR_WIDGETS on LOCATION_CHANGE', (done) => { const checkActions = actions => { @@ -667,4 +672,64 @@ describe('widgets Epics', () => { [onEditorChange("widgetType", "chart")], checkActions, state); }); + it('onLayerSelectedEpic', (done) => { + const checkActions = actions => { + expect(actions.length).toBe(1); + expect(actions[0].type).toBe(CHANGE_MAP_EDITOR); + expect(actions[0].mapData).toEqual({ + ...DEFAULT_MAP_SETTINGS, + bbox: { + crs: "EPSG:4326", + bounds: { + minx: -18, miny: -9, maxx: 18, maxy: 9 + } + }, + center: { + crs: "EPSG:4326", + x: 0, + y: 0 + } + }); + done(); + }; + const state = { + layers: { + flat: [{ + id: "1", + name: "layer", + bbox: { + crs: "EPSG:4326", + bounds: { + minx: -18, miny: -9, maxx: 18, maxy: 9 + } + } + }, { + id: "2", + name: "layer2" + }, { + id: "3", + name: "layer3" + }], + selected: ["1"] + }, + dashboard: { + editor: { + layer: { + bbox: { + crs: "EPSG:4326", + bounds: { + minx: -18, miny: -9, maxx: 18, maxy: 9 + } + } + }, + available: false + }, + editing: true + } + }; + testEpic(onLayerSelectedEpic, + 1, + [onEditorChange("chart-layers", {})], + checkActions, state); + }); }); diff --git a/web/client/epics/dashboard.js b/web/client/epics/dashboard.js index 253d407e45..1b87bdd00c 100644 --- a/web/client/epics/dashboard.js +++ b/web/client/epics/dashboard.js @@ -77,7 +77,6 @@ export const closeDashboardEditorOnExit = (action$, {getState = () => {}} = {}) /** * Manages interaction with QueryPanel and Dashboard - * [ ] EDIT ALSO HERE */ export const handleDashboardWidgetsFilterPanel = (action$, {getState = () => {}} = {}) => action$.ofType(OPEN_FILTER_EDITOR) .filter(() => isDashboardAvailable(getState())) diff --git a/web/client/epics/widgets.js b/web/client/epics/widgets.js index d9550f03be..25d5c3e37e 100644 --- a/web/client/epics/widgets.js +++ b/web/client/epics/widgets.js @@ -9,9 +9,6 @@ import Rx from 'rxjs'; import { endsWith, has, get, includes, isEqual, omit, omitBy } from 'lodash'; -import { LOCATION_CHANGE } from 'connected-react-router'; -import { saveAs } from 'file-saver'; -import converter from 'json-2-csv'; import { EXPORT_CSV, @@ -35,7 +32,6 @@ import { } from '../actions/widgets'; import { MAP_CONFIG_LOADED } from '../actions/config'; -import { SET_CONTROL_PROPERTY } from '../actions/controls'; import { availableDependenciesSelector, @@ -45,17 +41,18 @@ import { getWidgetLayer } from '../selectors/widgets'; import { CHANGE_LAYER_PROPERTIES, LAYER_LOAD, LAYER_ERROR } from '../actions/layers'; -import { zoomToExtent } from '../actions/map'; import { getLayerFromId } from '../selectors/layers'; import { pathnameSelector } from '../selectors/router'; import { isDashboardEditing } from '../selectors/dashboard'; import { MAP_CREATED, SAVING_MAP, MAP_ERROR } from '../actions/maps'; import { DASHBOARD_LOADED } from '../actions/dashboard'; +import { LOCATION_CHANGE } from 'connected-react-router'; +import { saveAs } from 'file-saver'; import {downloadCanvasDataURL} from '../utils/FileUtils'; +import converter from 'json-2-csv'; import { updateDependenciesMapOfMapList, DEFAULT_MAP_SETTINGS } from "../utils/WidgetsUtils"; -import { transformExtentToArray } from "../utils/CoordinatesUtils"; const updateDependencyMap = (active, targetId, { dependenciesMap, mappings}) => { const tableDependencies = ["layer", "filter", "quickFilters", "options"]; @@ -341,12 +338,6 @@ export const onLayerSelectedEpic = (action$, store) => y: (layer.bbox.bounds.maxy + layer.bbox.bounds.miny) / 2 } }) - ).concat( - action$.ofType(SET_CONTROL_PROPERTY) - .filter(({control, property, value}) => control === "queryPanelWithMap" && property === "enabled" && value) - .switchMap(() => { - return Rx.Observable.of(zoomToExtent(transformExtentToArray(layer.bbox.bounds), layer.bbox.crs, 21)); - }) ); } return observable$; diff --git a/web/client/plugins/QueryPanel.jsx b/web/client/plugins/QueryPanel.jsx index 4ceecf74a8..c1f323b393 100644 --- a/web/client/plugins/QueryPanel.jsx +++ b/web/client/plugins/QueryPanel.jsx @@ -76,8 +76,6 @@ import layerFilterReducers from '../reducers/layerFilter'; import queryReducers from '../reducers/query'; import queryformReducers from '../reducers/queryform'; import { isDashboardAvailable } from '../selectors/dashboard'; -// import BaseMap from '../components/map/BaseMap'; - import { groupsSelector, selectedLayerLoadingErrorSelector } from '../selectors/layers'; import { mapSelector } from '../selectors/map'; import { mapLayoutValuesSelector } from '../selectors/maplayout'; @@ -91,6 +89,8 @@ import { sortLayers, sortUsing, toggleByType } from '../utils/LayersUtils'; import Message from './locale/Message'; import {typeNameSelector} from "../selectors/query"; +// include application component + const onReset = reset.bind(null, "query"); // connecting a Dumb component to the store @@ -185,7 +185,6 @@ const SmartQueryForm = connect((state) => { const tocSelector = createSelector( [ - mapSelector, (state) => state.controls && state.controls.toolbar && state.controls.toolbar.active === 'toc', groupsSelector, (state) => state.layers && state.layers.settings, @@ -197,8 +196,7 @@ const tocSelector = createSelector( (state) => state && state.query && state.query.isLayerFilter, selectedLayerLoadingErrorSelector, typeNameSelector - ], (map, enabled, groups, settings, queryPanelEnabled, layoutHeight, dashboardAvailable, appliedFilter, storedFilter, advancedToolbar, loadingError, selectedLayer) => ({ - map, + ], (enabled, groups, settings, queryPanelEnabled, layoutHeight, dashboardAvailable, appliedFilter, storedFilter, advancedToolbar, loadingError, selectedLayer) => ({ enabled, groups, settings, @@ -216,7 +214,6 @@ class QueryPanel extends React.Component { static propTypes = { id: PropTypes.number, buttonContent: PropTypes.node, - map: PropTypes.object, groups: PropTypes.array, settings: PropTypes.object, queryPanelEnabled: PropTypes.bool, @@ -287,16 +284,10 @@ class QueryPanel extends React.Component { this.props.onInit(); } } - onToggle = () => { - if (this.props.advancedToolbar && !isEqual(this.props.appliedFilter, this.props.storedFilter)) { - this.setState(() => ({showModal: true})); - } else { - this.props.onToggleQuery(); - } - } getNoBackgroundLayers = (group) => { return group.name !== 'background'; }; + renderSidebar = () => { return ( ); }; + onToggle = () => { + if (this.props.advancedToolbar && !isEqual(this.props.appliedFilter, this.props.storedFilter)) { + this.setState(() => ({showModal: true})); + } else { + this.props.onToggleQuery(); + } + } + restoreAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onRestoreFilter(); + this.props.onToggleQuery(); + } + storeAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onSaveFilter(); + this.props.onToggleQuery(); + } renderQueryPanel = () => { return (
-
{ - this.setState(() => ({showModal: false})); - this.props.onRestoreFilter(); - this.props.onToggleQuery(); - } - storeAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onSaveFilter(); - this.props.onToggleQuery(); - } } /** diff --git a/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx b/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx index 69ca6748de..68da010af5 100644 --- a/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx +++ b/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx @@ -128,9 +128,7 @@ class PanelWithMap extends React.Component { toolsOptions={this.props.toolsOptions} />
- +
{ expect(state.dependencies.center).toBe("map.center"); expect(state.dependencies.zoom).toBe("map.zoom"); }); + it('CHANGE_MAP_EDITOR', () => { + const state = widgets(undefined, changeMapEditor({bbox: {}})); + expect(state.builder.map).toEqual({bbox: {}}); + }); it('editNewWidget', () => { const state = widgets(undefined, editNewWidget({a: "A"}, {step: 0})); expect(state.builder.editor.a).toBe("A"); diff --git a/web/client/reducers/widgets.js b/web/client/reducers/widgets.js index aea5faed4f..05bda20fcd 100644 --- a/web/client/reducers/widgets.js +++ b/web/client/reducers/widgets.js @@ -105,10 +105,7 @@ function widgetsReducer(state = emptyState, action) { ...state, builder: { ...state.builder, - map: { - ...state.builder.map, - ...action.mapData - } + map: action.mapData } }; } diff --git a/web/client/selectors/__tests__/controls-test.js b/web/client/selectors/__tests__/controls-test.js index 4ffabf555c..8c85b6defe 100644 --- a/web/client/selectors/__tests__/controls-test.js +++ b/web/client/selectors/__tests__/controls-test.js @@ -10,6 +10,7 @@ import expect from 'expect'; import { queryPanelSelector, + queryPanelWithMapSelector, wfsDownloadSelector, widgetBuilderAvailable, widgetBuilderSelector, @@ -26,6 +27,9 @@ const state = { queryPanel: { enabled: true }, + queryPanelWithMap: { + enabled: false + }, layerdownload: { available: true, enabled: true @@ -57,7 +61,10 @@ describe('Test controls selectors', () => { expect(retVal).toExist(); expect(retVal).toBe(true); }); - + it('test queryPanelWithMapSelector', () => { + const retVal = queryPanelWithMapSelector(state); + expect(retVal).toBeFalsy(); + }); it('test wfsDownloadSelector', () => { const retVal = wfsDownloadSelector(state); expect(retVal).toExist(); diff --git a/web/client/selectors/__tests__/widgets-test.js b/web/client/selectors/__tests__/widgets-test.js index 989c084d7a..96db903f44 100644 --- a/web/client/selectors/__tests__/widgets-test.js +++ b/web/client/selectors/__tests__/widgets-test.js @@ -18,6 +18,7 @@ import { getEditingWidgetLayer, getEditingWidgetFilter, getEditorSettings, + getMapConfigSelector, getWidgetLayer, getWidgetAttributeFilter, dependenciesSelector, @@ -79,6 +80,10 @@ describe('widgets selectors', () => { expect(getEditorSettings(state)).toExist(); expect(getEditorSettings(state).flag).toBe(true); }); + it('getMapConfigSelector ', () => { + const state = set(`widgets.builder.map`, { id: "map-id" }, {}); + expect(getMapConfigSelector(state)).toEqual({ id: "map-id" }); + }); it('returnToFeatureGridSelector', () => { const state = set(`widgets.builder.editor`, { returnToFeatureGrid: true }, {}); expect(returnToFeatureGridSelector(state)).toExist(); diff --git a/web/client/utils/__tests__/CoordinatesUtils-test.js b/web/client/utils/__tests__/CoordinatesUtils-test.js index a019bc2955..8d73c4a703 100644 --- a/web/client/utils/__tests__/CoordinatesUtils-test.js +++ b/web/client/utils/__tests__/CoordinatesUtils-test.js @@ -37,7 +37,11 @@ import { makeNumericEPSG, getPolygonFromCircle, checkIfLayerFitsExtentForProjection, - getLonLatFromPoint, convertRadianToDegrees, convertDegreesToRadian, transformExtentToObj + getLonLatFromPoint, + convertRadianToDegrees, + convertDegreesToRadian, + transformExtentToObj, + transformExtentToArray } from '../CoordinatesUtils'; import Proj4js from 'proj4'; @@ -781,6 +785,11 @@ describe('CoordinatesUtils', () => { }); }); }); + it('test transformExtentToArray', ()=>{ + const extent = [1, 1, 5, 5]; + const bounds = { minx: 1, miny: 1, maxx: 5, maxy: 5 }; + expect(transformExtentToArray(bounds)).toEqual(extent); + }); it('extractCrsFromURN #1', () => { const urn = 'urn:ogc:def:crs:EPSG:6.6:4326'; expect(extractCrsFromURN(urn)).toBe('EPSG:4326'); From f3c641f4c4b09a14813a25cd629728edc572d2f3 Mon Sep 17 00:00:00 2001 From: MV88 Date: Thu, 28 Sep 2023 14:57:27 +0200 Subject: [PATCH 03/21] add clearing of map builder logic --- web/client/epics/__tests__/widgets-test.js | 23 +++++++++++++++++++++- web/client/epics/widgets.js | 4 ++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/web/client/epics/__tests__/widgets-test.js b/web/client/epics/__tests__/widgets-test.js index aa90953b1e..d395226f80 100644 --- a/web/client/epics/__tests__/widgets-test.js +++ b/web/client/epics/__tests__/widgets-test.js @@ -672,7 +672,7 @@ describe('widgets Epics', () => { [onEditorChange("widgetType", "chart")], checkActions, state); }); - it('onLayerSelectedEpic', (done) => { + it('onLayerSelectedEpic by selecting a map', (done) => { const checkActions = actions => { expect(actions.length).toBe(1); expect(actions[0].type).toBe(CHANGE_MAP_EDITOR); @@ -732,4 +732,25 @@ describe('widgets Epics', () => { [onEditorChange("chart-layers", {})], checkActions, state); }); + it('onLayerSelectedEpic by clearing map state used by queryform in dashboard', (done) => { + const checkActions = actions => { + expect(actions.length).toBe(1); + expect(actions[0].type).toBe(CHANGE_MAP_EDITOR); + expect(actions[0].mapData).toEqual(null); + done(); + }; + const state = { + layers: {}, + dashboard: { + editor: { + available: false + }, + editing: true + } + }; + testEpic(onLayerSelectedEpic, + 1, + [onEditorChange("chart-layers")], + checkActions, state); + }); }); diff --git a/web/client/epics/widgets.js b/web/client/epics/widgets.js index 25d5c3e37e..16f284dd5c 100644 --- a/web/client/epics/widgets.js +++ b/web/client/epics/widgets.js @@ -339,6 +339,10 @@ export const onLayerSelectedEpic = (action$, store) => } }) ); + } else { + observable$ = Rx.Observable.of( + changeMapEditor(null) + ); } return observable$; }); From 6c481bb580f28b0255e2b3accbadef85e73d30a0 Mon Sep 17 00:00:00 2001 From: MV88 Date: Tue, 10 Oct 2023 17:42:29 +0200 Subject: [PATCH 04/21] Fixes after review request --- .../actions/__tests__/queryform-test.js | 11 +- web/client/actions/__tests__/widgets-test.js | 9 - web/client/actions/queryform.js | 11 ++ web/client/actions/widgets.js | 10 - .../components/data/query/QueryBuilder.jsx | 2 +- .../components/data/query/SpatialFilter.jsx | 4 +- web/client/components/map/BaseMap.jsx | 8 +- web/client/configs/localConfig.json | 3 +- web/client/epics/__tests__/dashboard-test.js | 2 +- web/client/epics/__tests__/widgets-test.js | 14 +- web/client/epics/dashboard.js | 6 +- web/client/epics/widgets.js | 57 ++++-- web/client/plugins/QueryPanel.jsx | 166 ++++++++-------- web/client/plugins/QueryPanelWithMap.jsx | 4 +- .../plugins/queryPanelWithMap/MapComp.jsx | 6 +- .../plugins/queryPanelWithMap/MapWithDraw.jsx | 5 +- .../queryPanelWithMap/PanelWithMap.jsx | 178 ------------------ .../queryPanelWithMap/SmartQueryForm.jsx | 148 --------------- .../queryPanelWithMap/SpatialFilter.jsx | 2 +- .../enhancers/mapEnhancer.js | 17 +- web/client/product/plugins.js | 1 - .../reducers/__tests__/queryform-test.js | 17 +- web/client/reducers/__tests__/widgets-test.js | 5 - web/client/reducers/queryform.js | 29 ++- web/client/reducers/widgets.js | 10 - .../selectors/__tests__/controls-test.js | 8 - .../selectors/__tests__/queryform-test.js | 7 + .../selectors/__tests__/widgets-test.js | 5 - web/client/selectors/controls.js | 1 - web/client/selectors/queryform.js | 1 + web/client/selectors/widgets.js | 2 +- web/client/utils/WidgetsUtils.js | 48 +---- 32 files changed, 243 insertions(+), 554 deletions(-) delete mode 100644 web/client/plugins/queryPanelWithMap/PanelWithMap.jsx delete mode 100644 web/client/plugins/queryPanelWithMap/SmartQueryForm.jsx diff --git a/web/client/actions/__tests__/queryform-test.js b/web/client/actions/__tests__/queryform-test.js index 1023d49665..7eb9a24026 100644 --- a/web/client/actions/__tests__/queryform-test.js +++ b/web/client/actions/__tests__/queryform-test.js @@ -83,11 +83,20 @@ import { changeSpatialFilterValue, updateCrossLayerFilterFieldOptions, upsertFilters, - removeFilters + changeMapEditor, + removeFilters, + CHANGE_MAP_EDITOR } from '../queryform'; describe('Test correctness of the queryform actions', () => { + it('changeMapEditor', () => { + var retval = changeMapEditor(null); + + expect(retval).toExist(); + expect(retval.type).toBe(CHANGE_MAP_EDITOR); + expect(retval.mapData).toBe(null); + }); it('addFilterField', () => { let groupId = 1; diff --git a/web/client/actions/__tests__/widgets-test.js b/web/client/actions/__tests__/widgets-test.js index cd80ea9035..fb3a95e86e 100644 --- a/web/client/actions/__tests__/widgets-test.js +++ b/web/client/actions/__tests__/widgets-test.js @@ -9,7 +9,6 @@ import expect from 'expect'; import { - CHANGE_MAP_EDITOR, NEW, INSERT, UPDATE, @@ -29,7 +28,6 @@ import { TOGGLE_TRAY, TOGGLE_MAXIMIZE, NEW_CHART, - changeMapEditor, createChart, exportCSV, exportImage, @@ -56,13 +54,6 @@ import { describe('Test correctness of the widgets actions', () => { - it('changeMapEditor', () => { - const map = {id: "map-id"}; - const retval = changeMapEditor(map); - expect(retval).toExist(); - expect(retval.type).toBe(CHANGE_MAP_EDITOR); - expect(retval.mapData).toBe(map); - }); it('exportCSV', () => { const data = [{a: "a"}]; const retval = exportCSV({data, title: "TITLE"}); diff --git a/web/client/actions/queryform.js b/web/client/actions/queryform.js index c0828d0bd2..9394cec16a 100644 --- a/web/client/actions/queryform.js +++ b/web/client/actions/queryform.js @@ -58,8 +58,19 @@ export const LOAD_FILTER = 'QUERYFORM:LOAD_FILTER'; export const UPSERT_FILTERS = 'QUERYFORM:UPSERT_FILTERS'; export const REMOVE_FILTERS = 'QUERYFORM:REMOVE_FILTERS'; +export const CHANGE_MAP_EDITOR = "QUERYFORM:CHANGE_MAP_EDITOR"; + import axios from '../libs/ajax'; +/** + * Changes the map config to be used by query form for creating spatial filters + * @param {object} mapData the new map data + */ +export const changeMapEditor = (mapData) => ({ + type: CHANGE_MAP_EDITOR, + mapData +}); + export function addFilterField(groupId) { return { type: ADD_FILTER_FIELD, diff --git a/web/client/actions/widgets.js b/web/client/actions/widgets.js index 397c062a34..d214810a6b 100644 --- a/web/client/actions/widgets.js +++ b/web/client/actions/widgets.js @@ -12,7 +12,6 @@ export const NEW = "WIDGETS:NEW"; export const EDIT = "WIDGETS:EDIT"; export const EDIT_NEW = "WIDGETS:EDIT_NEW"; export const EDITOR_CHANGE = "WIDGETS:EDITOR_CHANGE"; -export const CHANGE_MAP_EDITOR = "WIDGETS:CHANGE_MAP_EDITOR"; export const EDITOR_SETTING_CHANGE = "WIDGETS:EDITOR_SETTING_CHANGE"; export const UPDATE = "WIDGETS:UPDATE"; export const UPDATE_PROPERTY = "WIDGETS:UPDATE_PROPERTY"; @@ -187,15 +186,6 @@ export const onEditorChange = (key, value) => ({ key, value }); -/** - * Changes the map config in the widget editor to be used by query form for creating spatial filters - * @param {object} mapData the new map data - * @return {object} The action of type `WIDGETS:CHANGE_MAP_EDITOR` with map data - */ -export const changeMapEditor = (mapData) => ({ - type: CHANGE_MAP_EDITOR, - mapData -}); /** * Changes a setting of the editor (e.g. the page) diff --git a/web/client/components/data/query/QueryBuilder.jsx b/web/client/components/data/query/QueryBuilder.jsx index 39fb252666..8ad141ba70 100644 --- a/web/client/components/data/query/QueryBuilder.jsx +++ b/web/client/components/data/query/QueryBuilder.jsx @@ -216,7 +216,7 @@ class QueryBuilder extends React.Component { {this.renderItems('layers', { spatialOperations, spatialMethodOptions, ...toolsOptions })} {this.renderItems('end', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - :
; + :
; } filterItem = (target, layerName) => (el) => { diff --git a/web/client/components/data/query/SpatialFilter.jsx b/web/client/components/data/query/SpatialFilter.jsx index 143c386431..5da79f0312 100644 --- a/web/client/components/data/query/SpatialFilter.jsx +++ b/web/client/components/data/query/SpatialFilter.jsx @@ -216,8 +216,8 @@ class SpatialFilter extends React.Component { {this.props.spatialMethodOptions.length > 1 ? this.renderSpatialHeader() : } {this.renderZoneFields()} {this.props.spatialField.method - && this.getMethodFromId(this.props.spatialField.method) - && this.getMethodFromId(this.props.spatialField.method).type === "wfsGeocoder" + && this.getMethodFromId(this.props.spatialField.method) + && this.getMethodFromId(this.props.spatialField.method).type === "wfsGeocoder" ? this.renderRoiPanel() : null} {this.props.spatialOperations.length > 1 ? diff --git a/web/client/components/map/BaseMap.jsx b/web/client/components/map/BaseMap.jsx index 6da09af94b..ca728de42e 100644 --- a/web/client/components/map/BaseMap.jsx +++ b/web/client/components/map/BaseMap.jsx @@ -43,7 +43,8 @@ class BaseMap extends React.Component { plugins: PropTypes.any, tools: PropTypes.array, getLayerProps: PropTypes.func, - env: PropTypes.array + env: PropTypes.array, + zoomControl: PropTypes.bool }; static defaultProps = { @@ -60,7 +61,8 @@ class BaseMap extends React.Component { onLayerLoading: () => {}, onLayerError: () => {} }, - env: [] + env: [], + zoomControl: false }; getTool = (tool) => { @@ -143,7 +145,7 @@ class BaseMap extends React.Component { projectionDefs={this.props.projectionDefs} style={this.props.styleMap} id={this.props.id} - zoomControl={false} + zoomControl={this.props.zoomControl} center={{ x: 0, y: 0 }} zoom={1} hookRegister={this.props.hookRegister} diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index fe97c51fb3..fc3807a029 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -810,8 +810,9 @@ "containerPosition": "columns" } }, - { "name": "QueryPanelWithMap", + { "name": "QueryPanel", "cfg": { + "useEmbeddedMap": true, "toolsOptions": { "hideCrossLayer": true }, diff --git a/web/client/epics/__tests__/dashboard-test.js b/web/client/epics/__tests__/dashboard-test.js index 1035810cce..1d67d3beb8 100644 --- a/web/client/epics/__tests__/dashboard-test.js +++ b/web/client/epics/__tests__/dashboard-test.js @@ -245,7 +245,7 @@ describe('openDashboardWidgetEditor epic', () => { actions.map((action) => { switch (action.type) { case SET_CONTROL_PROPERTY: - if (action.control === "queryPanelWithMap") { + if (action.control === "queryPanel") { expect(action.property).toBe("enabled"); } break; diff --git a/web/client/epics/__tests__/widgets-test.js b/web/client/epics/__tests__/widgets-test.js index d395226f80..d8cc52d2b0 100644 --- a/web/client/epics/__tests__/widgets-test.js +++ b/web/client/epics/__tests__/widgets-test.js @@ -21,7 +21,10 @@ import { } from '../widgets'; import { - CHANGE_MAP_EDITOR, + CHANGE_MAP_EDITOR +} from '../../actions/queryform'; + +import { CLEAR_WIDGETS, insertWidget, toggleConnection, @@ -678,17 +681,12 @@ describe('widgets Epics', () => { expect(actions[0].type).toBe(CHANGE_MAP_EDITOR); expect(actions[0].mapData).toEqual({ ...DEFAULT_MAP_SETTINGS, - bbox: { - crs: "EPSG:4326", - bounds: { - minx: -18, miny: -9, maxx: 18, maxy: 9 - } - }, center: { crs: "EPSG:4326", x: 0, y: 0 - } + }, + zoom: 21 }); done(); }; diff --git a/web/client/epics/dashboard.js b/web/client/epics/dashboard.js index 1b87bdd00c..eaf304b836 100644 --- a/web/client/epics/dashboard.js +++ b/web/client/epics/dashboard.js @@ -85,12 +85,12 @@ export const handleDashboardWidgetsFilterPanel = (action$, {getState = () => {}} Rx.Observable.of( featureTypeSelected(...getFTSelectedArgs(getState())), loadFilter(getEditingWidgetFilter(getState())), - setControlProperty('queryPanelWithMap', "enabled", true) + setControlProperty('queryPanel', "enabled", true) // wait for any filter update(search) or query form close event ).concat( Rx.Observable.race( action$.ofType(QUERY_FORM_SEARCH).take(1), - action$.ofType(TOGGLE_CONTROL).filter(({control, property} = {}) => control === "queryPanelWithMap" && (!property || property === "enabled")).take(1) + action$.ofType(TOGGLE_CONTROL).filter(({control, property} = {}) => control === "queryPanel" && (!property || property === "enabled")).take(1) ) // then close the query panel, open widget form and update the current filter for the widget in editing .switchMap( action => @@ -108,7 +108,7 @@ export const handleDashboardWidgetsFilterPanel = (action$, {getState = () => {}} .merge(action$.ofType(TOGGLE_CONTROL).filter(({control, property} = {}) => control === "widgetBuilder" && (!property === false)))) .concat( Rx.Observable.of(// drawSupportReset(), - setControlProperty('queryPanelWithMap', "enabled", false) + setControlProperty('queryPanel', "enabled", false) ) ) ); diff --git a/web/client/epics/widgets.js b/web/client/epics/widgets.js index 16f284dd5c..2b8dfaf32d 100644 --- a/web/client/epics/widgets.js +++ b/web/client/epics/widgets.js @@ -18,7 +18,6 @@ import { WIDGET_SELECTED, EDITOR_SETTING_CHANGE, onEditorChange, - changeMapEditor, updateWidgetLayer, clearWidgets, loadDependencies, @@ -28,9 +27,11 @@ import { UPDATE_PROPERTY, replaceWidgets, WIDGETS_MAPS_REGEX, - EDITOR_CHANGE + EDITOR_CHANGE, + EDIT } from '../actions/widgets'; +import { changeMapEditor } from '../actions/queryform'; import { MAP_CONFIG_LOADED } from '../actions/config'; import { @@ -50,8 +51,9 @@ import { DASHBOARD_LOADED } from '../actions/dashboard'; import { LOCATION_CHANGE } from 'connected-react-router'; import { saveAs } from 'file-saver'; import {downloadCanvasDataURL} from '../utils/FileUtils'; +import {transformExtentToArray} from '../utils/CoordinatesUtils'; import converter from 'json-2-csv'; - +import { getZoomForExtent } from '../utils/MapUtils'; import { updateDependenciesMapOfMapList, DEFAULT_MAP_SETTINGS } from "../utils/WidgetsUtils"; const updateDependencyMap = (active, targetId, { dependenciesMap, mappings}) => { @@ -320,31 +322,47 @@ export const onWidgetCreationFromMap = (action$, store) => }); +const getMapConfig = (layer) => { + return { + ...DEFAULT_MAP_SETTINGS, + // bbox: layer.bbox, + zoom: getZoomForExtent(transformExtentToArray(layer.bbox), DEFAULT_MAP_SETTINGS.size, 0, 21), + center: { + crs: layer.bbox.crs, + x: (layer.bbox.bounds.maxx + layer.bbox.bounds.minx) / 2, + y: (layer.bbox.bounds.maxy + layer.bbox.bounds.miny) / 2 + } + }; +}; export const onLayerSelectedEpic = (action$, store) => action$.ofType(EDITOR_CHANGE) .filter(({key}) => key === 'chart-layers' && isDashboardEditing(store.getState())) .switchMap(() => { - let observable$ = Rx.Observable.empty(); const state = store.getState(); const layer = getWidgetLayer(state); if (layer?.bbox) { - observable$ = Rx.Observable.of( - changeMapEditor({ - ...DEFAULT_MAP_SETTINGS, - bbox: layer.bbox, - center: { - crs: layer.bbox.crs, - x: (layer.bbox.bounds.maxx + layer.bbox.bounds.minx) / 2, - y: (layer.bbox.bounds.maxy + layer.bbox.bounds.miny) / 2 - } - }) + return Rx.Observable.of( + changeMapEditor(getMapConfig(layer)) ); - } else { - observable$ = Rx.Observable.of( - changeMapEditor(null) + } + return Rx.Observable.of( + changeMapEditor(null) + ); + }); +export const onEditWidgetEpic = (action$, store) => + action$.ofType(EDIT) + .filter(() => isDashboardEditing(store.getState())) + .switchMap(() => { + const state = store.getState(); + const layer = getWidgetLayer(state); + if (layer?.bbox) { + return Rx.Observable.of( + changeMapEditor(getMapConfig(layer)) ); } - return observable$; + return Rx.Observable.of( + changeMapEditor(null) + ); }); export default { @@ -357,5 +375,6 @@ export default { updateLayerOnLoadingErrorChange, updateDependenciesMapOnMapSwitch, onWidgetCreationFromMap, - onLayerSelectedEpic + onLayerSelectedEpic, + onEditWidgetEpic }; diff --git a/web/client/plugins/QueryPanel.jsx b/web/client/plugins/QueryPanel.jsx index c1f323b393..70f48ab537 100644 --- a/web/client/plugins/QueryPanel.jsx +++ b/web/client/plugins/QueryPanel.jsx @@ -20,6 +20,9 @@ import { changeDrawingStatus } from '../actions/draw'; import { getLayerCapabilities } from '../actions/layerCapabilities'; import { queryPanelSelector } from '../selectors/controls'; import { applyFilter, discardCurrentFilter, storeCurrentFilter } from '../actions/layerFilter'; +import MapComp from './queryPanelWithMap/MapComp'; +import SpatialFilterCustom from './queryPanelWithMap/SpatialFilter'; + import { changeGroupProperties, changeLayerProperties, @@ -74,6 +77,7 @@ import queryFormEpics from '../epics/queryform'; import {featureTypeSelectedEpic, redrawSpatialFilterEpic, viewportSelectedEpic, wfsQueryEpic} from '../epics/wfsquery'; import layerFilterReducers from '../reducers/layerFilter'; import queryReducers from '../reducers/query'; +import drawReducers from '../reducers/draw'; import queryformReducers from '../reducers/queryform'; import { isDashboardAvailable } from '../selectors/dashboard'; import { groupsSelector, selectedLayerLoadingErrorSelector } from '../selectors/layers'; @@ -206,47 +210,30 @@ const tocSelector = createSelector( storedFilter, advancedToolbar, loadingError, - selectedLayer + selectedLayer, + mapComp: MapComp }) ); class QueryPanel extends React.Component { static propTypes = { - id: PropTypes.number, - buttonContent: PropTypes.node, - groups: PropTypes.array, - settings: PropTypes.object, - queryPanelEnabled: PropTypes.bool, - groupStyle: PropTypes.object, - groupPropertiesChangeHandler: PropTypes.func, - layerPropertiesChangeHandler: PropTypes.func, - onToggleGroup: PropTypes.func, - onToggleLayer: PropTypes.func, - onToggleQuery: PropTypes.func, - onZoomToExtent: PropTypes.func, - retrieveLayerData: PropTypes.func, - onSort: PropTypes.func, - onInit: PropTypes.func, - onSettings: PropTypes.func, - hideSettings: PropTypes.func, - updateSettings: PropTypes.func, - updateNode: PropTypes.func, - removeNode: PropTypes.func, - activateRemoveLayer: PropTypes.bool, - activateLegendTool: PropTypes.bool, - activateZoomTool: PropTypes.bool, - activateSettingsTool: PropTypes.bool, - visibilityCheckType: PropTypes.string, - settingsOptions: PropTypes.object, - layout: PropTypes.object, - toolsOptions: PropTypes.object, - appliedFilter: PropTypes.object, - storedFilter: PropTypes.object, advancedToolbar: PropTypes.bool, - onSaveFilter: PropTypes.func, - onRestoreFilter: PropTypes.func, + appliedFilter: PropTypes.object, items: PropTypes.array, - selectedLayer: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]) + layout: PropTypes.object, + loadingError: PropTypes.bool, + mapComp: PropTypes.node, + onInit: PropTypes.func, + onRestoreFilter: PropTypes.func, + onSaveFilter: PropTypes.func, + onToggleQuery: PropTypes.func, + queryPanelEnabled: PropTypes.bool, + selectedLayer: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + spatialMethodOptions: PropTypes.array, + spatialOperations: PropTypes.array, + storedFilter: PropTypes.object, + toolsOptions: PropTypes.object, + useEmbeddedMap: PropTypes.bool }; static defaultProps = { @@ -273,7 +260,8 @@ class QueryPanel extends React.Component { onSaveFilter: () => {}, onRestoreFilter: () => {}, items: [], - selectedLayer: false + selectedLayer: false, + useEmbeddedMap: false }; constructor(props) { super(props); @@ -284,41 +272,7 @@ class QueryPanel extends React.Component { this.props.onInit(); } } - getNoBackgroundLayers = (group) => { - return group.name !== 'background'; - }; - renderSidebar = () => { - return ( - -
- - ); - }; onToggle = () => { if (this.props.advancedToolbar && !isEqual(this.props.appliedFilter, this.props.storedFilter)) { this.setState(() => ({showModal: true})); @@ -326,17 +280,21 @@ class QueryPanel extends React.Component { this.props.onToggleQuery(); } } - restoreAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onRestoreFilter(); - this.props.onToggleQuery(); - } - storeAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onSaveFilter(); - this.props.onToggleQuery(); - } + + getNoBackgroundLayers = (group) => { + return group.name !== 'background'; + }; + renderQueryPanel = () => { + const MapComponent = this.props.mapComp; + if (this.props.useEmbeddedMap) { + standardItems.spatial = [{ + id: "spatialFilter", + plugin: SpatialFilterCustom, + cfg: {}, + position: 1 + }]; + } return (
+ {this.props.useEmbeddedMap ? +
+ +
+ : null}
); }; - + renderSidebar = () => { + return ( + +
+ + ); + }; render() { return this.renderSidebar(); } + + restoreAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onRestoreFilter(); + this.props.onToggleQuery(); + } + storeAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onSaveFilter(); + this.props.onToggleQuery(); + } } /** @@ -517,6 +522,7 @@ const QueryPanelPlugin = connect(tocSelector, { export default { QueryPanelPlugin, reducers: { + draw: drawReducers, queryform: queryformReducers, query: queryReducers, layerFilter: layerFilterReducers diff --git a/web/client/plugins/QueryPanelWithMap.jsx b/web/client/plugins/QueryPanelWithMap.jsx index 60e4605bd3..a89f280344 100644 --- a/web/client/plugins/QueryPanelWithMap.jsx +++ b/web/client/plugins/QueryPanelWithMap.jsx @@ -22,7 +22,7 @@ import queryReducers from '../reducers/query'; import draw from '../reducers/draw'; import mapReducers from '../reducers/map'; import queryformReducers from '../reducers/queryform'; -import { queryPanelWithMapSelector } from '../selectors/controls'; +import { queryPanelSelector } from '../selectors/controls'; import { isDashboardAvailable } from '../selectors/dashboard'; import { groupsSelector, selectedLayerLoadingErrorSelector } from '../selectors/layers'; import { mapSelector } from '../selectors/map'; @@ -146,7 +146,7 @@ const QueryPanelWithMapPlugin = connect( mapSelector, (state) => state.controls && state.controls.toolbar && state.controls.toolbar.active === 'toc', groupsSelector, - queryPanelWithMapSelector, + queryPanelSelector, state => mapLayoutValuesSelector(state, {height: true}), isDashboardAvailable, appliedFilterSelector, diff --git a/web/client/plugins/queryPanelWithMap/MapComp.jsx b/web/client/plugins/queryPanelWithMap/MapComp.jsx index fa97667062..298e4ea194 100644 --- a/web/client/plugins/queryPanelWithMap/MapComp.jsx +++ b/web/client/plugins/queryPanelWithMap/MapComp.jsx @@ -11,9 +11,11 @@ import { createSelector } from 'reselect'; import MapWithDraw from './MapWithDraw'; import { - getWidgetLayer, - getMapConfigSelector + getWidgetLayer } from '../../selectors/widgets'; +import { + getMapConfigSelector +} from '../../selectors/queryform'; const MapComp = connect( createSelector([ diff --git a/web/client/plugins/queryPanelWithMap/MapWithDraw.jsx b/web/client/plugins/queryPanelWithMap/MapWithDraw.jsx index 44b801fecb..d81d060e84 100644 --- a/web/client/plugins/queryPanelWithMap/MapWithDraw.jsx +++ b/web/client/plugins/queryPanelWithMap/MapWithDraw.jsx @@ -35,16 +35,17 @@ const MapWithDraw = ({ layer = {}, onMapReady = () => {} }) => { - return ( + return map ? ( - ); + ) : null; }; MapWithDraw.propTypes = { diff --git a/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx b/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx deleted file mode 100644 index 68da010af5..0000000000 --- a/web/client/plugins/queryPanelWithMap/PanelWithMap.jsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2023, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { isEqual } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Sidebar from 'react-sidebar'; - -import SpatialFilterCustom from './SpatialFilter'; -import standardItems from '../querypanel/index'; -import QueryPanelHeader from '../../components/data/query/QueryPanelHeader'; -import Portal from '../../components/misc/Portal'; -import ResizableModal from '../../components/misc/ResizableModal'; -import Message from '../locale/Message'; -import SmartQueryForm from './SmartQueryForm'; - -class PanelWithMap extends React.Component { - static propTypes = { - advancedToolbar: PropTypes.bool, - appliedFilter: PropTypes.object, - items: PropTypes.array, - layout: PropTypes.object, - loadingError: PropTypes.bool, - map: PropTypes.object, - mapComp: PropTypes.node, - onInit: PropTypes.func, - onRestoreFilter: PropTypes.func, - onSaveFilter: PropTypes.func, - onToggleQuery: PropTypes.func, - queryPanelEnabled: PropTypes.bool, - selectedLayer: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - spatialMethodOptions: PropTypes.array, - spatialOperations: PropTypes.array, - storedFilter: PropTypes.object, - toolsOptions: PropTypes.object - }; - - static defaultProps = { - items: [], - layout: {}, - loadingError: false, - onInit: () => {}, - onRestoreFilter: () => {}, - onSaveFilter: () => {}, - onToggleQuery: () => {}, - queryPanelEnabled: false, - selectedLayer: false, - toolsOptions: {} - }; - constructor(props) { - super(props); - this.state = {showModal: false}; - } - UNSAFE_componentWillReceiveProps(newProps) { - if (newProps.queryPanelEnabled === true && this.props.queryPanelEnabled === false) { - this.props.onInit(); - } - } - onToggle = () => { - if (this.props.advancedToolbar && !isEqual(this.props.appliedFilter, this.props.storedFilter)) { - this.setState(() => ({showModal: true})); - } else { - this.props.onToggleQuery(); - } - } - getNoBackgroundLayers = (group) => { - return group.name !== 'background'; - }; - renderSidebar = () => { - return ( - -
- - ); - }; - renderQueryPanel = () => { - const MapComponent = this.props.mapComp; - standardItems.spatial = [{ - id: "spatialFilter", - plugin: SpatialFilterCustom, - cfg: {}, - position: 1 - }]; - return (
- } - header={} - items={this.props.items} - loadingError={this.props.loadingError} - queryPanelEnabled={this.props.queryPanelEnabled} - selectedLayer={this.props.selectedLayer} - spatialMethodOptions={this.props.spatialMethodOptions} - spatialOperations={this.props.spatialOperations} - standardItems={standardItems} - storedFilter={this.props.storedFilter} - toolsOptions={this.props.toolsOptions} - /> -
- -
- - } - size="xs" - onClose={() => this.setState(() => ({showModal: false}))} - buttons={[ - { - bsStyle: 'primary', - text: , - onClick: this.storeAndClose - }, - { - bsStyle: 'primary', - text: , - onClick: this.restoreAndClose - } - ]}> -
-
- -
-
-
-
-
); - }; - - - render() { - return this.renderSidebar(); - } - restoreAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onRestoreFilter(); - this.props.onToggleQuery(); - } - storeAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onSaveFilter(); - this.props.onToggleQuery(); - } -} - -export default PanelWithMap; diff --git a/web/client/plugins/queryPanelWithMap/SmartQueryForm.jsx b/web/client/plugins/queryPanelWithMap/SmartQueryForm.jsx deleted file mode 100644 index 0e98e03db3..0000000000 --- a/web/client/plugins/queryPanelWithMap/SmartQueryForm.jsx +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2023, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; - - -import { toggleControl } from '../../actions/controls'; -import { changeDrawingStatus } from '../../actions/draw'; -import { applyFilter, discardCurrentFilter, storeCurrentFilter } from '../../actions/layerFilter'; -import { - addCrossLayerFilterField, - addFilterField, - addGroupField, - changeCascadingValue, - changeDwithinValue, - changeSpatialFilterValue, - expandAttributeFilterPanel, - expandCrossLayerFilterPanel, - expandSpatialFilterPanel, - removeCrossLayerFilterField, - removeFilterField, - removeGroupField, - removeSpatialSelection, - reset, - resetCrossLayerFilter, - search, - selectSpatialMethod, - selectSpatialOperation, - selectViewportSpatialMethod, - setCrossLayerFilterParameter, - showSpatialSelectionDetails, - toggleMenu, - updateCrossLayerFilterField, - updateExceptionField, - updateFilterField, - updateLogicCombo, - zoneChange, - zoneGetValues, - zoneSearch -} from '../../actions/queryform'; -import QueryBuilder from '../../components/data/query/QueryBuilder'; - -import { mapSelector } from '../../selectors/map'; -import { - availableCrossLayerFilterLayersSelector, - crossLayerFilterSelector -} from '../../selectors/queryform'; - - -const onReset = reset.bind(null, "query"); -// connecting a Dumb component to the store -// makes it a smart component -// we both connect state => props -// and actions to event handlers - -const SmartQueryForm = connect((state) => { - return { - // QueryBuilder props - useMapProjection: state.queryform.useMapProjection, - groupLevels: state.queryform.groupLevels, - groupFields: state.queryform.groupFields, - filterFields: state.queryform.filterFields, - attributes: state.query && state.query.typeName && state.query.featureTypes && state.query.featureTypes[state.query.typeName] && state.query.featureTypes[state.query.typeName].attributes, - featureTypeError: state.query && state.query.typeName && state.query.featureTypes && state.query.featureTypes[state.query.typeName] && state.query.featureTypes[state.query.typeName].error, - spatialField: state.queryform.spatialField, - filters: state.queryform.filters, - showDetailsPanel: state.queryform.showDetailsPanel, - toolbarEnabled: state.queryform.toolbarEnabled, - attributePanelExpanded: state.queryform.attributePanelExpanded, - autocompleteEnabled: state.queryform.autocompleteEnabled, - crossLayerExpanded: state.queryform.crossLayerExpanded, - crossLayerFilterOptions: { - layers: availableCrossLayerFilterLayersSelector(state), - crossLayerFilter: crossLayerFilterSelector(state), - ...(state.queryform.crossLayerFilterOptions || {}) - }, - maxFeaturesWPS: state.queryform.maxFeaturesWPS, - spatialPanelExpanded: state.queryform.spatialPanelExpanded, - featureTypeConfigUrl: state.query && state.query.url, - searchUrl: state.query && state.query.url, - featureTypeName: state.query && state.query.typeName, - ogcVersion: "1.1.0", - params: {typeName: state.query && state.query.typeName}, - resultTitle: "Query Result", - showGeneratedFilter: false, - allowEmptyFilter: true, - emptyFilterWarning: true, - maxHeight: state.map && state.map.present && state.map.present.size && state.map.present.size.height, - zoom: (mapSelector(state) || {}).zoom, - projection: (mapSelector(state) || {}).projection - }; -}, dispatch => { - return { - attributeFilterActions: bindActionCreators({ - onAddGroupField: addGroupField, - onAddFilterField: addFilterField, - onRemoveFilterField: removeFilterField, - onUpdateFilterField: updateFilterField, - onUpdateExceptionField: updateExceptionField, - onUpdateLogicCombo: updateLogicCombo, - onRemoveGroupField: removeGroupField, - onChangeCascadingValue: changeCascadingValue, - toggleMenu: toggleMenu, - onExpandAttributeFilterPanel: expandAttributeFilterPanel - }, dispatch), - spatialFilterActions: bindActionCreators({ - onChangeSpatialFilterValue: changeSpatialFilterValue, - onExpandSpatialFilterPanel: expandSpatialFilterPanel, - onSelectSpatialMethod: selectSpatialMethod, - onSelectViewportSpatialMethod: selectViewportSpatialMethod, - onSelectSpatialOperation: selectSpatialOperation, - onChangeDrawingStatus: changeDrawingStatus, - onRemoveSpatialSelection: removeSpatialSelection, - onShowSpatialSelectionDetails: showSpatialSelectionDetails, - onChangeDwithinValue: changeDwithinValue, - zoneFilter: zoneGetValues, - zoneSearch, - zoneChange - }, dispatch), - queryToolbarActions: bindActionCreators({ - onQuery: search, - onReset, - onSaveFilter: storeCurrentFilter, - onRestoreFilter: discardCurrentFilter, - storeAppliedFilter: applyFilter, - onChangeDrawingStatus: changeDrawingStatus - - }, dispatch), - crossLayerFilterActions: bindActionCreators({ - expandCrossLayerFilterPanel, - setCrossLayerFilterParameter, - addCrossLayerFilterField, - updateCrossLayerFilterField, - removeCrossLayerFilterField, - resetCrossLayerFilter, - toggleMenu: (rowId, status) => toggleMenu(rowId, status, "crossLayer") - }, dispatch), - controlActions: bindActionCreators({onToggleQuery: toggleControl.bind(null, 'queryPanelWithMap', null)}, dispatch) - }; -})(QueryBuilder); - -export default SmartQueryForm; diff --git a/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx b/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx index 6ce15e0fb1..de0f814295 100644 --- a/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx +++ b/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ import {connect} from "react-redux"; -import {getMapConfigSelector} from "../../selectors/widgets"; +import {getMapConfigSelector} from "../../selectors/queryform"; import {bindActionCreators} from "redux"; import { changeDwithinValue, diff --git a/web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js b/web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js index c556b0e099..aca260d504 100644 --- a/web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js +++ b/web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js @@ -8,7 +8,7 @@ import { compose, withStateHandlers, defaultProps, withPropsOnChange, withProps } from 'recompose'; import { isEmpty } from 'lodash'; -import { getCenterForExtent, getZoomForExtent } from '../../../utils/MapUtils'; +import { getCenterForExtent, getZoomForExtent, createRegisterHooks, ZOOM_TO_EXTENT_HOOK } from '../../../utils/MapUtils'; import { reprojectBbox, getExtentFromViewport } from '../../../utils/CoordinatesUtils'; const defaultBaseLayer = { @@ -33,15 +33,28 @@ const mapEnhancer = compose( withPropsOnChange("baseLayer", props => { return {layers: [props.baseLayer]}; }), + withPropsOnChange(["id"], + ({hookRegister = null}) => ({ + hookRegister: hookRegister || createRegisterHooks() + })), withStateHandlers(() => ({ initialized: false }), { onMapViewChanges: () => (map) => ( {map}), - onLayerLoad: ({map, initialized}, {onMapReady, baseLayer}) => (layerId) => { + onLayerLoad: ({map, initialized}, {onMapReady, baseLayer, hookRegister, layer}) => (layerId) => { // Map is ready when background is loaded just first load if (!initialized && layerId === baseLayer.id) { onMapReady(map); + const bounds4326 = layer.bbox.bounds; + const hook = hookRegister.getHook(ZOOM_TO_EXTENT_HOOK); + if (hook) { + // trigger "internal" zoom to extent + hook(bounds4326, { + crs: layer.bbox.crs, + maxZoom: 21 + }); + } return {initialized: true}; } return {}; diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index 3fb9081df8..ceef399b86 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -125,7 +125,6 @@ export const plugins = { OmniBarPlugin: toModulePlugin('OmniBar', () => import(/* webpackChunkName: 'plugins/omniBar' */ '../plugins/OmniBar')), PlaybackPlugin: toModulePlugin('Playback', () => import(/* webpackChunkName: 'plugins/playback' */ '../plugins/Playback')), QueryPanelPlugin: toModulePlugin('QueryPanel', () => import(/* webpackChunkName: 'plugins/queryPanel' */ '../plugins/QueryPanel')), - QueryPanelWithMapPlugin: toModulePlugin('QueryPanelWithMap', () => import(/* webpackChunkName: 'plugins/queryPanelWithMap' */ '../plugins/QueryPanelWithMap')), RedirectPlugin: toModulePlugin('Redirect', () => import(/* webpackChunkName: 'plugins/redirect' */ '../plugins/Redirect')), RedoPlugin: toModulePlugin('Redo', () => import(/* webpackChunkName: 'plugins/history' */ '../plugins/History')), SavePlugin: toModulePlugin('Save', () => import(/* webpackChunkName: 'plugins/save' */ '../plugins/Save')), diff --git a/web/client/reducers/__tests__/queryform-test.js b/web/client/reducers/__tests__/queryform-test.js index a5e9abfd2d..429afb9174 100644 --- a/web/client/reducers/__tests__/queryform-test.js +++ b/web/client/reducers/__tests__/queryform-test.js @@ -23,13 +23,28 @@ import { removeCrossLayerFilterField, changeSpatialFilterValue, upsertFilters, - removeFilters + removeFilters, + changeMapEditor } from '../../actions/queryform'; import { END_DRAWING, CHANGE_DRAWING_STATUS } from '../../actions/draw'; +import { setEditing } from '../../actions/dashboard'; +import { insertWidget } from '../../actions/widgets'; describe('Test the queryform reducer', () => { + it('CHANGE_MAP_EDITOR', () => { + const state = queryform(undefined, changeMapEditor(null)); + expect(state.map).toEqual(null); + }); + it('DASHBOARD:SET_EDITING', () => { + const state = queryform(undefined, setEditing(false)); + expect(state.map).toEqual(null); + }); + it('WIDGETS:INSERT', () => { + const state = queryform(undefined, insertWidget({})); + expect(state.map).toEqual(null); + }); it('returns the initial state on unrecognized action', () => { const initialState = { diff --git a/web/client/reducers/__tests__/widgets-test.js b/web/client/reducers/__tests__/widgets-test.js index bcc48fff36..57b92bcb4e 100644 --- a/web/client/reducers/__tests__/widgets-test.js +++ b/web/client/reducers/__tests__/widgets-test.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ import { - changeMapEditor, editWidget, editNewWidget, changeEditorSetting, @@ -44,10 +43,6 @@ describe('Test the widgets reducer', () => { expect(state.dependencies.center).toBe("map.center"); expect(state.dependencies.zoom).toBe("map.zoom"); }); - it('CHANGE_MAP_EDITOR', () => { - const state = widgets(undefined, changeMapEditor({bbox: {}})); - expect(state.builder.map).toEqual({bbox: {}}); - }); it('editNewWidget', () => { const state = widgets(undefined, editNewWidget({a: "A"}, {step: 0})); expect(state.builder.editor.a).toBe("A"); diff --git a/web/client/reducers/queryform.js b/web/client/reducers/queryform.js index 75ce6ca72b..43dd9c83a6 100644 --- a/web/client/reducers/queryform.js +++ b/web/client/reducers/queryform.js @@ -49,10 +49,13 @@ import { LOAD_FILTER, UPDATE_CROSS_LAYER_FILTER_FIELD_OPTIONS, UPSERT_FILTERS, - REMOVE_FILTERS + REMOVE_FILTERS, + CHANGE_MAP_EDITOR } from '../actions/queryform'; import { END_DRAWING, CHANGE_DRAWING_STATUS } from '../actions/draw'; +import { INSERT } from '../actions/widgets'; +import { SET_EDITING } from '../actions/dashboard'; import assign from 'object-assign'; import union from 'turf-union'; import bbox from 'turf-bbox'; @@ -85,7 +88,8 @@ const initialState = { operation: "INTERSECTS", geometry: null }, - simpleFilterFields: [] + simpleFilterFields: [], + map: null }; const updateFilterField = (field = {}, action = {}) => { @@ -102,6 +106,27 @@ const updateFilterField = (field = {}, action = {}) => { function queryform(state = initialState, action) { switch (action.type) { + case CHANGE_MAP_EDITOR: { + return { + ...state, + map: action.mapData + }; + } + case INSERT: { + return { + ...state, + map: null + }; + } + case SET_EDITING: { + if (!action.editing) { + return { + ...state, + map: null + }; + } + return state; + } case ADD_FILTER_FIELD: { // // Calculate the key number, this should be different for each new element diff --git a/web/client/reducers/widgets.js b/web/client/reducers/widgets.js index 05bda20fcd..16b5bf2506 100644 --- a/web/client/reducers/widgets.js +++ b/web/client/reducers/widgets.js @@ -14,7 +14,6 @@ import { UPDATE_LAYER, DELETE, EDITOR_CHANGE, - CHANGE_MAP_EDITOR, EDITOR_SETTING_CHANGE, CHANGE_LAYOUT, CLEAR_WIDGETS, @@ -100,15 +99,6 @@ function widgetsReducer(state = emptyState, action) { case EDITOR_CHANGE: { return editorChange(action, state); } - case CHANGE_MAP_EDITOR: { - return { - ...state, - builder: { - ...state.builder, - map: action.mapData - } - }; - } case INSERT: { let widget = {...action.widget}; if (widget.widgetType === 'chart') { diff --git a/web/client/selectors/__tests__/controls-test.js b/web/client/selectors/__tests__/controls-test.js index 8c85b6defe..7950d9c392 100644 --- a/web/client/selectors/__tests__/controls-test.js +++ b/web/client/selectors/__tests__/controls-test.js @@ -10,7 +10,6 @@ import expect from 'expect'; import { queryPanelSelector, - queryPanelWithMapSelector, wfsDownloadSelector, widgetBuilderAvailable, widgetBuilderSelector, @@ -27,9 +26,6 @@ const state = { queryPanel: { enabled: true }, - queryPanelWithMap: { - enabled: false - }, layerdownload: { available: true, enabled: true @@ -61,10 +57,6 @@ describe('Test controls selectors', () => { expect(retVal).toExist(); expect(retVal).toBe(true); }); - it('test queryPanelWithMapSelector', () => { - const retVal = queryPanelWithMapSelector(state); - expect(retVal).toBeFalsy(); - }); it('test wfsDownloadSelector', () => { const retVal = wfsDownloadSelector(state); expect(retVal).toExist(); diff --git a/web/client/selectors/__tests__/queryform-test.js b/web/client/selectors/__tests__/queryform-test.js index fd78c9c715..055f59a005 100644 --- a/web/client/selectors/__tests__/queryform-test.js +++ b/web/client/selectors/__tests__/queryform-test.js @@ -8,11 +8,14 @@ import expect from 'expect'; +import { set } from '../../utils/ImmutableUtils'; + import { availableCrossLayerFilterLayersSelector, spatialFieldSelector, spatialFieldGeomSelector, spatialFieldGeomTypeSelector, + getMapConfigSelector, spatialFieldGeomProjSelector, spatialFieldGeomCoordSelector, spatialFieldMethodSelector, @@ -97,6 +100,10 @@ const initialState = { }; describe('Test queryform selectors', () => { + it('getMapConfigSelector ', () => { + const state = set(`queryform.map`, { id: "map-id" }, {}); + expect(getMapConfigSelector(state)).toEqual({ id: "map-id" }); + }); it('spatialFieldSelector', () => { const spatialfield = spatialFieldSelector(initialState); expect(spatialfield).toExist(); diff --git a/web/client/selectors/__tests__/widgets-test.js b/web/client/selectors/__tests__/widgets-test.js index 96db903f44..989c084d7a 100644 --- a/web/client/selectors/__tests__/widgets-test.js +++ b/web/client/selectors/__tests__/widgets-test.js @@ -18,7 +18,6 @@ import { getEditingWidgetLayer, getEditingWidgetFilter, getEditorSettings, - getMapConfigSelector, getWidgetLayer, getWidgetAttributeFilter, dependenciesSelector, @@ -80,10 +79,6 @@ describe('widgets selectors', () => { expect(getEditorSettings(state)).toExist(); expect(getEditorSettings(state).flag).toBe(true); }); - it('getMapConfigSelector ', () => { - const state = set(`widgets.builder.map`, { id: "map-id" }, {}); - expect(getMapConfigSelector(state)).toEqual({ id: "map-id" }); - }); it('returnToFeatureGridSelector', () => { const state = set(`widgets.builder.editor`, { returnToFeatureGrid: true }, {}); expect(returnToFeatureGridSelector(state)).toExist(); diff --git a/web/client/selectors/controls.js b/web/client/selectors/controls.js index c5e7192b96..5e9f45ec6a 100644 --- a/web/client/selectors/controls.js +++ b/web/client/selectors/controls.js @@ -24,7 +24,6 @@ export const shareSelector = (state) => get(state, "controls.share.enabled"); export const measureSelector = (state) => get(state, "controls.measure.enabled"); export const versionInfoSelector = (state) => get(state, "controls.version.enabled"); export const queryPanelSelector = (state) => get(state, "controls.queryPanel.enabled"); -export const queryPanelWithMapSelector = (state) => get(state, "controls.queryPanelWithMap.enabled"); export const printSelector = (state) => get(state, "controls.print.enabled"); export const wfsDownloadSelector = state => !!get(state, "controls.layerdownload.enabled"); export const widgetBuilderAvailable = state => get(state, "controls.widgetBuilder.available", false); diff --git a/web/client/selectors/queryform.js b/web/client/selectors/queryform.js index e4d529b491..b5cf6d5bfe 100644 --- a/web/client/selectors/queryform.js +++ b/web/client/selectors/queryform.js @@ -12,6 +12,7 @@ import { layersSelector } from './layers'; import { currentLocaleSelector } from './locale'; import { getLocalizedProp } from '../utils/LocaleUtils'; +export const getMapConfigSelector = state => get(state, "queryform.map"); export const crossLayerFilterSelector = state => get(state, "queryform.crossLayerFilter"); // TODO we should also check if the layer are from the same source to allow cross layer filtering export const availableCrossLayerFilterLayersSelector = state =>(layersSelector(state) || []).filter(({type, group} = {}) => type === "wms" && group !== "background").map(({title, ...layer}) => ({...layer, title: getLocalizedProp(currentLocaleSelector(state), title)})); diff --git a/web/client/selectors/widgets.js b/web/client/selectors/widgets.js index 03b9a57304..40472ce7df 100644 --- a/web/client/selectors/widgets.js +++ b/web/client/selectors/widgets.js @@ -18,7 +18,7 @@ import { createSelector, createStructuredSelector } from 'reselect'; import { createShallowSelector } from '../utils/ReselectUtils'; import { getAttributesNames } from "../utils/FeatureGridUtils"; -export const getMapConfigSelector = state => get(state, "widgets.builder.map"); + export const getEditorSettings = state => get(state, "widgets.builder.settings"); export const getDependenciesMap = s => get(s, "widgets.dependencies") || {}; export const getDependenciesKeys = s => Object.keys(getDependenciesMap(s)).map(k => getDependenciesMap(s)[k]); diff --git a/web/client/utils/WidgetsUtils.js b/web/client/utils/WidgetsUtils.js index 8abb91155c..7fb894ac01 100644 --- a/web/client/utils/WidgetsUtils.js +++ b/web/client/utils/WidgetsUtils.js @@ -340,7 +340,6 @@ export const DEFAULT_MAP_SETTINGS = { y: 43.380053862794, crs: 'EPSG:4326' }, - zoom: 5, maxExtent: [ -20037508.34, -20037508.34, @@ -352,52 +351,7 @@ export const DEFAULT_MAP_SETTINGS = { width: 1300, height: 920 }, - bbox: { - bounds: { - minx: -3446291.017841229, - miny: 3136815.7816202897, - maxx: 5946291.017841229, - maxy: 7603184.218379708 - }, - crs: 'EPSG:3857', - rotation: 0 - }, version: 2, limits: {}, - mousePointer: 'pointer', - mapStateSource: 'mapWizard', - resolution: 4891.96981025128, - resolutions: [ - 156543.03392804097, - 78271.51696402048, - 39135.75848201024, - 19567.87924100512, - 9783.93962050256, - 4891.96981025128, - 2445.98490512564, - 1222.99245256282, - 611.49622628141, - 305.748113140705, - 152.8740565703525, - 76.43702828517625, - 38.21851414258813, - 19.109257071294063, - 9.554628535647032, - 4.777314267823516, - 2.388657133911758, - 1.194328566955879, - 0.5971642834779395, - 0.29858214173896974, - 0.14929107086948487, - 0.07464553543474244, - 0.03732276771737122, - 0.01866138385868561, - 0.009330691929342804, - 0.004665345964671402, - 0.002332672982335701, - 0.0011663364911678506, - 0.0005831682455839253, - 0.00029158412279196264, - 0.00014579206139598132 - ] + mousePointer: 'pointer' }; From 55e812cf4da429ba394cebf31e6b2b95e35847c9 Mon Sep 17 00:00:00 2001 From: MV88 Date: Wed, 11 Oct 2023 10:52:21 +0200 Subject: [PATCH 05/21] removed QueryPanelWithMap plugin --- web/client/plugins/QueryPanelWithMap.jsx | 188 ----------------------- 1 file changed, 188 deletions(-) delete mode 100644 web/client/plugins/QueryPanelWithMap.jsx diff --git a/web/client/plugins/QueryPanelWithMap.jsx b/web/client/plugins/QueryPanelWithMap.jsx deleted file mode 100644 index a89f280344..0000000000 --- a/web/client/plugins/QueryPanelWithMap.jsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2023, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; - -import { toggleControl } from '../actions/controls'; -import { discardCurrentFilter, storeCurrentFilter } from '../actions/layerFilter'; -import { initQueryPanel } from '../actions/wfsquery'; -import PanelWithMap from './queryPanelWithMap/PanelWithMap'; -import autocompleteEpics from '../epics/autocomplete'; -import layerFilterEpics from '../epics/layerfilter'; -import queryFormEpics from '../epics/queryform'; -import { featureTypeSelectedEpic, redrawSpatialFilterEpic, viewportSelectedEpic, wfsQueryEpic } from '../epics/wfsquery'; -import layerFilterReducers from '../reducers/layerFilter'; -import queryReducers from '../reducers/query'; -import draw from '../reducers/draw'; -import mapReducers from '../reducers/map'; -import queryformReducers from '../reducers/queryform'; -import { queryPanelSelector } from '../selectors/controls'; -import { isDashboardAvailable } from '../selectors/dashboard'; -import { groupsSelector, selectedLayerLoadingErrorSelector } from '../selectors/layers'; -import { mapSelector } from '../selectors/map'; -import { mapLayoutValuesSelector } from '../selectors/maplayout'; -import { appliedFilterSelector, storedFilterSelector } from '../selectors/queryform'; -import { typeNameSelector } from "../selectors/query"; -import MapComp from './queryPanelWithMap/MapComp'; - - -/** - * @class - * @classdesc - * QueryPanelPlugin allow to query a layer in different ways, using attributes of that layer, spatial filters - * @name QueryPanel - * @memberof plugins - * @prop {boolean} cfg.activateQueryTool: Activate query tool options, default `false` - * @prop {object[]} cfg.spatialMethodOptions: The list of geometric methods use to create/draw the spatial filter
- * Here you can configure a list of methods used to draw (BBOX, Circle, Polygon) or create (Viewport and wfsGeocoder types) regarding the wfsGeocoder.
The options for wfsGeocoder are: - * - id: id of the method - * - name: label used in the DropdownList - * - type: must be wfsGeocoder - * - customItemClassName: a custom class for used for this method in the DropdownList - * - geodesic: {bool} draw a geodesic geometry for filter (supported only for Circle) - * - filterProps: - * - blacklist {string[]} a list of banned words excluded from the wfs search - * - maxFeatures {number} the maximum features fetched per request - * - predicate {string} the cql predicate - * - querableAttributes {string[]} list of attributes to query on. - * - typeName {string} the workspace + layer name on geoserver - * - valueField {string} the attribute from features properties used as value/label in the autocomplete list - * - srsName {string} The projection of the requested features fetched via wfs - * Plugin acts as container and by default it have three panels: "AttributesFilter", "SpatialFilter" and "CrossLayerFilter" (see "standardItems" variable) - * Panels can be customized by injection from another plugins (see example below). - * Targets available for injection: "start", "attributes", "afterAttributes", "spatial", "afterSpatial", "layers", "end". - - * @prop {object[]} cfg.spatialOperations: The list of geometric operations use to create the spatial filter.
- * @prop {boolean} cfg.toolsOptions.hideCrossLayer force cross layer filter panel to hide (when is not used or not usable) - * @prop {boolean} cfg.toolsOptions.hideAttributeFilter force attribute filter panel to hide (when is not used or not usable). In general any `hide${CapitailizedItemId}` works to hide a particular panel of the query panel. - * @prop {boolean} cfg.toolsOptions.hideSpatialFilter force spatial filter panel to hide (when is not used or not usable) - * - * @example - * // This example configure a layer with polygons geometry as spatial filter method - * "spatialOperations": [ - * {"id": "INTERSECTS", "name": "queryform.spatialfilter.operations.intersects"}, - * {"id": "BBOX", "name": "queryform.spatialfilter.operations.bbox"}, - * {"id": "CONTAINS", "name": "queryform.spatialfilter.operations.contains"}, - * {"id": "WITHIN", "name": "queryform.spatialfilter.operations.within"}, - * {"id": "DWITHIN", "name": "queryform.spatialfilter.operations.dwithin"} - * ], - * "spatialMethodOptions": [ - * {"id": "Viewport", "name": "queryform.spatialfilter.methods.viewport"}, - * {"id": "BBOX", "name": "queryform.spatialfilter.methods.box"}, - * {"id": "Circle", "name": "queryform.spatialfilter.methods.circle"}, - * {"id": "Polygon", "name": "queryform.spatialfilter.methods.poly"}, - * { - * "id": "methodId", - * "name": "methodName", - * "type": "wfsGeocoder", - * "url": "urlToGeoserver", - * "crossLayer": { // if this is present, allows to optimize the filter using crossLayerFilter functinalities instead of geometry. The server must support them - * "cqlTemplate": "ATTRIBUTE_Y = '${properties.ATTRIBUTE_Y}'", // a template to generate the filter from the feature properties - * "geometryName": "GEOMETRY", - * "typeName": "workspace:typeName" - * }, - * "filterProps": { - * "blacklist": [], - * "maxFeatures": 5, - * "predicate": "LIKE", - * "queriableAttributes": ["ATTRIBUTE_X"], - * "typeName": "workspace:typeName", - * "valueField": "ATTRIBUTE_Y", - * "srsName": "EPSG:3857" - * }, - * "customItemClassName": "customItemClassName" - * } - * - * @example - * // customize the QueryPanels UI via plugin(s) - * import {createPlugin} from "../utils/PluginsUtils"; - * - * export default createPlugin('QueryPanelCustomizations', { - * component: () => null, - * containers: { - * QueryPanel: [ - * // Hide the attribute filter by injecting a `component: () => null` for one of the default panels, e.g. `attributeFilter`. - * { - * id: 'attributeFilter', - * component: () => null, - * target: 'attributes', - * position: 0, - * layerNameRegex: "^gs:us_states__[0-9]*" - * }, - * // adds a panel after the attribute panel (if present) at position `0` - * { - * id: 'attributeFilterNew', - * component: () => 'Sample text', - * target: 'attributes', - * position: 0 - * }, - * { - * id: 'customPanel', - * component: () => 'Panel content; Added to attributes target', - * target: 'attributes', - * position: 3 - * }, - * { - * id: 'customPanel2', - * component: () => 'Another panel added to start', - * target: 'start', - * position: 3 - * } - * ] - * } - * }); - */ - - -const QueryPanelWithMapPlugin = connect( - createSelector( - [ - mapSelector, - (state) => state.controls && state.controls.toolbar && state.controls.toolbar.active === 'toc', - groupsSelector, - queryPanelSelector, - state => mapLayoutValuesSelector(state, {height: true}), - isDashboardAvailable, - appliedFilterSelector, - storedFilterSelector, - (state) => state && state.query && state.query.isLayerFilter, - selectedLayerLoadingErrorSelector, - typeNameSelector - ], (map, enabled, groups, queryPanelEnabled, layoutHeight, dashboardAvailable, appliedFilter, storedFilter, advancedToolbar, loadingError, selectedLayer) => ({ - map, - enabled, - groups, - queryPanelEnabled, - layout: !dashboardAvailable ? layoutHeight : {}, - mapComp: MapComp, - appliedFilter, - storedFilter, - advancedToolbar, - loadingError, - selectedLayer - }) - ), { - onToggleQuery: toggleControl.bind(null, 'queryPanelWithMap', null), - onInit: initQueryPanel, - onSaveFilter: storeCurrentFilter, - onRestoreFilter: discardCurrentFilter - })(PanelWithMap); - - -export default { - QueryPanelWithMapPlugin, - reducers: { - draw, - queryform: queryformReducers, - map: mapReducers, - query: queryReducers, - layerFilter: layerFilterReducers - }, - epics: {featureTypeSelectedEpic, wfsQueryEpic, viewportSelectedEpic, redrawSpatialFilterEpic, ...autocompleteEpics, ...queryFormEpics, ...layerFilterEpics} -}; From f7ba032f41a40d2cb48dd0bbd59edb8e58aee323 Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 11 Oct 2023 11:46:03 +0200 Subject: [PATCH 06/21] Reorganize panel usage and documented new configuration --- .../components/data/query/QueryBuilder.jsx | 4 +- web/client/configs/localConfig.json | 4 +- web/client/plugins/QueryPanel.jsx | 20 ++----- .../queryPanelWithMap/SpatialFilter.jsx | 52 ------------------- .../MapComp.jsx | 0 .../MapWithDraw.jsx | 0 .../plugins/querypanel/SpatialFilter.jsx | 28 ++++++++-- .../plugins/querypanel/SpatialFilterMap.jsx | 9 ++++ .../enhancers/mapEnhancer.js | 0 web/client/plugins/querypanel/index.js | 9 +++- 10 files changed, 49 insertions(+), 77 deletions(-) delete mode 100644 web/client/plugins/queryPanelWithMap/SpatialFilter.jsx rename web/client/plugins/{queryPanelWithMap => querypanel}/MapComp.jsx (100%) rename web/client/plugins/{queryPanelWithMap => querypanel}/MapWithDraw.jsx (100%) create mode 100644 web/client/plugins/querypanel/SpatialFilterMap.jsx rename web/client/plugins/{queryPanelWithMap => querypanel}/enhancers/mapEnhancer.js (100%) diff --git a/web/client/components/data/query/QueryBuilder.jsx b/web/client/components/data/query/QueryBuilder.jsx index 8ad141ba70..021dc79bb1 100644 --- a/web/client/components/data/query/QueryBuilder.jsx +++ b/web/client/components/data/query/QueryBuilder.jsx @@ -207,7 +207,7 @@ class QueryBuilder extends React.Component { />
); const { spatialMethodOptions, toolsOptions, spatialOperations} = this.props; return this.props.attributes.length > 0 ? - + <> {this.renderItems('start', { spatialOperations, spatialMethodOptions, ...toolsOptions })} {this.renderItems('attributes', { spatialOperations, spatialMethodOptions, ...toolsOptions })} {this.renderItems('afterAttributes', { spatialOperations, spatialMethodOptions, ...toolsOptions })} @@ -216,6 +216,8 @@ class QueryBuilder extends React.Component { {this.renderItems('layers', { spatialOperations, spatialMethodOptions, ...toolsOptions })} {this.renderItems('end', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + {this.renderItems('map', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + :
; } diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index fc3807a029..69a108fe12 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -812,9 +812,9 @@ }, { "name": "QueryPanel", "cfg": { - "useEmbeddedMap": true, "toolsOptions": { - "hideCrossLayer": true + "hideCrossLayer": true, + "useEmbeddedMap": true }, "spatialPanelExpanded": false, "spatialOperations": [ diff --git a/web/client/plugins/QueryPanel.jsx b/web/client/plugins/QueryPanel.jsx index 70f48ab537..9f82b4afe2 100644 --- a/web/client/plugins/QueryPanel.jsx +++ b/web/client/plugins/QueryPanel.jsx @@ -20,8 +20,7 @@ import { changeDrawingStatus } from '../actions/draw'; import { getLayerCapabilities } from '../actions/layerCapabilities'; import { queryPanelSelector } from '../selectors/controls'; import { applyFilter, discardCurrentFilter, storeCurrentFilter } from '../actions/layerFilter'; -import MapComp from './queryPanelWithMap/MapComp'; -import SpatialFilterCustom from './queryPanelWithMap/SpatialFilter'; +import MapComp from './querypanel/MapComp'; import { changeGroupProperties, @@ -286,15 +285,6 @@ class QueryPanel extends React.Component { }; renderQueryPanel = () => { - const MapComponent = this.props.mapComp; - if (this.props.useEmbeddedMap) { - standardItems.spatial = [{ - id: "spatialFilter", - plugin: SpatialFilterCustom, - cfg: {}, - position: 1 - }]; - } return (
- {this.props.useEmbeddedMap ? -
- -
- : null} * @prop {boolean} cfg.toolsOptions.hideCrossLayer force cross layer filter panel to hide (when is not used or not usable) * @prop {boolean} cfg.toolsOptions.hideAttributeFilter force attribute filter panel to hide (when is not used or not usable). In general any `hide${CapitailizedItemId}` works to hide a particular panel of the query panel. * @prop {boolean} cfg.toolsOptions.hideSpatialFilter force spatial filter panel to hide (when is not used or not usable) + * @prop {boolean} cfg.toolsOptions.useEmbeddedMap if spatial filter panel is present, this option allows to use the embedded map instead of the map plugin * * @example * // This example configure a layer with polygons geometry as spatial filter method diff --git a/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx b/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx deleted file mode 100644 index de0f814295..0000000000 --- a/web/client/plugins/queryPanelWithMap/SpatialFilter.jsx +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright 2023, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -import {connect} from "react-redux"; -import {getMapConfigSelector} from "../../selectors/queryform"; -import {bindActionCreators} from "redux"; -import { - changeDwithinValue, - changeSpatialFilterValue, - expandSpatialFilterPanel, - removeSpatialSelection, - selectSpatialMethod, - selectSpatialOperation, - selectViewportSpatialMethod, showSpatialSelectionDetails, - zoneChange, zoneGetValues, zoneSearch -} from "../../actions/queryform"; -import {changeDrawingStatus} from "../../actions/draw"; -import SpatialFilterComponent from "../../components/data/query/SpatialFilter"; - -const SpatialFilter = connect((state) => { - return { - useMapProjection: state.queryform.useMapProjection, - spatialField: state.queryform.spatialField, - showDetailsPanel: state.queryform.showDetailsPanel, - spatialPanelExpanded: state.queryform.spatialPanelExpanded, - zoom: (getMapConfigSelector(state) || {}).zoom, - projection: (getMapConfigSelector(state) || {}).projection - }; -}, dispatch => { - return { - actions: bindActionCreators({ - onChangeSpatialFilterValue: changeSpatialFilterValue, - onExpandSpatialFilterPanel: expandSpatialFilterPanel, - onSelectSpatialMethod: selectSpatialMethod, - onSelectViewportSpatialMethod: selectViewportSpatialMethod, - onSelectSpatialOperation: selectSpatialOperation, - onChangeDrawingStatus: changeDrawingStatus, - onRemoveSpatialSelection: removeSpatialSelection, - onShowSpatialSelectionDetails: showSpatialSelectionDetails, - onChangeDwithinValue: changeDwithinValue, - zoneFilter: zoneGetValues, - zoneSearch, - zoneChange - }, dispatch) - }; -})(SpatialFilterComponent); - -export default SpatialFilter; diff --git a/web/client/plugins/queryPanelWithMap/MapComp.jsx b/web/client/plugins/querypanel/MapComp.jsx similarity index 100% rename from web/client/plugins/queryPanelWithMap/MapComp.jsx rename to web/client/plugins/querypanel/MapComp.jsx diff --git a/web/client/plugins/queryPanelWithMap/MapWithDraw.jsx b/web/client/plugins/querypanel/MapWithDraw.jsx similarity index 100% rename from web/client/plugins/queryPanelWithMap/MapWithDraw.jsx rename to web/client/plugins/querypanel/MapWithDraw.jsx diff --git a/web/client/plugins/querypanel/SpatialFilter.jsx b/web/client/plugins/querypanel/SpatialFilter.jsx index 1d31ec2819..09afed2e18 100644 --- a/web/client/plugins/querypanel/SpatialFilter.jsx +++ b/web/client/plugins/querypanel/SpatialFilter.jsx @@ -5,8 +5,10 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ +import React from 'react'; import {connect} from "react-redux"; import {mapSelector} from "../../selectors/map"; +import {getMapConfigSelector} from "../../selectors/queryform"; import {bindActionCreators} from "redux"; import { changeDwithinValue, @@ -21,14 +23,12 @@ import { import {changeDrawingStatus} from "../../actions/draw"; import SpatialFilterComponent from "../../components/data/query/SpatialFilter"; -const SpatialFilter = connect((state) => { +const BaseSpatialFilter = connect((state) => { return { useMapProjection: state.queryform.useMapProjection, spatialField: state.queryform.spatialField, showDetailsPanel: state.queryform.showDetailsPanel, - spatialPanelExpanded: state.queryform.spatialPanelExpanded, - zoom: (mapSelector(state) || {}).zoom, - projection: (mapSelector(state) || {}).projection + spatialPanelExpanded: state.queryform.spatialPanelExpanded }; }, dispatch => { return { @@ -49,4 +49,24 @@ const SpatialFilter = connect((state) => { }; })(SpatialFilterComponent); +/** + * Connected to the Map plugin + */ +const SpatialFilterMapPlugin = connect((state) => ({ + zoom: (mapSelector(state) || {}).zoom, + projection: (mapSelector(state) || {}).projection +}))(BaseSpatialFilter); + +/** + * Connected to the embedded map of the query panel + */ +export const SpatialFilterEmbeddedMap = connect((state) => ({ + zoom: (getMapConfigSelector(state) || {}).zoom, + projection: (getMapConfigSelector(state) || {}).projection +}))(BaseSpatialFilter); + +export const SpatialFilter = ({useEmbeddedMap, ...props}) => { + return useEmbeddedMap ? : ; +}; + export default SpatialFilter; diff --git a/web/client/plugins/querypanel/SpatialFilterMap.jsx b/web/client/plugins/querypanel/SpatialFilterMap.jsx new file mode 100644 index 0000000000..c4bc5b36f9 --- /dev/null +++ b/web/client/plugins/querypanel/SpatialFilterMap.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import MapComponent from './MapComp'; +export default ({useEmbeddedMap, ...props}) => + useEmbeddedMap ? + (
+ +
) + : null; + diff --git a/web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js b/web/client/plugins/querypanel/enhancers/mapEnhancer.js similarity index 100% rename from web/client/plugins/queryPanelWithMap/enhancers/mapEnhancer.js rename to web/client/plugins/querypanel/enhancers/mapEnhancer.js diff --git a/web/client/plugins/querypanel/index.js b/web/client/plugins/querypanel/index.js index a94877dc46..26bbe0dd1e 100644 --- a/web/client/plugins/querypanel/index.js +++ b/web/client/plugins/querypanel/index.js @@ -1,6 +1,7 @@ import AttributeFilter from "./AttributeFilter"; import SpatialFilter from "./SpatialFilter"; import CrossLayerFilter from "./CrossLayerFilter"; +import SpatialFilterMap from './SpatialFilterMap'; const standardItems = { start: [], @@ -30,7 +31,13 @@ const standardItems = { position: 1 } ], - end: [] + end: [], + map: [{ + id: "spatialFilterMap", + plugin: SpatialFilterMap, + cfg: {}, + position: 1 + }] }; export default standardItems; From dee98d9454b70340c887b77c7264b40b6b0e590e Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 11 Oct 2023 11:47:56 +0200 Subject: [PATCH 07/21] Better indentation for QueryBuilder --- .../components/data/query/QueryBuilder.jsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/web/client/components/data/query/QueryBuilder.jsx b/web/client/components/data/query/QueryBuilder.jsx index 021dc79bb1..ecdbbd1e56 100644 --- a/web/client/components/data/query/QueryBuilder.jsx +++ b/web/client/components/data/query/QueryBuilder.jsx @@ -207,16 +207,17 @@ class QueryBuilder extends React.Component { />
); const { spatialMethodOptions, toolsOptions, spatialOperations} = this.props; return this.props.attributes.length > 0 ? - <> - {this.renderItems('start', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - {this.renderItems('attributes', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - {this.renderItems('afterAttributes', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - {this.renderItems('spatial', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - {this.renderItems('afterSpatial', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - {this.renderItems('layers', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - {this.renderItems('end', { spatialOperations, spatialMethodOptions, ...toolsOptions })} - - {this.renderItems('map', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + <> + + {this.renderItems('start', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + {this.renderItems('attributes', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + {this.renderItems('afterAttributes', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + {this.renderItems('spatial', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + {this.renderItems('afterSpatial', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + {this.renderItems('layers', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + {this.renderItems('end', { spatialOperations, spatialMethodOptions, ...toolsOptions })} + + {this.renderItems('map', { spatialOperations, spatialMethodOptions, ...toolsOptions })} :
; } From f461e6fea8271255ad7b629aa6ea5c50780c840a Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 11 Oct 2023 12:02:38 +0200 Subject: [PATCH 08/21] clean up QueryPanel unnecessary changes --- web/client/plugins/QueryPanel.jsx | 106 ++++++++++++++---------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/web/client/plugins/QueryPanel.jsx b/web/client/plugins/QueryPanel.jsx index 9f82b4afe2..390c7ff02e 100644 --- a/web/client/plugins/QueryPanel.jsx +++ b/web/client/plugins/QueryPanel.jsx @@ -20,7 +20,6 @@ import { changeDrawingStatus } from '../actions/draw'; import { getLayerCapabilities } from '../actions/layerCapabilities'; import { queryPanelSelector } from '../selectors/controls'; import { applyFilter, discardCurrentFilter, storeCurrentFilter } from '../actions/layerFilter'; -import MapComp from './querypanel/MapComp'; import { changeGroupProperties, @@ -209,8 +208,7 @@ const tocSelector = createSelector( storedFilter, advancedToolbar, loadingError, - selectedLayer, - mapComp: MapComp + selectedLayer }) ); @@ -221,7 +219,6 @@ class QueryPanel extends React.Component { items: PropTypes.array, layout: PropTypes.object, loadingError: PropTypes.bool, - mapComp: PropTypes.node, onInit: PropTypes.func, onRestoreFilter: PropTypes.func, onSaveFilter: PropTypes.func, @@ -231,8 +228,7 @@ class QueryPanel extends React.Component { spatialMethodOptions: PropTypes.array, spatialOperations: PropTypes.array, storedFilter: PropTypes.object, - toolsOptions: PropTypes.object, - useEmbeddedMap: PropTypes.bool + toolsOptions: PropTypes.object }; static defaultProps = { @@ -259,19 +255,55 @@ class QueryPanel extends React.Component { onSaveFilter: () => {}, onRestoreFilter: () => {}, items: [], - selectedLayer: false, - useEmbeddedMap: false + selectedLayer: false }; constructor(props) { super(props); this.state = {showModal: false}; } + UNSAFE_componentWillReceiveProps(newProps) { if (newProps.queryPanelEnabled === true && this.props.queryPanelEnabled === false) { this.props.onInit(); } } + getNoBackgroundLayers = (group) => { + return group.name !== 'background'; + }; + + renderSidebar = () => { + return ( + +
+ + ); + }; onToggle = () => { if (this.props.advancedToolbar && !isEqual(this.props.appliedFilter, this.props.storedFilter)) { this.setState(() => ({showModal: true})); @@ -279,10 +311,16 @@ class QueryPanel extends React.Component { this.props.onToggleQuery(); } } - - getNoBackgroundLayers = (group) => { - return group.name !== 'background'; - }; + restoreAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onRestoreFilter(); + this.props.onToggleQuery(); + } + storeAndClose = () => { + this.setState(() => ({showModal: false})); + this.props.onSaveFilter(); + this.props.onToggleQuery(); + } renderQueryPanel = () => { return (
@@ -329,53 +367,11 @@ class QueryPanel extends React.Component {
); }; - renderSidebar = () => { - return ( - -
- - ); - }; + render() { return this.renderSidebar(); } - - restoreAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onRestoreFilter(); - this.props.onToggleQuery(); - } - storeAndClose = () => { - this.setState(() => ({showModal: false})); - this.props.onSaveFilter(); - this.props.onToggleQuery(); - } } /** From 2a2effdb0c5e3e7f63e9a438d26ec8cd754c489c Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 11 Oct 2023 12:07:52 +0200 Subject: [PATCH 09/21] Removed unncecessary component file --- web/client/plugins/querypanel/MapComp.jsx | 34 ----------------- .../plugins/querypanel/SpatialFilterMap.jsx | 37 ++++++++++++++++++- 2 files changed, 36 insertions(+), 35 deletions(-) delete mode 100644 web/client/plugins/querypanel/MapComp.jsx diff --git a/web/client/plugins/querypanel/MapComp.jsx b/web/client/plugins/querypanel/MapComp.jsx deleted file mode 100644 index 298e4ea194..0000000000 --- a/web/client/plugins/querypanel/MapComp.jsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; - -import MapWithDraw from './MapWithDraw'; -import { - getWidgetLayer -} from '../../selectors/widgets'; -import { - getMapConfigSelector -} from '../../selectors/queryform'; - -const MapComp = connect( - createSelector([ - getWidgetLayer, - getMapConfigSelector - ], (layer, map) => { - return { - layer, - map, - mapStateSource: "wizardMap", - containerSelector: ".mapstore-query-map" - }; - } - ), {} )(MapWithDraw); - -export default MapComp; diff --git a/web/client/plugins/querypanel/SpatialFilterMap.jsx b/web/client/plugins/querypanel/SpatialFilterMap.jsx index c4bc5b36f9..58b50e0de4 100644 --- a/web/client/plugins/querypanel/SpatialFilterMap.jsx +++ b/web/client/plugins/querypanel/SpatialFilterMap.jsx @@ -1,5 +1,40 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ import React from 'react'; -import MapComponent from './MapComp'; + +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import MapWithDraw from './MapWithDraw'; +import { + getWidgetLayer +} from '../../selectors/widgets'; +import { + getMapConfigSelector +} from '../../selectors/queryform'; + +/** + * Component connected to the widgetLayer + */ +export const MapComponent = connect( + createSelector([ + getWidgetLayer, + getMapConfigSelector + ], (layer, map) => { + return { + layer, + map, + mapStateSource: "wizardMap", + containerSelector: ".mapstore-query-map" + }; + } + ), {} )(MapWithDraw); + export default ({useEmbeddedMap, ...props}) => useEmbeddedMap ? (
From e2e5b70e90c9dadaf371ae6fc60b6ab34612388f Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 11 Oct 2023 15:26:21 +0200 Subject: [PATCH 10/21] Removed unused portal --- web/client/plugins/querypanel/MapWithDraw.jsx | 22 ++++++++----------- .../plugins/querypanel/SpatialFilterMap.jsx | 7 +++--- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/web/client/plugins/querypanel/MapWithDraw.jsx b/web/client/plugins/querypanel/MapWithDraw.jsx index d81d060e84..359ac36e84 100644 --- a/web/client/plugins/querypanel/MapWithDraw.jsx +++ b/web/client/plugins/querypanel/MapWithDraw.jsx @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ import React from 'react'; -import { Portal } from 'react-overlays'; import { compose } from 'recompose'; import PropTypes from 'prop-types'; @@ -29,27 +28,24 @@ const MapWitDrawComp = compose( const MapWithDraw = ({ - containerSelector, map, mapStateSource, layer = {}, onMapReady = () => {} }) => { return map ? ( - - - ) : null; + + ) : null; }; MapWithDraw.propTypes = { - containerSelector: PropTypes.string, map: PropTypes.object, mapStateSource: PropTypes.string, onMapReady: PropTypes.bool, diff --git a/web/client/plugins/querypanel/SpatialFilterMap.jsx b/web/client/plugins/querypanel/SpatialFilterMap.jsx index 58b50e0de4..a7d4a26503 100644 --- a/web/client/plugins/querypanel/SpatialFilterMap.jsx +++ b/web/client/plugins/querypanel/SpatialFilterMap.jsx @@ -29,14 +29,13 @@ export const MapComponent = connect( return { layer, map, - mapStateSource: "wizardMap", - containerSelector: ".mapstore-query-map" + mapStateSource: "wizardMap" }; } ), {} )(MapWithDraw); -export default ({useEmbeddedMap, ...props}) => - useEmbeddedMap ? +export default ({useEmbeddedMap, hideSpatialFilter, ...props}) => + useEmbeddedMap && !hideSpatialFilter ? (
) From ff45230d6d372e4dc64870dc40d0a57b104045c0 Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 11 Oct 2023 17:02:38 +0200 Subject: [PATCH 11/21] Removed specific dashboard css overrides using portal --- web/client/plugins/querypanel/MapWithDraw.jsx | 2 +- .../plugins/querypanel/SpatialFilterMap.jsx | 23 +++++++++++++----- web/client/themes/default/less/dashboard.less | 24 ------------------- .../themes/default/less/query-panel.less | 23 ++++++++++++++++++ 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/web/client/plugins/querypanel/MapWithDraw.jsx b/web/client/plugins/querypanel/MapWithDraw.jsx index 359ac36e84..a7b239b6cd 100644 --- a/web/client/plugins/querypanel/MapWithDraw.jsx +++ b/web/client/plugins/querypanel/MapWithDraw.jsx @@ -39,7 +39,7 @@ const MapWithDraw = ({ mapStateSource={mapStateSource} onMapReady={onMapReady} zoomControl - options={{ style: { margin: 10, height: 'calc(100% - 20px)' }}} + options={{ style: { height: 'calc(100% - 20px)' }}} layer={layer} tools={["draw"]}/> ) : null; diff --git a/web/client/plugins/querypanel/SpatialFilterMap.jsx b/web/client/plugins/querypanel/SpatialFilterMap.jsx index a7d4a26503..92d748b793 100644 --- a/web/client/plugins/querypanel/SpatialFilterMap.jsx +++ b/web/client/plugins/querypanel/SpatialFilterMap.jsx @@ -9,6 +9,9 @@ import React from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import Portal from '../../components/misc/Portal'; + +import withContainer from '../../components/misc/WithContainer'; import MapWithDraw from './MapWithDraw'; import { @@ -34,10 +37,18 @@ export const MapComponent = connect( } ), {} )(MapWithDraw); -export default ({useEmbeddedMap, hideSpatialFilter, ...props}) => - useEmbeddedMap && !hideSpatialFilter ? - (
- -
) +export default withContainer((props) => { + const { + container, + useEmbeddedMap, + hideSpatialFilter, + queryPanelEnabled + } = props; + return useEmbeddedMap && !hideSpatialFilter && queryPanelEnabled ? + ( +
+ +
+
) : null; - +}); diff --git a/web/client/themes/default/less/dashboard.less b/web/client/themes/default/less/dashboard.less index 6a46f8216f..4fdcee055b 100644 --- a/web/client/themes/default/less/dashboard.less +++ b/web/client/themes/default/less/dashboard.less @@ -35,31 +35,7 @@ position: relative; overflow: auto; } - .query-form-root { - width: 100% !important; - .query-form-panel-container { - .spinner-panel{ - width: 600px !important; - } - width: 100% !important; - transition: unset !important; - .mapstore-query-builder { - position: unset; - display: flex; - #query-form-panel { - width: 600px !important; - } - .mapstore-query-map { - #__base_map__ { - margin: 0 !important; - height: 100% !important; - } - flex: 1 - } - } - } - } } .dashboard-editor { diff --git a/web/client/themes/default/less/query-panel.less b/web/client/themes/default/less/query-panel.less index d26059ab83..19a474c036 100644 --- a/web/client/themes/default/less/query-panel.less +++ b/web/client/themes/default/less/query-panel.less @@ -38,6 +38,10 @@ .border-left-color-var(@theme-vars[main-color]); } } + .mapstore-query-map { + .background-color-var(@theme-vars[main-bg]); + box-shadow: inset 0 0 10px #6e6e6e; + } } // ************** @@ -228,3 +232,22 @@ } } } +.mapstore-query-map { + position: absolute; + top: 0; + z-index: 1000; + /* left: 0; */ + bottom: 0; + right: 0; + width: calc(100% - 600px); // TODO: 600px is width of query panel. It should be calculated dynamically or set in config + padding: 10px; + animation: query-map-fade-in 0.3s ease-in; +} +@keyframes query-map-fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} From c7bc7ea31e9972e773af0599571956b989326f48 Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 11 Oct 2023 17:48:28 +0200 Subject: [PATCH 12/21] Fixed tests --- .../query/__tests__/QueryBuilder-test.jsx | 71 ++++++++++++++++--- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/web/client/components/data/query/__tests__/QueryBuilder-test.jsx b/web/client/components/data/query/__tests__/QueryBuilder-test.jsx index a01f730c12..8d3aa92179 100644 --- a/web/client/components/data/query/__tests__/QueryBuilder-test.jsx +++ b/web/client/components/data/query/__tests__/QueryBuilder-test.jsx @@ -10,21 +10,19 @@ import expect from 'expect'; import React from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-dom/test-utils'; - +import {Provider} from 'react-redux'; import QueryBuilder from '../QueryBuilder'; import standardItemsReference from "../../../../plugins/querypanel/index"; -import SwitchPanel from "../../../misc/switch/SwitchPanel"; +import configureMockStore from 'redux-mock-store'; +const mockStore = configureMockStore(); -const standardItems = Object.keys(standardItemsReference).reduce((prev, cur) => { - return {...prev, [cur]: standardItemsReference[cur].map(el => ({ - ...el, - plugin: () => - }))}; -}, {}); describe('QueryBuilder', () => { - + let store; beforeEach((done) => { + store = mockStore({ + queryform: {} + }); document.body.innerHTML = '
'; setTimeout(done); }); @@ -34,7 +32,13 @@ describe('QueryBuilder', () => { document.body.innerHTML = ''; setTimeout(done); }); - + const standardItems = Object.keys(standardItemsReference).reduce((prev, cur) => { + return {...prev, [cur]: standardItemsReference[cur].map(el => ({ + ...el, + plugin: el.plugin ? (props) => : undefined, + component: el.component ? (props) => : undefined + }))}; + }, {}); it('creates the QueryBuilder component with his default content', () => { const querybuilder = ReactDOM.render(, document.getElementById("container")); expect(querybuilder).toExist(); @@ -198,6 +202,53 @@ describe('QueryBuilder', () => { // only attribute filter should be shown expect(document.querySelectorAll('.mapstore-switch-panel').length).toBe(1); }); + it('useEmbeddedMap', () => { + const groupLevels = 5; + + const groupFields = []; + + const filterFields = [{ + rowId: 100, + groupId: 1, + attribute: "", + operator: null, + value: null, + exception: null + }]; + + const attributes = [{ + id: "Attribute", + type: "list", + values: [ + "attribute1", + "attribute2", + "attribute3", + "attribute4", + "attribute5" + ] + }]; + + const querybuilder = ReactDOM.render( + , + document.getElementById("container") + ); + expect(querybuilder).toExist(); + // only attribute filter should be shown + expect(document.querySelectorAll('.mapstore-switch-panel').length).toBe(2); + expect(document.querySelectorAll('.mapstore-query-map').length).toBe(1); + }); it('creates the QueryBuilder component in error state', () => { From b7219bd5439fde32562822536ea1479e8972a5d4 Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 11 Oct 2023 18:31:52 +0200 Subject: [PATCH 13/21] removed wrong height change --- web/client/plugins/querypanel/MapWithDraw.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/plugins/querypanel/MapWithDraw.jsx b/web/client/plugins/querypanel/MapWithDraw.jsx index a7b239b6cd..32e02c836b 100644 --- a/web/client/plugins/querypanel/MapWithDraw.jsx +++ b/web/client/plugins/querypanel/MapWithDraw.jsx @@ -39,7 +39,7 @@ const MapWithDraw = ({ mapStateSource={mapStateSource} onMapReady={onMapReady} zoomControl - options={{ style: { height: 'calc(100% - 20px)' }}} + options={{ style: { height: '100%' }}} layer={layer} tools={["draw"]}/> ) : null; From bec8d64742664b789e0d75f1fc95bbd13f22fb95 Mon Sep 17 00:00:00 2001 From: MV88 Date: Thu, 12 Oct 2023 17:51:15 +0200 Subject: [PATCH 14/21] Fix final issues on zoom step and redraw of spatial filter --- .../mapstore-migration-guide.md | 38 ++++++++ .../builder/wizard/chart/ColorClassModal.jsx | 4 +- web/client/configs/localConfig.json | 1 - web/client/epics/__tests__/widgets-test.js | 86 +------------------ web/client/epics/widgets.js | 64 ++++++-------- web/client/plugins/QueryPanel.jsx | 2 +- .../plugins/querypanel/SpatialFilterMap.jsx | 7 +- web/client/reducers/queryform.js | 19 ++-- web/client/translations/data.de-DE.json | 1 + web/client/translations/data.en-US.json | 1 + web/client/translations/data.es-ES.json | 1 + web/client/translations/data.fr-FR.json | 1 + web/client/translations/data.it-IT.json | 1 + web/client/utils/WidgetsUtils.js | 35 +++++++- 14 files changed, 118 insertions(+), 143 deletions(-) diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index 5a46f0951c..4bd32f06f8 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -22,6 +22,44 @@ This is a list of things to check if you want to update from a previous version ## Migration from 2023.02.xx to 2024.01.00 +### Adding spatial filter to dashboard widgets + +In order to enable the possibility to add in and the spatial filter to the widgets [#9098](https://github.com/geosolutions-it/MapStore2/issues/9098) you have to edit the QueryPanel config in `localConfig.json` file adding: + +- **useEmbeddedMap** flag, +- **spatialOperations** +- **spatialMethodOptions** + + *Note: these props can vary, for example you can limit the spatial filtering operation to intersect only* + +```json +... +dashboard: [ +... +{ + "name": "QueryPanel", + "cfg": { + "toolsOptions": { + "hideCrossLayer": true, + "useEmbeddedMap": true + }, + "spatialPanelExpanded": false, + "spatialOperations": [ + {"id": "INTERSECTS", "name": "queryform.spatialfilter.operations.intersects"}, + {"id": "CONTAINS", "name": "queryform.spatialfilter.operations.contains"}, + {"id": "WITHIN", "name": "queryform.spatialfilter.operations.within"} + ], + "spatialMethodOptions": [ + {"id": "BBOX", "name": "queryform.spatialfilter.methods.box"}, + {"id": "Circle", "name": "queryform.spatialfilter.methods.circle"}, + {"id": "Polygon", "name": "queryform.spatialfilter.methods.poly"} + ], + "containerPosition": "columns" + } +} + +``` + ### MapFish Print update The **MapFish Print** library has been updated to work with the latest GeoTools version and Java 11 as well as being aligned with the same dependency used by the official GeoServer printing extension (see this issue ) diff --git a/web/client/components/widgets/builder/wizard/chart/ColorClassModal.jsx b/web/client/components/widgets/builder/wizard/chart/ColorClassModal.jsx index 17375f9c40..655de29dd2 100644 --- a/web/client/components/widgets/builder/wizard/chart/ColorClassModal.jsx +++ b/web/client/components/widgets/builder/wizard/chart/ColorClassModal.jsx @@ -140,14 +140,14 @@ const ColorClassModal = ({ ColorClassModal.propTypes = { modalClassName: PropTypes.string, - show: PropTypes.boolean, + show: PropTypes.bool, onClose: PropTypes.func, onSaveClassification: PropTypes.func, onChangeClassAttribute: PropTypes.func, classificationAttribute: PropTypes.string, onUpdateClasses: PropTypes.func, options: PropTypes.array, - placeHolder: PropTypes.string, + placeHolder: PropTypes.oneOfType(PropTypes.string, PropTypes.object), classification: PropTypes.array, rangeClassification: PropTypes.array, defaultCustomColor: PropTypes.string, diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 69a108fe12..9551f09a05 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -823,7 +823,6 @@ {"id": "WITHIN", "name": "queryform.spatialfilter.operations.within"} ], "spatialMethodOptions": [ - {"id": "Viewport", "name": "queryform.spatialfilter.methods.viewport"}, {"id": "BBOX", "name": "queryform.spatialfilter.methods.box"}, {"id": "Circle", "name": "queryform.spatialfilter.methods.circle"}, {"id": "Polygon", "name": "queryform.spatialfilter.methods.poly"} diff --git a/web/client/epics/__tests__/widgets-test.js b/web/client/epics/__tests__/widgets-test.js index d8cc52d2b0..a676e303d5 100644 --- a/web/client/epics/__tests__/widgets-test.js +++ b/web/client/epics/__tests__/widgets-test.js @@ -16,14 +16,9 @@ import { updateLayerOnLayerPropertiesChange, updateLayerOnLoadingErrorChange, updateDependenciesMapOnMapSwitch, - onWidgetCreationFromMap, - onLayerSelectedEpic + onWidgetCreationFromMap } from '../widgets'; -import { - CHANGE_MAP_EDITOR -} from '../../actions/queryform'; - import { CLEAR_WIDGETS, insertWidget, @@ -46,9 +41,6 @@ import { onLocationChanged } from 'connected-react-router'; import { ActionsObservable } from 'redux-observable'; import Rx from 'rxjs'; -import { DEFAULT_MAP_SETTINGS } from '../../utils/WidgetsUtils'; - - describe('widgets Epics', () => { it('clearWidgetsOnLocationChange triggers CLEAR_WIDGETS on LOCATION_CHANGE', (done) => { const checkActions = actions => { @@ -675,80 +667,4 @@ describe('widgets Epics', () => { [onEditorChange("widgetType", "chart")], checkActions, state); }); - it('onLayerSelectedEpic by selecting a map', (done) => { - const checkActions = actions => { - expect(actions.length).toBe(1); - expect(actions[0].type).toBe(CHANGE_MAP_EDITOR); - expect(actions[0].mapData).toEqual({ - ...DEFAULT_MAP_SETTINGS, - center: { - crs: "EPSG:4326", - x: 0, - y: 0 - }, - zoom: 21 - }); - done(); - }; - const state = { - layers: { - flat: [{ - id: "1", - name: "layer", - bbox: { - crs: "EPSG:4326", - bounds: { - minx: -18, miny: -9, maxx: 18, maxy: 9 - } - } - }, { - id: "2", - name: "layer2" - }, { - id: "3", - name: "layer3" - }], - selected: ["1"] - }, - dashboard: { - editor: { - layer: { - bbox: { - crs: "EPSG:4326", - bounds: { - minx: -18, miny: -9, maxx: 18, maxy: 9 - } - } - }, - available: false - }, - editing: true - } - }; - testEpic(onLayerSelectedEpic, - 1, - [onEditorChange("chart-layers", {})], - checkActions, state); - }); - it('onLayerSelectedEpic by clearing map state used by queryform in dashboard', (done) => { - const checkActions = actions => { - expect(actions.length).toBe(1); - expect(actions[0].type).toBe(CHANGE_MAP_EDITOR); - expect(actions[0].mapData).toEqual(null); - done(); - }; - const state = { - layers: {}, - dashboard: { - editor: { - available: false - }, - editing: true - } - }; - testEpic(onLayerSelectedEpic, - 1, - [onEditorChange("chart-layers")], - checkActions, state); - }); }); diff --git a/web/client/epics/widgets.js b/web/client/epics/widgets.js index 2b8dfaf32d..a06d3f613a 100644 --- a/web/client/epics/widgets.js +++ b/web/client/epics/widgets.js @@ -28,11 +28,13 @@ import { replaceWidgets, WIDGETS_MAPS_REGEX, EDITOR_CHANGE, - EDIT + OPEN_FILTER_EDITOR } from '../actions/widgets'; import { changeMapEditor } from '../actions/queryform'; import { MAP_CONFIG_LOADED } from '../actions/config'; +import { TOGGLE_CONTROL } from '../actions/controls'; +import { queryPanelSelector } from '../selectors/controls'; import { availableDependenciesSelector, @@ -51,9 +53,9 @@ import { DASHBOARD_LOADED } from '../actions/dashboard'; import { LOCATION_CHANGE } from 'connected-react-router'; import { saveAs } from 'file-saver'; import {downloadCanvasDataURL} from '../utils/FileUtils'; -import {transformExtentToArray} from '../utils/CoordinatesUtils'; +import {reprojectBbox} from '../utils/CoordinatesUtils'; import converter from 'json-2-csv'; -import { getZoomForExtent } from '../utils/MapUtils'; +import { defaultGetZoomForExtent } from '../utils/MapUtils'; import { updateDependenciesMapOfMapList, DEFAULT_MAP_SETTINGS } from "../utils/WidgetsUtils"; const updateDependencyMap = (active, targetId, { dependenciesMap, mappings}) => { @@ -322,44 +324,30 @@ export const onWidgetCreationFromMap = (action$, store) => }); -const getMapConfig = (layer) => { - return { - ...DEFAULT_MAP_SETTINGS, - // bbox: layer.bbox, - zoom: getZoomForExtent(transformExtentToArray(layer.bbox), DEFAULT_MAP_SETTINGS.size, 0, 21), - center: { - crs: layer.bbox.crs, - x: (layer.bbox.bounds.maxx + layer.bbox.bounds.minx) / 2, - y: (layer.bbox.bounds.maxy + layer.bbox.bounds.miny) / 2 - } - }; -}; -export const onLayerSelectedEpic = (action$, store) => - action$.ofType(EDITOR_CHANGE) - .filter(({key}) => key === 'chart-layers' && isDashboardEditing(store.getState())) +export const onOpenFilterEditorEpic = (action$, store) => + action$.ofType(OPEN_FILTER_EDITOR) .switchMap(() => { const state = store.getState(); const layer = getWidgetLayer(state); - if (layer?.bbox) { - return Rx.Observable.of( - changeMapEditor(getMapConfig(layer)) - ); - } - return Rx.Observable.of( - changeMapEditor(null) - ); + const zoom = defaultGetZoomForExtent(reprojectBbox(layer.bbox.bounds, "EPSG:4326", "EPSG:3857", true), DEFAULT_MAP_SETTINGS.size, 0, 21, 96, DEFAULT_MAP_SETTINGS.resolutions); + const map = { + ...DEFAULT_MAP_SETTINGS, + zoom, + center: { + crs: layer.bbox.crs, + x: (layer.bbox.bounds.maxx + layer.bbox.bounds.minx) / 2, + y: (layer.bbox.bounds.maxy + layer.bbox.bounds.miny) / 2 + } + }; + const mapData = layer?.bbox ? map : null; + return Rx.Observable.of( changeMapEditor(mapData) ); }); -export const onEditWidgetEpic = (action$, store) => - action$.ofType(EDIT) - .filter(() => isDashboardEditing(store.getState())) + + +export const onResetMapEpic = (action$, store) => + action$.ofType(TOGGLE_CONTROL) + .filter((type, control) => !queryPanelSelector(store.getState()) && control === "queryPanel" || isDashboardEditing(store.getState())) .switchMap(() => { - const state = store.getState(); - const layer = getWidgetLayer(state); - if (layer?.bbox) { - return Rx.Observable.of( - changeMapEditor(getMapConfig(layer)) - ); - } return Rx.Observable.of( changeMapEditor(null) ); @@ -375,6 +363,6 @@ export default { updateLayerOnLoadingErrorChange, updateDependenciesMapOnMapSwitch, onWidgetCreationFromMap, - onLayerSelectedEpic, - onEditWidgetEpic + onOpenFilterEditorEpic, + onResetMapEpic }; diff --git a/web/client/plugins/QueryPanel.jsx b/web/client/plugins/QueryPanel.jsx index 390c7ff02e..c56a9c2fd7 100644 --- a/web/client/plugins/QueryPanel.jsx +++ b/web/client/plugins/QueryPanel.jsx @@ -263,7 +263,7 @@ class QueryPanel extends React.Component { } UNSAFE_componentWillReceiveProps(newProps) { - if (newProps.queryPanelEnabled === true && this.props.queryPanelEnabled === false) { + if (!newProps.toolsOptions.useEmbeddedMap && newProps.queryPanelEnabled === true && this.props.queryPanelEnabled === false) { this.props.onInit(); } } diff --git a/web/client/plugins/querypanel/SpatialFilterMap.jsx b/web/client/plugins/querypanel/SpatialFilterMap.jsx index 92d748b793..97f66e0e62 100644 --- a/web/client/plugins/querypanel/SpatialFilterMap.jsx +++ b/web/client/plugins/querypanel/SpatialFilterMap.jsx @@ -20,6 +20,9 @@ import { import { getMapConfigSelector } from '../../selectors/queryform'; +import { + initQueryPanel +} from '../../actions/wfsquery'; /** * Component connected to the widgetLayer @@ -35,7 +38,9 @@ export const MapComponent = connect( mapStateSource: "wizardMap" }; } - ), {} )(MapWithDraw); + ), { + onMapReady: initQueryPanel + } )(MapWithDraw); export default withContainer((props) => { const { diff --git a/web/client/reducers/queryform.js b/web/client/reducers/queryform.js index 43dd9c83a6..d0815eb446 100644 --- a/web/client/reducers/queryform.js +++ b/web/client/reducers/queryform.js @@ -50,12 +50,11 @@ import { UPDATE_CROSS_LAYER_FILTER_FIELD_OPTIONS, UPSERT_FILTERS, REMOVE_FILTERS, - CHANGE_MAP_EDITOR + CHANGE_MAP_EDITOR, + QUERY_FORM_SEARCH } from '../actions/queryform'; import { END_DRAWING, CHANGE_DRAWING_STATUS } from '../actions/draw'; -import { INSERT } from '../actions/widgets'; -import { SET_EDITING } from '../actions/dashboard'; import assign from 'object-assign'; import union from 'turf-union'; import bbox from 'turf-bbox'; @@ -112,21 +111,12 @@ function queryform(state = initialState, action) { map: action.mapData }; } - case INSERT: { + case QUERY_FORM_SEARCH: { return { ...state, map: null }; } - case SET_EDITING: { - if (!action.editing) { - return { - ...state, - map: null - }; - } - return state; - } case ADD_FILTER_FIELD: { // // Calculate the key number, this should be different for each new element @@ -393,7 +383,8 @@ function queryform(state = initialState, action) { return assign({}, state, initialState, { spatialField, crossLayerFilter, - filters: [] + filters: [], + map: state.map }); } case SHOW_GENERATED_FILTER: { diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 9851d5e3d2..6a8dfc716e 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -2150,6 +2150,7 @@ }, "mapSync": "Live-Filter nach Ansichtsfenster", "displayLegend": { + "default": "Legende anzeigen", "line": "Legende anzeigen", "pie": "Legende anzeigen", "bar": "Legende anzeigen", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index b3bae12fc4..fde0ff932e 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -2113,6 +2113,7 @@ }, "mapSync": "Live Filter by viewport", "displayLegend": { + "default": "Display Legend", "line": "Display Legend", "pie": "Display Legend", "bar": "Display Legend", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 7cfb227562..c648ff2900 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -2113,6 +2113,7 @@ }, "mapSync": "Filtro dinámico por extensión de la vista actual", "displayLegend": { + "default": "Mostrar leyenda", "line": "Mostrar leyenda", "pie": "Mostrar leyenda", "bar": "Mostrar leyenda", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 9f0f885de3..abfc6db491 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -2113,6 +2113,7 @@ }, "mapSync": "Live Filter par viewport", "displayLegend": { + "default": "Afficher la légende", "line": "Afficher la légende", "pie": "Afficher la légende", "bar": "Afficher la légende", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index ca82e09927..2b00ecd891 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -2114,6 +2114,7 @@ }, "mapSync": "Filtra dati sull'area visibile", "displayLegend": { + "default": "Mostra legenda", "line": "Mostra legenda", "pie": "Mostra legenda", "bar": "Mostra legenda", diff --git a/web/client/utils/WidgetsUtils.js b/web/client/utils/WidgetsUtils.js index 7fb894ac01..fa8af34a22 100644 --- a/web/client/utils/WidgetsUtils.js +++ b/web/client/utils/WidgetsUtils.js @@ -353,5 +353,38 @@ export const DEFAULT_MAP_SETTINGS = { }, version: 2, limits: {}, - mousePointer: 'pointer' + mousePointer: 'pointer', + resolutions: [ + 156543.03392804097, + 78271.51696402048, + 39135.75848201024, + 19567.87924100512, + 9783.93962050256, + 4891.96981025128, + 2445.98490512564, + 1222.99245256282, + 611.49622628141, + 305.748113140705, + 152.8740565703525, + 76.43702828517625, + 38.21851414258813, + 19.109257071294063, + 9.554628535647032, + 4.777314267823516, + 2.388657133911758, + 1.194328566955879, + 0.5971642834779395, + 0.29858214173896974, + 0.14929107086948487, + 0.07464553543474244, + 0.03732276771737122, + 0.01866138385868561, + 0.009330691929342804, + 0.004665345964671402, + 0.002332672982335701, + 0.0011663364911678506, + 0.0005831682455839253, + 0.00029158412279196264, + 0.00014579206139598132 + ] }; From 2e17e0184cff4a01c65364b8549c2cab7d7efe15 Mon Sep 17 00:00:00 2001 From: Matteo V Date: Fri, 13 Oct 2023 12:11:04 +0200 Subject: [PATCH 15/21] Update docs/developer-guide/mapstore-migration-guide.md Co-authored-by: Lorenzo Natali --- docs/developer-guide/mapstore-migration-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index 4bd32f06f8..4ed1077155 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -34,7 +34,7 @@ In order to enable the possibility to add in and the spatial filter to the widge ```json ... -dashboard: [ +"dashboard": [ ... { "name": "QueryPanel", From 83535747df1e58d179af0835ac406cfd7025c333 Mon Sep 17 00:00:00 2001 From: Matteo V Date: Fri, 13 Oct 2023 12:11:11 +0200 Subject: [PATCH 16/21] Update docs/developer-guide/mapstore-migration-guide.md Co-authored-by: Lorenzo Natali --- docs/developer-guide/mapstore-migration-guide.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index 4ed1077155..89d5b6bb76 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -30,7 +30,6 @@ In order to enable the possibility to add in and the spatial filter to the widge - **spatialOperations** - **spatialMethodOptions** - *Note: these props can vary, for example you can limit the spatial filtering operation to intersect only* ```json ... From 1fc6172281a275e9cea46e31380d9246aedb781d Mon Sep 17 00:00:00 2001 From: Matteo V Date: Fri, 13 Oct 2023 12:11:24 +0200 Subject: [PATCH 17/21] Update docs/developer-guide/mapstore-migration-guide.md Co-authored-by: Lorenzo Natali --- docs/developer-guide/mapstore-migration-guide.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index 89d5b6bb76..d1d434145f 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -26,9 +26,9 @@ This is a list of things to check if you want to update from a previous version In order to enable the possibility to add in and the spatial filter to the widgets [#9098](https://github.com/geosolutions-it/MapStore2/issues/9098) you have to edit the QueryPanel config in `localConfig.json` file adding: -- **useEmbeddedMap** flag, -- **spatialOperations** -- **spatialMethodOptions** +- **useEmbeddedMap**: flag to enable the embedded map +- **spatialOperations**: The list of spatial operations allowed for this plugin +- **spatialMethodOptions**: the list of spatial methods to use. ```json From 56842b584a9f17f5a127aad01d26384b4c3ba341 Mon Sep 17 00:00:00 2001 From: Matteo V Date: Fri, 13 Oct 2023 12:51:57 +0200 Subject: [PATCH 18/21] Update web/client/epics/widgets.js Co-authored-by: Lorenzo Natali --- web/client/epics/widgets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/epics/widgets.js b/web/client/epics/widgets.js index a06d3f613a..84b57451cb 100644 --- a/web/client/epics/widgets.js +++ b/web/client/epics/widgets.js @@ -346,7 +346,7 @@ export const onOpenFilterEditorEpic = (action$, store) => export const onResetMapEpic = (action$, store) => action$.ofType(TOGGLE_CONTROL) - .filter((type, control) => !queryPanelSelector(store.getState()) && control === "queryPanel" || isDashboardEditing(store.getState())) + .filter((type, control) => !queryPanelSelector(store.getState()) && control === "queryPanel" && isDashboardEditing(store.getState())) .switchMap(() => { return Rx.Observable.of( changeMapEditor(null) From f2b22c5901e0ca350b4211dcc60a49c82a71fb0b Mon Sep 17 00:00:00 2001 From: MV88 Date: Fri, 13 Oct 2023 13:05:27 +0200 Subject: [PATCH 19/21] fix lint --- docs/developer-guide/mapstore-migration-guide.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index d1d434145f..9f033f55b1 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -30,7 +30,6 @@ In order to enable the possibility to add in and the spatial filter to the widge - **spatialOperations**: The list of spatial operations allowed for this plugin - **spatialMethodOptions**: the list of spatial methods to use. - ```json ... "dashboard": [ From 9694f36705c9fb6d01af8f39127948aa870c69b7 Mon Sep 17 00:00:00 2001 From: MV88 Date: Fri, 13 Oct 2023 13:12:49 +0200 Subject: [PATCH 20/21] add comment --- web/client/plugins/QueryPanel.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/client/plugins/QueryPanel.jsx b/web/client/plugins/QueryPanel.jsx index c56a9c2fd7..77330ff2c6 100644 --- a/web/client/plugins/QueryPanel.jsx +++ b/web/client/plugins/QueryPanel.jsx @@ -263,6 +263,8 @@ class QueryPanel extends React.Component { } UNSAFE_componentWillReceiveProps(newProps) { + // triggering the init only if not using the embedded map since this was happening too early + // making the redraw of spatial filter not happening if (!newProps.toolsOptions.useEmbeddedMap && newProps.queryPanelEnabled === true && this.props.queryPanelEnabled === false) { this.props.onInit(); } From e0e9908ae571fdf4881c2990abdf8542b5330188 Mon Sep 17 00:00:00 2001 From: Matteo V Date: Fri, 13 Oct 2023 13:40:48 +0200 Subject: [PATCH 21/21] Update docs/developer-guide/mapstore-migration-guide.md Co-authored-by: Lorenzo Natali --- docs/developer-guide/mapstore-migration-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index 9f033f55b1..0fecd10eb3 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -24,7 +24,7 @@ This is a list of things to check if you want to update from a previous version ### Adding spatial filter to dashboard widgets -In order to enable the possibility to add in and the spatial filter to the widgets [#9098](https://github.com/geosolutions-it/MapStore2/issues/9098) you have to edit the QueryPanel config in `localConfig.json` file adding: +In order to enable the possibility to add in and the spatial filter to the widgets ( see [#9098](https://github.com/geosolutions-it/MapStore2/issues/9098) ) you have to edit the `QueryPanel` config in the `plugins.dashboard` array of the `localConfig.json` file by adding: - **useEmbeddedMap**: flag to enable the embedded map - **spatialOperations**: The list of spatial operations allowed for this plugin