diff --git a/web/client/actions/maplayout.js b/web/client/actions/maplayout.js index c05dbfec33..f3f77afac0 100644 --- a/web/client/actions/maplayout.js +++ b/web/client/actions/maplayout.js @@ -7,6 +7,7 @@ */ export const UPDATE_MAP_LAYOUT = 'MAP_LAYOUT:UPDATE_MAP_LAYOUT'; +export const FORCE_UPDATE_MAP_LAYOUT = 'MAP_LAYOUT:FORCE_UPDATE_MAP_LAYOUT'; /** * updateMapLayout action, type `UPDATE_MAP_LAYOUT` @@ -21,6 +22,12 @@ export function updateMapLayout(layout) { }; } +export function forceUpdateMapLayout() { + return { + type: FORCE_UPDATE_MAP_LAYOUT + }; +} + /** * Actions for map layout. * @name actions.mapLayout diff --git a/web/client/components/map/cesium/Map.jsx b/web/client/components/map/cesium/Map.jsx index fd9973da52..111366d397 100644 --- a/web/client/components/map/cesium/Map.jsx +++ b/web/client/components/map/cesium/Map.jsx @@ -222,7 +222,9 @@ class CesiumMap extends React.Component { x: x, y: y }, - height: this.props.mapOptions && this.props.mapOptions.terrainProvider ? cartographic.height : undefined, + height: (this.props.mapOptions && this.props.mapOptions.terrainProvider) || intersectedFeatures.length > 0 + ? cartographic.height + : undefined, cartographic, latlng: { lat: latitude, @@ -293,25 +295,16 @@ class CesiumMap extends React.Component { }; getIntersectedFeatures = (map, position) => { - const features = map.scene.drillPick(position); - if (features) { - const groupIntersectedFeatures = features.reduce((acc, feature) => { - if (feature instanceof Cesium.Cesium3DTileFeature && feature?.tileset?.msId) { - const msId = feature.tileset.msId; - // 3d tile feature does not contain a geometry in the Cesium3DTileFeature class - // it has content but refers to the whole tile model - const propertyNames = feature.getPropertyNames(); - const properties = Object.fromEntries(propertyNames.map(key => [key, feature.getProperty(key)])); - return { - ...acc, - [msId]: acc[msId] - ? [...acc[msId], { type: 'Feature', properties, geometry: null }] - : [{ type: 'Feature', properties, geometry: null }] - }; - } - return acc; - }, []); - return Object.keys(groupIntersectedFeatures).map(id => ({ id, features: groupIntersectedFeatures[id] })); + // we can use pick so the only first intersect feature will be returned + // this is more intuitive for uses such as get feature info + const feature = map.scene.pick(position); + if (feature instanceof Cesium.Cesium3DTileFeature && feature?.tileset?.msId) { + const msId = feature.tileset.msId; + // 3d tile feature does not contain a geometry in the Cesium3DTileFeature class + // it has content but refers to the whole tile model + const propertyNames = feature.getPropertyNames(); + const properties = Object.fromEntries(propertyNames.map(key => [key, feature.getProperty(key)])); + return [{ id: msId, features: [{ type: 'Feature', properties, geometry: null }] }]; } return []; } diff --git a/web/client/containers/MapViewer.jsx b/web/client/containers/MapViewer.jsx index f972236914..f039506b62 100644 --- a/web/client/containers/MapViewer.jsx +++ b/web/client/containers/MapViewer.jsx @@ -9,21 +9,33 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import assign from 'object-assign'; import url from 'url'; +import isEqual from 'lodash/isEqual'; const urlQuery = url.parse(window.location.href, true).query; import ConfigUtils from '../utils/ConfigUtils'; import { getMonitoredState } from '../utils/PluginsUtils'; +import { createShallowSelectorCreator } from '../utils/ReselectUtils'; -const PluginsContainer = connect((state) => ({ - statePluginsConfig: state.plugins, - mode: urlQuery.mode || state.mode || (state.browser && state.browser.mobile ? 'mobile' : 'desktop'), - pluginsState: assign({}, state && state.controls, state && state.layers && state.layers.settings && { - layerSettings: state.layers.settings - }), - monitoredState: getMonitoredState(state, ConfigUtils.getConfigProp('monitorState')) -}))(require('../components/plugins/PluginsContainer').default); +const PluginsContainer = connect( + createShallowSelectorCreator(isEqual)( + state => state.plugins, + state => state.mode, + state => state?.browser?.mobile, + state => state.controls, + state => state?.layers?.settings, + state => getMonitoredState(state, ConfigUtils.getConfigProp('monitorState')), + (statePluginsConfig, stateMode, mobile, controls, layerSettings, monitoredState) => ({ + statePluginsConfig, + mode: urlQuery.mode || stateMode || (mobile ? 'mobile' : 'desktop'), + pluginsState: { + ...controls, + ...(layerSettings && { layerSettings }) + }, + monitoredState + }) + ) +)(require('../components/plugins/PluginsContainer').default); class MapViewer extends React.Component { static propTypes = { diff --git a/web/client/epics/__tests__/identify-test.js b/web/client/epics/__tests__/identify-test.js index cb08f31b0b..c8b90379ef 100644 --- a/web/client/epics/__tests__/identify-test.js +++ b/web/client/epics/__tests__/identify-test.js @@ -72,6 +72,7 @@ import { setControlProperties } from '../../actions/controls'; import { BROWSE_DATA } from '../../actions/layers'; import { configureMap } from '../../actions/config'; import { changeMapType } from './../../actions/maptype'; +import { FORCE_UPDATE_MAP_LAYOUT } from '../../actions/maplayout'; const TEST_MAP_STATE = { present: { @@ -173,11 +174,11 @@ describe('identify Epics', () => { } }; const sentActions = [featureInfoClick({ latlng: { lat: 36.95, lng: -79.84 } })]; - const NUM_ACTIONS = 5; + const NUM_ACTIONS = 6; testEpic(getFeatureInfoOnFeatureInfoClick, NUM_ACTIONS, sentActions, (actions) => { try { - expect(actions.length).toBe(5); - const [a0, a1, a2, a3, a4] = actions; + expect(actions.length).toBe(6); + const [a0, a1, a2, a3, a4, a5] = actions; expect(a0).toExist(); expect(a0.type).toBe(PURGE_MAPINFO_RESULTS); expect(a1).toExist(); @@ -194,8 +195,12 @@ describe('identify Epics', () => { expect(a3.requestParams).toExist(); expect(a3.reqId).toExist(); expect(a3.layerMetadata.title).toBe(state.layers.flat[a3.requestParams.id === "TEST" ? 0 : 1].title); + expect(a4).toExist(); - expect(a4.layerMetadata.title).toBe(state.layers.flat[a4.requestParams.id === "TEST" ? 0 : 1].title); + expect(a4.type).toBe(FORCE_UPDATE_MAP_LAYOUT); + + expect(a5).toExist(); + expect(a5.layerMetadata.title).toBe(state.layers.flat[a5.requestParams.id === "TEST" ? 0 : 1].title); done(); } catch (ex) { done(ex); @@ -580,7 +585,7 @@ describe('identify Epics', () => { }, state); }); it('getFeatureInfoOnFeatureInfoClick with enableInfoForSelectedLayers set to false', (done) => { - const NUM_ACTIONS = 5; + const NUM_ACTIONS = 6; const state = { map: TEST_MAP_STATE, mapInfo: { @@ -624,6 +629,8 @@ describe('identify Epics', () => { expect(action.reqId).toBeTruthy(); expect([state.layers.flat[0].title, state.layers.flat[1].title].includes(action.layerMetadata.title)).toBeTruthy(); break; + case FORCE_UPDATE_MAP_LAYOUT: + break; default: expect(true).toBe(false); diff --git a/web/client/epics/identify.js b/web/client/epics/identify.js index f871afdbf0..38612091f5 100644 --- a/web/client/epics/identify.js +++ b/web/client/epics/identify.js @@ -33,6 +33,7 @@ import { closeAnnotations } from '../actions/annotations'; import { MAP_CONFIG_LOADED } from '../actions/config'; import {addPopup, cleanPopups, removePopup, REMOVE_MAP_POPUP} from '../actions/mapPopups'; import { cancelSelectedItem } from '../actions/search'; +import { forceUpdateMapLayout } from '../actions/maplayout'; import { stopGetFeatureInfoSelector, identifyOptionsSelector, clickPointSelector, clickLayerSelector, @@ -96,6 +97,9 @@ export const getFeatureInfoOnFeatureInfoClick = (action$, { getState = () => { } "filter", "propertyName" ]; + + let firstResponseReturned = false; + const out$ = Rx.Observable.from((queryableLayers.filter(l => { // filtering a subset of layers return filterNameList.length ? (filterNameList.filter(name => name.indexOf(l.name) !== -1).length > 0) : true; @@ -120,12 +124,26 @@ export const getFeatureInfoOnFeatureInfoClick = (action$, { getState = () => { } const reqId = uuid.v1(); const param = { ...appParams, ...requestParams }; return getFeatureInfo(basePath, param, layer, {attachJSON, itemId}) + // this 0 delay is needed for vector/3dtiles layer because makes the response async and give time to the GUI to render + // these type of layers don't perform requests to the server because the values are taken from the client map so the response were applied synchronously + // this delay allows the panel to open and show the spinner for the first one + // this delay mitigates the freezing of the app when there are a great amount of queried layers at the same time + .delay(0) .map((response) => response.data.exceptions ? exceptionsFeatureInfo(reqId, response.data.exceptions, requestParams, lMetaData) : loadFeatureInfo(reqId, response.data, requestParams, { ...lMetaData, features: response.features, featuresCrs: response.featuresCrs }, layer) ) .catch((e) => Rx.Observable.of(errorFeatureInfo(reqId, e.data || e.statusText || e.status, requestParams, lMetaData))) + .concat(Rx.Observable.defer(() => { + // update the layout only after the initial response + // we don't need to trigger this for each query layer + if (!firstResponseReturned) { + firstResponseReturned = true; + return Rx.Observable.of(forceUpdateMapLayout()); + } + return Rx.Observable.empty(); + })) .startWith(newMapInfoRequest(reqId, param)); } return Rx.Observable.of(getVectorInfo(layer, request, metadata, queryableLayers)); @@ -147,10 +165,13 @@ export const handleMapInfoMarker = (action$, {getState}) => ? hideMapinfoMarker() : showMapinfoMarker() ); -export const closeFeatureGridFromIdentifyEpic = (action$) => +export const closeFeatureGridFromIdentifyEpic = (action$, store) => action$.ofType(LOAD_FEATURE_INFO, GET_VECTOR_INFO) .switchMap(() => { - return Rx.Observable.of(closeFeatureGrid()); + if (isFeatureGridOpen(store.getState())) { + return Rx.Observable.of(closeFeatureGrid()); + } + return Rx.Observable.empty(); }); /** * Check if something is editing in feature info. diff --git a/web/client/epics/maplayout.js b/web/client/epics/maplayout.js index b71bdf8b5a..1bf1bef618 100644 --- a/web/client/epics/maplayout.js +++ b/web/client/epics/maplayout.js @@ -7,17 +7,14 @@ */ import Rx from 'rxjs'; -import { updateMapLayout } from '../actions/maplayout'; -import { TOGGLE_CONTROL, SET_CONTROL_PROPERTY, SET_CONTROL_PROPERTIES } from '../actions/controls'; +import {updateMapLayout, FORCE_UPDATE_MAP_LAYOUT} from '../actions/maplayout'; +import {TOGGLE_CONTROL, SET_CONTROL_PROPERTY, SET_CONTROL_PROPERTIES} from '../actions/controls'; import { MAP_CONFIG_LOADED } from '../actions/config'; import { SIZE_CHANGE, CLOSE_FEATURE_GRID, OPEN_FEATURE_GRID } from '../actions/featuregrid'; import { CLOSE_IDENTIFY, - ERROR_FEATURE_INFO, TOGGLE_MAPINFO_STATE, - LOAD_FEATURE_INFO, - EXCEPTIONS_FEATURE_INFO, NO_QUERYABLE_LAYERS } from '../actions/mapInfo'; @@ -55,14 +52,13 @@ export const updateMapLayoutEpic = (action$, store) => CLOSE_IDENTIFY, NO_QUERYABLE_LAYERS, TOGGLE_MAPINFO_STATE, - LOAD_FEATURE_INFO, - EXCEPTIONS_FEATURE_INFO, TOGGLE_CONTROL, SET_CONTROL_PROPERTY, SET_CONTROL_PROPERTIES, SHOW_SETTINGS, HIDE_SETTINGS, - ERROR_FEATURE_INFO) + FORCE_UPDATE_MAP_LAYOUT + ) .switchMap(() => { const state = store.getState(); diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index 4de1d70056..fec0bc81aa 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -9,7 +9,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; import { compose, branch, withPropsOnChange } from 'recompose'; import { Glyphicon } from 'react-bootstrap'; @@ -66,6 +65,8 @@ import { createWidget } from '../actions/widgets'; import { getMetadataRecordById } from '../actions/catalog'; import { activeSelector } from '../selectors/catalog'; import { isCesium } from '../selectors/maptype'; +import { createShallowSelectorCreator } from '../utils/ReselectUtils'; +import isEqual from 'lodash/isEqual'; const addFilteredAttributesGroups = (nodes, filters) => { return nodes.reduce((newNodes, currentNode) => { @@ -91,28 +92,27 @@ const filterLayersByTitle = (layer, filterText, currentLocale) => { return title.toLowerCase().indexOf(filterText.toLowerCase()) !== -1; }; -const tocSelector = createSelector( - [ - (state) => state.controls && state.controls.toolbar && state.controls.toolbar.active === 'toc', - groupsSelector, - layerSettingSelector, - layerSwipeSettingsSelector, - layerMetadataSelector, - wfsDownloadSelector, - mapSelector, - currentLocaleSelector, - currentLocaleLanguageSelector, - selectedNodesSelector, - layerFilterSelector, - layersSelector, - mapNameSelector, - activeSelector, - widgetBuilderAvailable, - generalInfoFormatSelector, - isCesium, - userSelector, - isLocalizedLayerStylesEnabledSelector - ], (enabled, groups, settings, swipeSettings, layerMetadata, layerdownload, map, currentLocale, currentLocaleLanguage, selectedNodes, filterText, layers, mapName, catalogActive, activateWidgetTool, generalInfoFormat, isCesiumActive, user, isLocalizedLayerStylesEnabled) => ({ +const tocSelector = createShallowSelectorCreator(isEqual)( + (state) => state.controls && state.controls.toolbar && state.controls.toolbar.active === 'toc', + groupsSelector, + layerSettingSelector, + layerSwipeSettingsSelector, + layerMetadataSelector, + wfsDownloadSelector, + mapSelector, + currentLocaleSelector, + currentLocaleLanguageSelector, + selectedNodesSelector, + layerFilterSelector, + layersSelector, + mapNameSelector, + activeSelector, + widgetBuilderAvailable, + generalInfoFormatSelector, + isCesium, + userSelector, + isLocalizedLayerStylesEnabledSelector, + (enabled, groups, settings, swipeSettings, layerMetadata, layerdownload, map, currentLocale, currentLocaleLanguage, selectedNodes, filterText, layers, mapName, catalogActive, activateWidgetTool, generalInfoFormat, isCesiumActive, user, isLocalizedLayerStylesEnabled) => ({ enabled, groups, settings, diff --git a/web/client/plugins/map/selector.js b/web/client/plugins/map/selector.js index 339e8e516f..9ac8043939 100644 --- a/web/client/plugins/map/selector.js +++ b/web/client/plugins/map/selector.js @@ -1,5 +1,3 @@ -import { createStructuredSelector } from 'reselect'; - import { mapSelector, projectionDefsSelector, isMouseMoveCoordinatesActiveSelector } from '../../selectors/map'; import { mapTypeSelector, isOpenlayers } from '../../selectors/maptype'; import { layerSelectorWithMarkers } from '../../selectors/layers'; @@ -7,21 +5,38 @@ import { highlighedFeatures } from '../../selectors/highlight'; import { securityTokenSelector } from '../../selectors/security'; import { currentLocaleLanguageSelector } from '../../selectors/locale'; import { isLocalizedLayerStylesEnabledSelector, localizedLayerStylesNameSelector } from '../../selectors/localizedLayerStyles'; +import { createShallowSelectorCreator } from '../../utils/ReselectUtils'; +import isEqual from 'lodash/isEqual'; /** * Map state to props selector for Map plugin */ -export default createStructuredSelector({ - projectionDefs: projectionDefsSelector, - map: mapSelector, - mapType: mapTypeSelector, - layers: layerSelectorWithMarkers, - features: highlighedFeatures, - loadingError: state => state.mapInitialConfig && state.mapInitialConfig.loadingError && state.mapInitialConfig.loadingError.data, - securityToken: securityTokenSelector, - elevationEnabled: isMouseMoveCoordinatesActiveSelector, - shouldLoadFont: isOpenlayers, - isLocalizedLayerStylesEnabled: isLocalizedLayerStylesEnabledSelector, - localizedLayerStylesName: localizedLayerStylesNameSelector, - currentLocaleLanguage: currentLocaleLanguageSelector -}); + +export default createShallowSelectorCreator(isEqual)( + projectionDefsSelector, + mapSelector, + mapTypeSelector, + layerSelectorWithMarkers, + highlighedFeatures, + state => state.mapInitialConfig && state.mapInitialConfig.loadingError && state.mapInitialConfig.loadingError.data, + securityTokenSelector, + isMouseMoveCoordinatesActiveSelector, + isOpenlayers, + isLocalizedLayerStylesEnabledSelector, + localizedLayerStylesNameSelector, + currentLocaleLanguageSelector, + (projectionDefs, map, mapType, layers, features, loadingError, securityToken, elevationEnabled, shouldLoadFont, isLocalizedLayerStylesEnabled, localizedLayerStylesName, currentLocaleLanguage) => ({ + projectionDefs, + map, + mapType, + layers, + features, + loadingError, + securityToken, + elevationEnabled, + shouldLoadFont, + isLocalizedLayerStylesEnabled, + localizedLayerStylesName, + currentLocaleLanguage + }) +); diff --git a/web/client/reducers/mapInfo.js b/web/client/reducers/mapInfo.js index 7999e720cd..1133a6249b 100644 --- a/web/client/reducers/mapInfo.js +++ b/web/client/reducers/mapInfo.js @@ -53,12 +53,14 @@ import { getValidator } from '../utils/MapInfoUtils'; const isIndexValid = (state, responses, requestIndex, isVector) => { const {configuration, requests, queryableLayers = [], index} = state; const {infoFormat} = configuration || {}; - + const { layer = {} } = responses[requestIndex] || {}; + // these layers do not perform requests to a backend + const isVectorLayer = !!(isVector || layer.type === '3dtiles'); // Index when first response received is valid const validResponse = getValidator(infoFormat)?.getValidResponses([responses[requestIndex]]); const inValidResponse = getValidator(infoFormat)?.getNoValidResponses(responses); const cond1 = isUndefined(index) && !!validResponse.length; - const cond2 = !isVector && requests.length === inValidResponse.filter(res => res).length; + const cond2 = !isVectorLayer && requests.length === inValidResponse.filter(res => res).length; const cond3 = isUndefined(index) && isVector && requests.filter(r => isEmpty(r)).length === queryableLayers.length; return (cond1 || cond2 || cond3); // Check if all requested layers are vector diff --git a/web/client/selectors/layers.js b/web/client/selectors/layers.js index 6c2bc2764b..648e6d2cab 100644 --- a/web/client/selectors/layers.js +++ b/web/client/selectors/layers.js @@ -61,7 +61,7 @@ export const layerSelectorWithMarkers = createSelector( }} })); const coords = centerToMarker === 'enabled' ? getNormalizedLatLon(markerPosition.latlng) : markerPosition.latlng; - newLayers.push(getMarkerLayer("GetFeatureInfo", coords)); + newLayers.push(getMarkerLayer("GetFeatureInfo", { ...coords, height: markerPosition.height })); } if ( highlightPoint ) { const coords = centerToMarker === 'enabled' ? getNormalizedLatLon(highlightPoint.latlng) : highlightPoint.latlng; diff --git a/web/client/utils/MapInfoUtils.js b/web/client/utils/MapInfoUtils.js index 1828c97a08..9784911dce 100644 --- a/web/client/utils/MapInfoUtils.js +++ b/web/client/utils/MapInfoUtils.js @@ -113,7 +113,13 @@ export const clickedPointToGeoJson = (clickedPoint) => { type: "Feature", geometry: { type: 'Point', - coordinates: [parseFloat(clickedPoint.lng), parseFloat(clickedPoint.lat)] + coordinates: [ + parseFloat(clickedPoint.lng), + parseFloat(clickedPoint.lat), + ...(clickedPoint.height !== undefined + ? [parseFloat(clickedPoint.height)] + : []) + ] }, style: [{ iconUrl, diff --git a/web/client/utils/cesium/ClickUtils.js b/web/client/utils/cesium/ClickUtils.js index a48e5a453f..e94764897a 100644 --- a/web/client/utils/cesium/ClickUtils.js +++ b/web/client/utils/cesium/ClickUtils.js @@ -18,11 +18,18 @@ export const getCartesian = function(viewer, event) { return null; }; export const getMouseXYZ = (viewer, event) => { - var scene = viewer.scene; + const scene = viewer.scene; const mousePosition = event.position || event.endPosition; if (!mousePosition) { return null; } + + const feature = scene.pick(mousePosition); + if (feature) { + const depthCartesian = scene.pickPosition(mousePosition); + return Cesium.Cartographic.fromCartesian(depthCartesian); + } + const ray = viewer.camera.getPickRay(mousePosition); const position = viewer.scene.globe.pick(ray, viewer.scene); const ellipsoid = scene._globe.ellipsoid;