diff --git a/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js b/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js
index e3648253fa..8dde240dea 100644
--- a/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js
+++ b/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js
@@ -44,5 +44,27 @@ describe('onMapViewChanges enhancer', () => {
expect(map.mapStateSource).toExist();
expect(map.projection).toExist();
});
+ it('onMapViewChanges rendering with zoomToExtentHandler', () => {
+ const Sink = onMapViewChanges(createSink( props => {
+ expect(props.eventHandlers.onMapViewChanges).toExist();
+ setTimeout(props.eventHandlers.onMapViewChanges("CENTER", "ZOOM", { bbox: { x: 2 } }, "SIZE", "mapStateSource", "projection", {}, "RESOLUTION", "ORINATE", () => {}));
+
+ }));
+ const actions = {
+ onMapViewChanges: () => {}
+ };
+ const spy = expect.spyOn(actions, 'onMapViewChanges');
+ ReactDOM.render(, document.getElementById("container"));
+ expect(spy).toHaveBeenCalled();
+ const map = spy.calls[0].arguments[0];
+ expect(map).toExist();
+ expect(map.center).toExist();
+ expect(map.zoom).toExist();
+ expect(map.bbox).toExist();
+ expect(map.size).toExist();
+ expect(map.mapStateSource).toExist();
+ expect(map.projection).toExist();
+ expect(map.zoomToExtentHandler).toExist();
+ });
});
diff --git a/web/client/components/map/enhancers/onMapViewChanges.js b/web/client/components/map/enhancers/onMapViewChanges.js
index 27238cb940..b1d6ffc9d3 100644
--- a/web/client/components/map/enhancers/onMapViewChanges.js
+++ b/web/client/components/map/enhancers/onMapViewChanges.js
@@ -14,7 +14,7 @@ import { compose, withHandlers, withPropsOnChange } from 'recompose';
*/
export default compose(
withHandlers({
- onMapViewChanges: ({ map = {}, onMapViewChanges = () => {}}) => (center, zoom, bbox, size, mapStateSource, projection, viewerOptions, resolution, orientate) => {
+ onMapViewChanges: ({ map = {}, onMapViewChanges = () => {}}) => (center, zoom, bbox, size, mapStateSource, projection, viewerOptions, resolution, orientate, zoomToExtentHandler) => {
onMapViewChanges({
...map,
center,
@@ -27,7 +27,8 @@ export default compose(
mapStateSource,
projection,
resolution,
- orientate
+ orientate,
+ zoomToExtentHandler
});
}
}),
diff --git a/web/client/components/map/openlayers/Map.jsx b/web/client/components/map/openlayers/Map.jsx
index 2268f7ac62..3bee6010e8 100644
--- a/web/client/components/map/openlayers/Map.jsx
+++ b/web/client/components/map/openlayers/Map.jsx
@@ -491,7 +491,9 @@ class OpenlayersMap extends React.Component {
this.props.id,
this.props.projection,
undefined, // viewerOptions,
- view.getResolution() // resolution
+ view.getResolution(), // resolution
+ undefined,
+ this.zoomToExtentHandler
);
}
};
@@ -561,6 +563,26 @@ class OpenlayersMap extends React.Component {
}
};
+ zoomToExtentHandler = (extent, { padding, crs, maxZoom: zoomLevel, duration, nearest} = {})=> {
+ let bounds = reprojectBbox(extent, crs, this.props.projection);
+ // TODO: improve this to manage all degenerated bounding boxes.
+ if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] &&
+ crs === "EPSG:4326" && isArray(extent) && extent[0] === -180 && extent[1] === -90) {
+ bounds = this.map.getView().getProjection().getExtent();
+ }
+ let maxZoom = zoomLevel;
+ if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] && isNil(maxZoom)) {
+ maxZoom = 21; // TODO: allow to this maxZoom to be customizable
+ }
+ this.map.getView().fit(bounds, {
+ size: this.map.getSize(),
+ padding: padding && [padding.top || 0, padding.right || 0, padding.bottom || 0, padding.left || 0],
+ maxZoom,
+ duration,
+ nearest
+ });
+ }
+
registerHooks = () => {
this.props.hookRegister.registerHook(mapUtils.RESOLUTIONS_HOOK, (srs) => {
return this.getResolutions(srs);
@@ -590,27 +612,7 @@ class OpenlayersMap extends React.Component {
this.props.hookRegister.registerHook(mapUtils.GET_COORDINATES_FROM_PIXEL_HOOK, (pixel) => {
return this.map.getCoordinateFromPixel(pixel);
});
- this.props.hookRegister.registerHook(mapUtils.ZOOM_TO_EXTENT_HOOK, (extent, { padding, crs, maxZoom: zoomLevel, duration, nearest} = {}) => {
- let bounds = reprojectBbox(extent, crs, this.props.projection);
- // if EPSG:4326 with max extent (-180, -90, 180, 90) bounds are 0,0,0,0. In this case zoom to max extent
- // TODO: improve this to manage all degenerated bounding boxes.
- if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] &&
- crs === "EPSG:4326" && isArray(extent) && extent[0] === -180 && extent[1] === -90) {
- bounds = this.map.getView().getProjection().getExtent();
- }
- let maxZoom = zoomLevel;
- if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] && isNil(maxZoom)) {
- maxZoom = 21; // TODO: allow to this maxZoom to be customizable
- }
-
- this.map.getView().fit(bounds, {
- size: this.map.getSize(),
- padding: padding && [padding.top || 0, padding.right || 0, padding.bottom || 0, padding.left || 0],
- maxZoom,
- duration,
- nearest
- });
- });
+ this.props.hookRegister.registerHook(mapUtils.ZOOM_TO_EXTENT_HOOK, this.zoomToExtentHandler);
};
}
diff --git a/web/client/components/map/openlayers/__tests__/Map-test.jsx b/web/client/components/map/openlayers/__tests__/Map-test.jsx
index b9900912f7..5ced20bdef 100644
--- a/web/client/components/map/openlayers/__tests__/Map-test.jsx
+++ b/web/client/components/map/openlayers/__tests__/Map-test.jsx
@@ -531,7 +531,7 @@ describe('OpenlayersMap', () => {
olMap.on('moveend', () => {
// The first call is triggered as soon as the map component is mounted, the second one is as a result of setZoom
expect(spy.calls.length).toEqual(2);
- expect(spy.calls[1].arguments.length).toEqual(8);
+ expect(spy.calls[1].arguments.length).toEqual(10);
expect(normalizeFloat(spy.calls[1].arguments[0].y, 1)).toBe(43.9);
expect(normalizeFloat(spy.calls[1].arguments[0].x, 1)).toBe(10.3);
expect(spy.calls[1].arguments[1]).toBe(12);
@@ -563,7 +563,7 @@ describe('OpenlayersMap', () => {
olMap.on('moveend', () => {
// The first call is triggered as soon as the map component is mounted, the second one is as a result of setCenter
expect(spy.calls.length).toEqual(2);
- expect(spy.calls[1].arguments.length).toEqual(8);
+ expect(spy.calls[1].arguments.length).toEqual(10);
expect(normalizeFloat(spy.calls[1].arguments[0].y, 1)).toBe(44);
expect(normalizeFloat(spy.calls[1].arguments[0].x, 1)).toBe(10);
expect(spy.calls[1].arguments[1]).toBe(11);
diff --git a/web/client/components/widgets/builder/wizard/table/TableOptions.jsx b/web/client/components/widgets/builder/wizard/table/TableOptions.jsx
index c4c412f53d..c16ccb7d2e 100644
--- a/web/client/components/widgets/builder/wizard/table/TableOptions.jsx
+++ b/web/client/components/widgets/builder/wizard/table/TableOptions.jsx
@@ -9,6 +9,7 @@
import React from 'react';
import { Col, Form, Row } from 'react-bootstrap';
import {compose, withProps} from 'recompose';
+import { isGeometryType } from '../../../../../utils/ogc/WFS/base';
import AttributeTable from '../../../../data/featuregrid/AttributeTable';
import Message from '../../../../I18N/Message';
@@ -28,6 +29,7 @@ const AttributeSelector = compose(
withProps(
({ attributes = [], options = {}, layer = {}} = {}) => ({ // TODO manage hide condition
attributes: attributes
+ .filter(a => !isGeometryType(a))
.map( a => {
const propertyNames = options?.propertyName?.map(p => p.name);
const currPropertyName = options?.propertyName?.find(p => p.name === a.name);
diff --git a/web/client/components/widgets/enhancers/__tests__/dependenciesToZoomTo-test.jsx b/web/client/components/widgets/enhancers/__tests__/dependenciesToZoomTo-test.jsx
index eb9a14cffb..730ad560b1 100644
--- a/web/client/components/widgets/enhancers/__tests__/dependenciesToZoomTo-test.jsx
+++ b/web/client/components/widgets/enhancers/__tests__/dependenciesToZoomTo-test.jsx
@@ -15,9 +15,6 @@ import MockAdapter from 'axios-mock-adapter';
import dependenciesToZoomTo from '../dependenciesToZoomTo';
-import MapUtils from '../../../../utils/MapUtils';
-
-
describe('widgets dependenciesToZoomTo enhancer', () => {
let mockAxios;
const widgetsProps = [{
@@ -30,13 +27,18 @@ describe('widgets dependenciesToZoomTo enhancer', () => {
maxZoom: 21
}
},
+ dependenciesMap: {
+ mapSync: "MapID[id]"
+ },
geomProp: "the_geom",
mapSync: true
},
{
- id: "123Map",
+ id: "MapID",
widgetType: "map",
- maps: [{}],
+ maps: [{
+ mapStateSource: "MapID"
+ }],
mapSync: true
}];
beforeEach((done) => {
@@ -60,19 +62,14 @@ describe('widgets dependenciesToZoomTo enhancer', () => {
});
it('dependenciesToZoomTo triggering zoom to extent', (done) => {
- let hookRegisterProps = MapUtils.createRegisterHooks();
const Sink = dependenciesToZoomTo(createSink(({
- hookRegister = hookRegisterProps, widgets = widgetsProps
+ widgets = widgetsProps
}) => {
- expect(hookRegister).toExist();
- const hook = hookRegister.getHook(MapUtils.ZOOM_TO_EXTENT_HOOK);
- expect(hook).toExist();
expect(widgets).toExist();
expect(widgets).toEqual(widgetsProps);
done();
}));
- hookRegisterProps.registerHook(MapUtils.ZOOM_TO_EXTENT_HOOK, {hookName: MapUtils.ZOOM_TO_EXTENT_HOOK});
- ReactDOM.render( {
+ ReactDOM.render( {
expect(path).toBe("dependencies.extentObj");
done();
}}/>, document.getElementById("container"));
diff --git a/web/client/components/widgets/enhancers/__tests__/tableWidget-test.jsx b/web/client/components/widgets/enhancers/__tests__/tableWidget-test.jsx
index 2cb9680de0..8d5f9a7798 100644
--- a/web/client/components/widgets/enhancers/__tests__/tableWidget-test.jsx
+++ b/web/client/components/widgets/enhancers/__tests__/tableWidget-test.jsx
@@ -45,4 +45,43 @@ describe('widgets tableWidget enhancer', () => {
expect(filter).toBe(someFilter);
}}/>, document.getElementById("container"));
});
+
+ it('tableWidget with gridTools including zoom icon for dashboard viewer', (done) => {
+ const Sink = tableWidget(createSink( props => {
+ expect(props).toExist();
+ expect(props.gridTools.length).toEqual(1);
+ props.gridTools[0].events.onClick(
+ {
+ bbox: [-10, 0, 0, -10]
+ }, {}, "", { crs: "", maxZoom: null }
+ );
+ done();
+ }));
+ ReactDOM.render( {
+ expect(path).toBe("dependencies.extentObj");
+ expect(id).toBe("123456");
+ expect(value).toEqual({
+ bbox: [-10, 0, 0, -10]
+ }, {}, "", { crs: "EPSG:4326", maxZoom: null });
+ }}/>, document.getElementById("container"));
+ const container = document.getElementById('container');
+ expect(container).toExist();
+
+ });
+ it('tableWidget with gridTools including zoom icon for mapViewer', (done) => {
+ const Sink = tableWidget(createSink( props => {
+ expect(props).toExist();
+ expect(props.gridTools.length).toEqual(1);
+ props.gridTools[0].events.onClick(
+ {
+ bbox: [-10, 0, 0, -10]
+ }, {}, "", { crs: "", maxZoom: null }
+ );
+ done();
+ }));
+ ReactDOM.render( , document.getElementById("container"));
+ const container = document.getElementById('container');
+ expect(container).toExist();
+
+ });
});
diff --git a/web/client/components/widgets/enhancers/dependenciesToExtent.js b/web/client/components/widgets/enhancers/dependenciesToExtent.js
index 728cecf9e9..d1b57c29f4 100644
--- a/web/client/components/widgets/enhancers/dependenciesToExtent.js
+++ b/web/client/components/widgets/enhancers/dependenciesToExtent.js
@@ -8,7 +8,7 @@
import xml2js from 'xml2js';
import { Observable } from 'rxjs';
-import { mapPropsStream, compose, branch, withPropsOnChange, defaultProps } from 'recompose';
+import { mapPropsStream, compose, branch, withPropsOnChange } from 'recompose';
import { isEmpty, isEqual } from 'lodash';
import { composeFilterObject } from './utils';
import wpsBounds from '../../../observables/wps/bounds';
@@ -22,9 +22,7 @@ import { createRegisterHooks, ZOOM_TO_EXTENT_HOOK } from '../../../utils/MapUtil
* @returns {object} the map with center and zoom updated
*/
export default compose(
- defaultProps({
- hookRegister: createRegisterHooks() // it is for zoomTo HOC
- }),
+
branch(
({mapSync, dependencies} = {}) => {
return mapSync && (!isEmpty(dependencies.quickFilters) || !isEmpty(dependencies.filter));
diff --git a/web/client/components/widgets/enhancers/dependenciesToZoomTo.js b/web/client/components/widgets/enhancers/dependenciesToZoomTo.js
index 816d640f68..458bf2d92d 100644
--- a/web/client/components/widgets/enhancers/dependenciesToZoomTo.js
+++ b/web/client/components/widgets/enhancers/dependenciesToZoomTo.js
@@ -1,24 +1,33 @@
+/*
+ * 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, { useEffect, memo } from 'react';
-const dependenciesToZoomTo = (WrappedCompoennet)=>{
+const dependenciesToZoomTo = (WrappedComponent)=>{
function DependenciesToZoomTo(props) {
let tblWidgetWithExtentObj = props.widgets.find(i=>i?.dependencies?.extentObj);
const extentObj = tblWidgetWithExtentObj?.dependencies?.extentObj;
+ let mapWidget = props?.widgets?.find(i=>tblWidgetWithExtentObj?.dependenciesMap?.mapSync.includes(i.id));
useEffect(()=>{
- if ( extentObj && extentObj ) {
- const hook = props?.hookRegister?.getHook("ZOOM_TO_EXTENT_HOOK");
- if (hook) {
+ if ( extentObj && mapWidget ) {
+ let connectedMap = mapWidget?.maps.find(i=>i.mapStateSource === mapWidget.id);
+ let zoomToExtentHandler = connectedMap?.zoomToExtentHandler;
+ if (zoomToExtentHandler) {
// trigger "internal" zoom to extent
- hook(extentObj.extent, {
+ zoomToExtentHandler(extentObj.extent, {
crs: extentObj.crs, maxZoom: extentObj.maxZoom
});
- // removeextentObj from state
+ // remove extentObj from state
props?.updateProperty(tblWidgetWithExtentObj.id, `dependencies.extentObj`, undefined);
}
}
}, [extentObj?.extent?.join(",")]);
- return ;
+ return ;
}
return memo(DependenciesToZoomTo);
};
diff --git a/web/client/components/widgets/enhancers/tableWidget.js b/web/client/components/widgets/enhancers/tableWidget.js
index bd9ea05658..208bd4fa21 100644
--- a/web/client/components/widgets/enhancers/tableWidget.js
+++ b/web/client/components/widgets/enhancers/tableWidget.js
@@ -5,16 +5,19 @@
* 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 { get } from 'lodash';
import {connect} from 'react-redux';
-import {bindActionCreators} from 'redux';
-import { compose, withPropsOnChange, withProps } from 'recompose';
+import { compose, withPropsOnChange } from 'recompose';
import debounce from 'lodash/debounce';
import bbox from '@turf/bbox';
import deleteWidget from './deleteWidget';
+import LoadingSpinner from '../../misc/LoadingSpinner';
import { defaultIcons, editableWidget, withHeaderTools } from './tools';
+import { zoomToExtent } from '../../../actions/map';
+import {error} from '../../../actions/notifications';
import { gridTools } from '../../../plugins/featuregrid/index';
+import { getFeature } from '../../../api/WFS';
const withSorting = () => withPropsOnChange(["gridEvents"], ({ gridEvents = {}, updateProperty = () => { }, id } = {}) => ({
gridEvents: {
...gridEvents,
@@ -27,27 +30,63 @@ const withSorting = () => withPropsOnChange(["gridEvents"], ({ gridEvents = {},
*/
export default compose(
compose(connect(null, (dispatch, ownProps)=>{
- let geoPropName = ownProps?.geomProp;
- let hasGeometryProp = !(ownProps?.columnSettings && ownProps?.columnSettings[geoPropName]?.hide);
let isTblDashboard = ownProps?.mapSync && ownProps?.widgetType === 'table' && ownProps?.isDashboardOpened;
+ let isTblWidgetInMapViewer = ownProps?.widgetType && !isTblDashboard;
let isTblSyncWithMap = ownProps?.mapSync;
return {
- gridTools: (hasGeometryProp && isTblSyncWithMap) ? gridTools.map((t) => ({
+ gridTools: (isTblSyncWithMap && isTblDashboard) || (isTblWidgetInMapViewer) ? gridTools.map((t) => ({
...t,
- events: isTblDashboard ? {
- onClick: (p, opts, describe, {crs, maxZoom} = {}) => {
- ownProps?.updateProperty(ownProps.id, `dependencies.extentObj`, {
- extent: bbox(p),
- crs: crs || "EPSG:4326", maxZoom
- });
+ events: {
+ onClick: async(p, opts, describe, {crs, maxZoom} = {}) => {
+ if (ownProps?.recordZoomLoading) return;
+ try {
+ // fetch feature with geomnetry and zoom to it if geometry not exist
+ if (!p?.bbox) {
+ ownProps?.updateProperty(ownProps.id, `dependencies.zoomLoader`, true); // show loader instead of zoom icon
+ let { data: featureData } = await getFeature(ownProps?.layer?.search?.url, ownProps?.layer?.name, {
+ outputFormat: "application/json",
+ srsname: 'EPSG:4326',
+ featureId: p.id,
+ propertyName: ownProps?.geomProp || "the_geom" // fetch only the geometry
+ });
+ p.geometry = featureData?.features[0].geometry; // set geometry to feature for the future hit
+ p.bbox = bbox(featureData?.features[0]); // set geometry to feature for the future hit
+ if (isTblDashboard) { // in case of table widget in dashboard view set extent to widget dependencies
+ ownProps?.updateProperty(ownProps.id, `dependencies.extentObj`, {
+ extent: p.bbox,
+ crs: crs || "EPSG:4326", maxZoom
+ });
+ } else { // in case of table widget within the map viewer zoom to the feature
+ dispatch(zoomToExtent(p.bbox, crs || "EPSG:4326", maxZoom));
+ }
+ ownProps?.updateProperty(ownProps.id, `dependencies.zoomLoader`, false); // stop zoom loader
+ } else { // in case the geometry is already existing --> zoom to feature directly without fetching
+ if (isTblDashboard) {
+ ownProps?.updateProperty(ownProps.id, `dependencies.extentObj`, {
+ extent: p.bbox,
+ crs: crs || "EPSG:4326", maxZoom
+ });
+ } else {
+ dispatch(zoomToExtent(p.bbox, crs || "EPSG:4326", maxZoom));
+ }
+ }
+ } catch (err) {
+ dispatch(error({
+ title: "warning",
+ message: "Error loading GF Geom", // TODO add tranlations
+ action: {
+ label: "warning" // TODO add tranlations
+ },
+ autoDismiss: 3,
+ position: "tc"
+ }));
+ ownProps?.updateProperty(ownProps.id, `dependencies.zoomLoader`, false); // stop zoom loader
+ }
}
- } : bindActionCreators(t.events, dispatch)
+ }, formatter: ownProps?.recordZoomLoading ? : t.tableWidgetFormatter
})) : []
};
})),
- withProps(()=>({
- showCheckbox: true // for selection
- })),
withPropsOnChange(["gridEvents"], ({ gridEvents = {}, updateProperty = () => {}, id } = {}) => {
const _debounceOnAddFilter = debounce((...args) => updateProperty(...args), 500);
return {
diff --git a/web/client/plugins/Widgets.jsx b/web/client/plugins/Widgets.jsx
index 27dc3e3549..64bcad7a93 100644
--- a/web/client/plugins/Widgets.jsx
+++ b/web/client/plugins/Widgets.jsx
@@ -22,7 +22,8 @@ import {
getFloatingWidgetsLayout,
getMaximizedState,
getVisibleFloatingWidgets,
- isTrayEnabled
+ isTrayEnabled,
+ getTblWidgetZoomLoader
} from '../selectors/widgets';
import {
changeLayout,
@@ -56,7 +57,8 @@ compose(
(state) => mapLayoutValuesSelector(state, { right: true}),
state => state.browser && state.browser.mobile,
getFloatingWidgets,
- (id, widgets, layouts, maximized, dependencies, mapLayout, isMobileAgent, dropdownWidgets) => ({
+ getTblWidgetZoomLoader,
+ (id, widgets, layouts, maximized, dependencies, mapLayout, isMobileAgent, dropdownWidgets, recordZoomLoading) => ({
id,
widgets,
layouts,
@@ -64,7 +66,8 @@ compose(
dependencies,
mapLayout,
isMobileAgent,
- dropdownWidgets
+ dropdownWidgets,
+ recordZoomLoading
})
), {
editWidget,
diff --git a/web/client/plugins/featuregrid/gridTools.jsx b/web/client/plugins/featuregrid/gridTools.jsx
index f440c9a2f7..6ce1314b8d 100644
--- a/web/client/plugins/featuregrid/gridTools.jsx
+++ b/web/client/plugins/featuregrid/gridTools.jsx
@@ -21,5 +21,8 @@ export default [{
:
}>
-
+ ,
+ tableWidgetFormatter: }>
+
+
}];
diff --git a/web/client/selectors/widgets.js b/web/client/selectors/widgets.js
index d4bba4b428..97f44fc81e 100644
--- a/web/client/selectors/widgets.js
+++ b/web/client/selectors/widgets.js
@@ -185,3 +185,8 @@ export const getWidgetFilterKey = (state) => {
// Set chart key if editor widget type is chart
return selectedChartId ? `charts[${selectedChartId}].filter` : "filter";
};
+
+export const getTblWidgetZoomLoader = state => {
+ let tableWidgets = (getFloatingWidgets(state) || []).filter(({ widgetType } = {}) => widgetType === "table");
+ return tableWidgets?.find(t=>t.dependencies?.zoomLoader) ? true : false;
+};