From eca6b146af7b4d56985f30097836c38b3b5ff64c Mon Sep 17 00:00:00 2001 From: stefano bovio Date: Wed, 6 Dec 2023 12:25:37 +0100 Subject: [PATCH] Fix #9775 Visibility limits not working in 3D for detached layers (#9777) * Fix #9775 Visibility limits not working in 3D for detached layers * requested changes * udpate zoom from height comment --- .../settings/VisibilityLimitsForm.jsx | 7 -- .../__tests__/VisibilityLimitsForm-test.jsx | 2 - web/client/components/map/cesium/Layer.jsx | 52 +++++++-- web/client/components/map/cesium/Map.jsx | 10 +- .../map/cesium/__tests__/Layer-test.jsx | 12 +- .../map/cesium/__tests__/Map-test.jsx | 2 +- .../map/cesium/plugins/MarkerLayer.js | 17 +-- .../map/cesium/plugins/OverlayLayer.js | 20 ++-- .../map/cesium/plugins/TerrainLayer.js | 3 +- .../map/cesium/plugins/ThreeDTilesLayer.js | 110 +++++++++--------- .../map/cesium/plugins/VectorLayer.js | 74 ++++++------ .../components/map/cesium/plugins/WFSLayer.js | 81 +++++++------ 12 files changed, 218 insertions(+), 172 deletions(-) diff --git a/web/client/components/TOC/fragments/settings/VisibilityLimitsForm.jsx b/web/client/components/TOC/fragments/settings/VisibilityLimitsForm.jsx index e51683cf7c..5d1cc702e1 100644 --- a/web/client/components/TOC/fragments/settings/VisibilityLimitsForm.jsx +++ b/web/client/components/TOC/fragments/settings/VisibilityLimitsForm.jsx @@ -254,13 +254,6 @@ function VisibilityLimitsForm({ clearMessages(); }, [ dpu, resolutionString ]); - useEffect(() => { - if (isMounted.current && (!isNil(maxResolution) || !isNil(minResolution))) { - setCapabilitiesMessage(maxResolution, minResolution); - setRangeError(maxResolution, minResolution); - } - }, [isMounted]); - return (
diff --git a/web/client/components/TOC/fragments/settings/__tests__/VisibilityLimitsForm-test.jsx b/web/client/components/TOC/fragments/settings/__tests__/VisibilityLimitsForm-test.jsx index a9226b24e2..bd441c78bb 100644 --- a/web/client/components/TOC/fragments/settings/__tests__/VisibilityLimitsForm-test.jsx +++ b/web/client/components/TOC/fragments/settings/__tests__/VisibilityLimitsForm-test.jsx @@ -47,8 +47,6 @@ describe('VisibilityLimitsForm', () => { '1 : 37795', 'layerProperties.visibilityLimits.scale' ]); - const message = document.querySelector('.alert-success'); - expect(message.textContent).toBe('layerProperties.visibilityLimits.serverValuesUpdate'); }); it('should render maxResolution and minResolution labels as resolution', () => { const layer = { diff --git a/web/client/components/map/cesium/Layer.jsx b/web/client/components/map/cesium/Layer.jsx index 6896af105b..bead9e5ee9 100644 --- a/web/client/components/map/cesium/Layer.jsx +++ b/web/client/components/map/cesium/Layer.jsx @@ -25,8 +25,11 @@ class CesiumLayer extends React.Component { }; componentDidMount() { - this.createLayer(this.props.type, this.props.options, this.props.position, this.props.map, this.props.securityToken); - if (this.props.options && this.layer && this.getVisibilityOption(this.props)) { + // initial visibility should also take into account the visibility limits + // in particular for detached layers (eg. Vector, WFS, 3D Tiles, ...) + const visibility = this.getVisibilityOption(this.props); + this.createLayer(this.props.type, { ...this.props.options, visibility }, this.props.position, this.props.map, this.props.securityToken); + if (this.props.options && this.layer && visibility) { this.addLayer(this.props); this.updateZIndex(); } @@ -129,19 +132,46 @@ class CesiumLayer extends React.Component { } }; + setDetachedLayerVisibility = (visibility, props) => { + // use internal setVisible + // if a detached layers implements setVisible + if (this.layer?.setVisible) { + this.layer.setVisible(visibility); + return; + } + // if visible we will remove the layer and create a new one + if (visibility) { + this.removeLayer(); + this.createLayer(props.type, { + ...props.options, + visibility + }, props.position, props.map, props.securityToken); + return; + } + // while hidden layers will be completely removed + this.removeLayer(); + return; + }; + + setImageryLayerVisibility = (visibility, props) => { + // this type of layer will be added and removed from the imageryLayers array of Cesium + if (visibility) { + this.addLayer(props); + this.updateZIndex(); + return; + } + this.removeLayer(); + return; + } + setLayerVisibility = (newProps) => { const oldVisibility = this.getVisibilityOption(this.props); const newVisibility = this.getVisibilityOption(newProps); if (newVisibility !== oldVisibility) { - if (this.layer?.detached && this.layer?.setVisible) { - this.layer.setVisible(newVisibility); + if (!!this.layer?.detached) { + this.setDetachedLayerVisibility(newVisibility, newProps); } else { - if (newVisibility) { - this.addLayer(newProps); - this.updateZIndex(); - } else { - this.removeLayer(); - } + this.setImageryLayerVisibility(newVisibility, newProps); } newProps.map.scene.requestRender(); } @@ -167,7 +197,7 @@ class CesiumLayer extends React.Component { return false; } } - return visibility; + return !!visibility; }; setLayerOpacity = (opacity) => { diff --git a/web/client/components/map/cesium/Map.jsx b/web/client/components/map/cesium/Map.jsx index 9caa4d90bd..474e9d0339 100644 --- a/web/client/components/map/cesium/Map.jsx +++ b/web/client/components/map/cesium/Map.jsx @@ -336,7 +336,15 @@ class CesiumMap extends React.Component { }; getZoomFromHeight = (height) => { - return Math.log2(this.props.zoomToHeight / height) + 1; + let distance = height; + // when camera is tilted we could compute the height as the distance between the camera point of view and the viewed point on the map + // the viewed point or target is computed as the intersection of an imaginary vector based on the camera direction (ray) and the globe surface + // if the camera is orthogonal to the globe distance should match the height so this computation is still valid + const target = this.map.scene.globe.pick(new Cesium.Ray(this.map.camera.position, this.map.camera.direction), this.map.scene); + if (target) { + distance = Cesium.Cartesian3.distance(target, this.map.camera.position); + } + return Math.log2(this.props.zoomToHeight / distance) + 1; }; getHeightFromZoom = (zoom) => { diff --git a/web/client/components/map/cesium/__tests__/Layer-test.jsx b/web/client/components/map/cesium/__tests__/Layer-test.jsx index 2e7ab23bb2..f8cf9e2cef 100644 --- a/web/client/components/map/cesium/__tests__/Layer-test.jsx +++ b/web/client/components/map/cesium/__tests__/Layer-test.jsx @@ -588,7 +588,8 @@ describe('Cesium layer', () => { let options = { id: 'overlay-1', - position: { x: 13, y: 43 } + position: { x: 13, y: 43 }, + visibility: true }; // create layers let layer = ReactDOM.render( @@ -616,7 +617,8 @@ describe('Cesium layer', () => { position: { x: 13, y: 43 }, onClose: () => { closed = true; - } + }, + visibility: true }; // create layers let layer = ReactDOM.render( @@ -643,7 +645,8 @@ describe('Cesium layer', () => { document.body.appendChild(element); let options = { id: 'overlay-1', - position: { x: 13, y: 43 } + position: { x: 13, y: 43 }, + visibility: true }; // create layers let layer = ReactDOM.render( @@ -659,7 +662,8 @@ describe('Cesium layer', () => { it('creates a marker layer for cesium map', () => { let options = { - point: { lng: 13, lat: 43 } + point: { lng: 13, lat: 43 }, + visibility: true }; // create layers let layer = ReactDOM.render( diff --git a/web/client/components/map/cesium/__tests__/Map-test.jsx b/web/client/components/map/cesium/__tests__/Map-test.jsx index 05f7bd6cca..04c4a7a8ff 100644 --- a/web/client/components/map/cesium/__tests__/Map-test.jsx +++ b/web/client/components/map/cesium/__tests__/Map-test.jsx @@ -168,7 +168,7 @@ describe('CesiumMap', () => { try { expect(Math.round(Math.round(center.y * precision) / precision)).toBe(30); expect(Math.round(Math.round(center.x * precision) / precision)).toBe(20); - expect(zoom).toBe(5); + expect(Math.round(zoom)).toBe(5); expect(bbox.bounds).toBeTruthy(); expect(bbox.crs).toBeTruthy(); expect(size.height).toBeTruthy(); diff --git a/web/client/components/map/cesium/plugins/MarkerLayer.js b/web/client/components/map/cesium/plugins/MarkerLayer.js index aeb1e5b201..1847d39152 100644 --- a/web/client/components/map/cesium/plugins/MarkerLayer.js +++ b/web/client/components/map/cesium/plugins/MarkerLayer.js @@ -16,6 +16,13 @@ import { isEqual } from 'lodash'; */ Layers.registerType('marker', { create: (options, map) => { + if (!options.visibility) { + return { + detached: true, + point: undefined, + remove: () => {} + }; + } const style = { point: { pixelSize: 5, @@ -26,7 +33,7 @@ Layers.registerType('marker', { ...options.style }; const point = map.entities.add({ - position: Cesium.Cartesian3.fromDegrees(options.point.lng, options.point.lat), + position: Cesium.Cartesian3.fromDegrees(options?.point?.lng || 0, options?.point?.lat || 0), ...style }); return { @@ -38,12 +45,8 @@ Layers.registerType('marker', { }; }, update: function(layer, newOptions, oldOptions, map) { - if (!isEqual(newOptions.point, oldOptions.point) - || newOptions.visibility !== oldOptions.visibility) { - layer.remove(); - return newOptions.visibility - ? this.create(newOptions, map) - : null; + if (!isEqual(newOptions.point, oldOptions.point)) { + return this.create(newOptions, map); } return null; } diff --git a/web/client/components/map/cesium/plugins/OverlayLayer.js b/web/client/components/map/cesium/plugins/OverlayLayer.js index f7beacf075..81f233f101 100644 --- a/web/client/components/map/cesium/plugins/OverlayLayer.js +++ b/web/client/components/map/cesium/plugins/OverlayLayer.js @@ -166,11 +166,19 @@ const cloneOriginalOverlay = (original, options) => { Layers.registerType('overlay', { create: (options, map) => { + if (!options.visibility) { + return { + detached: true, + info: undefined, + remove: () => {} + }; + } const original = document.getElementById(options.id); - const cloned = cloneOriginalOverlay(original, options); + // use a div fallback to avoid error if the original element does not exist + const cloned = original ? cloneOriginalOverlay(original, options) : document.createElement('div'); let infoWindow = new InfoWindow(map); - infoWindow.showAt(options.position.y, options.position.x, cloned); + infoWindow.showAt(options?.position?.y || 0, options?.position?.x || 0, cloned); infoWindow.setVisible(true); let info = map.scene.primitives.add(infoWindow); @@ -183,12 +191,8 @@ Layers.registerType('overlay', { }; }, update: function(layer, newOptions, oldOptions, map) { - if (!isEqual(newOptions.position, oldOptions.position) - || newOptions.visibility !== oldOptions.visibility) { - layer.remove(); - return newOptions.visibility - ? this.create(newOptions, map) - : null; + if (!isEqual(newOptions.position, oldOptions.position)) { + return this.create(newOptions, map); } return null; } diff --git a/web/client/components/map/cesium/plugins/TerrainLayer.js b/web/client/components/map/cesium/plugins/TerrainLayer.js index 41755b5462..0351362f4b 100644 --- a/web/client/components/map/cesium/plugins/TerrainLayer.js +++ b/web/client/components/map/cesium/plugins/TerrainLayer.js @@ -48,8 +48,7 @@ const createLayer = (config, map) => { terrainProvider, remove: () => { map.terrainProvider = new Cesium.EllipsoidTerrainProvider(); - }, - setVisible: () => {} + } }; }; diff --git a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js index 812c0abd5e..109a5a82ef 100644 --- a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js +++ b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js @@ -129,73 +129,67 @@ function updateShading(tileSet, options, map) { Layers.registerType('3dtiles', { create: (options, map) => { - if (options.visibility && options.url) { - - let tileSet; - const resource = new Cesium.Resource({ - url: options.url, - proxy: needProxy(options.url) ? new Cesium.DefaultProxy(getProxyUrl()) : undefined - // TODO: axios supports also adding access tokens or credentials (e.g. authkey, Authentication header ...). - // if we want to use internal cesium functionality to retrieve data - // we need to create a utility to set a CesiumResource that applies also this part. - // in addition to this proxy. - }); - Cesium.Cesium3DTileset.fromUrl(resource, - { - showCreditsOnScreen: true - } - ).then((_tileSet) => { - tileSet = _tileSet; - updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet); - map.scene.primitives.add(tileSet); - // assign the original mapstore id of the layer - tileSet.msId = options.id; - - ensureReady(tileSet, () => { - updateModelMatrix(tileSet, options); - clip3DTiles(tileSet, options, map); - updateShading(tileSet, options, map); - getStyle(options) - .then((style) => { - if (style) { - tileSet.style = new Cesium.Cesium3DTileStyle(style); - } - }); - }); - }); - + if (!options.visibility) { return { detached: true, - getTileSet: () => tileSet, - resource, - remove: () => { - if (tileSet) { - updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet); - map.scene.primitives.remove(tileSet); - } - }, - setVisible: (visible) => { - if (tileSet) { - tileSet.show = !!visible; - } - } + getTileSet: () => undefined, + remove: () => {} }; } + let tileSet; + const resource = new Cesium.Resource({ + url: options.url, + proxy: needProxy(options.url) ? new Cesium.DefaultProxy(getProxyUrl()) : undefined + // TODO: axios supports also adding access tokens or credentials (e.g. authkey, Authentication header ...). + // if we want to use internal cesium functionality to retrieve data + // we need to create a utility to set a CesiumResource that applies also this part. + // in addition to this proxy. + }); + let promise = Cesium.Cesium3DTileset.fromUrl(resource, + { + showCreditsOnScreen: true + } + ).then((_tileSet) => { + tileSet = _tileSet; + updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet); + map.scene.primitives.add(tileSet); + // assign the original mapstore id of the layer + tileSet.msId = options.id; + + ensureReady(tileSet, () => { + updateModelMatrix(tileSet, options); + clip3DTiles(tileSet, options, map); + updateShading(tileSet, options, map); + getStyle(options) + .then((style) => { + if (style) { + tileSet.style = new Cesium.Cesium3DTileStyle(style); + } + }); + }); + }); + const removeTileset = () => { + updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet); + map.scene.primitives.remove(tileSet); + tileSet = undefined; + }; return { detached: true, - getTileSet: () => undefined, - remove: () => {}, - setVisible: () => {} + getTileSet: () => tileSet, + resource, + remove: () => { + if (tileSet) { + removeTileset(); + return; + } + promise.then(() => { + removeTileset(); + }); + return; + } }; }, update: function(layer, newOptions, oldOptions, map) { - if (newOptions.visibility && !oldOptions.visibility) { - return this.create(newOptions, map); - } - if (!newOptions.visibility && oldOptions.visibility && layer?.remove) { - layer.remove(); - return null; - } const tileSet = layer?.getTileSet(); if ( (!isEqual(newOptions.clippingPolygon, oldOptions.clippingPolygon) diff --git a/web/client/components/map/cesium/plugins/VectorLayer.js b/web/client/components/map/cesium/plugins/VectorLayer.js index ade7ce74b5..2020c1f8ee 100644 --- a/web/client/components/map/cesium/plugins/VectorLayer.js +++ b/web/client/components/map/cesium/plugins/VectorLayer.js @@ -18,42 +18,50 @@ import { applyDefaultStyleToVectorLayer } from '../../../../utils/StyleUtils'; const createLayer = (options, map) => { - let dataSource = new Cesium.GeoJsonDataSource(options?.id); + if (!options.visibility) { + return { + detached: true, + dataSource: undefined, + remove: () => {} + }; + } + let dataSource = new Cesium.GeoJsonDataSource(options?.id); + dataSource.loadingEvent.addEventListener(() => { + // ensure it updates render on every loading + map.scene.requestRender(); + }); const features = flattenFeatures(options?.features || [], ({ style, ...feature }) => feature); const collection = { type: 'FeatureCollection', features }; - - if (options.visibility) { - dataSource.load(collection, { - // ensure default style is not applied - stroke: new Cesium.Color(0, 0, 0, 0), - fill: new Cesium.Color(0, 0, 0, 0), - markerColor: new Cesium.Color(0, 0, 0, 0), - strokeWidth: 0, - markerSize: 0 - }).then(() => { - map.dataSources.add(dataSource); - layerToGeoStylerStyle(options) - .then((style) => { - getStyle(applyDefaultStyleToVectorLayer({ ...options, style }), 'cesium') - .then((styleFunc) => { - if (styleFunc) { - styleFunc({ - entities: dataSource?.entities?.values, - map, - opacity: options.opacity ?? 1, - features: options.features - }).then(() => { - map.scene.requestRender(); - }); - } - }); - }); - }); - } + dataSource.load(collection, { + // ensure default style is not applied + stroke: new Cesium.Color(0, 0, 0, 0), + fill: new Cesium.Color(0, 0, 0, 0), + markerColor: new Cesium.Color(0, 0, 0, 0), + strokeWidth: 0, + markerSize: 0 + }).then(() => { + map.dataSources.add(dataSource); + layerToGeoStylerStyle(options) + .then((style) => { + getStyle(applyDefaultStyleToVectorLayer({ ...options, style }), 'cesium') + .then((styleFunc) => { + if (styleFunc) { + styleFunc({ + entities: dataSource?.entities?.values, + map, + opacity: options.opacity ?? 1, + features: options.features + }).then(() => { + map.scene.requestRender(); + }); + } + }); + }); + }); dataSource.show = !!options.visibility; dataSource.queryable = options.queryable === undefined || options.queryable; @@ -66,16 +74,14 @@ const createLayer = (options, map) => { map.dataSources.remove(dataSource); dataSource = undefined; } - }, - setVisible: () => {} + } }; }; Layers.registerType('vector', { create: createLayer, update: (layer, newOptions, oldOptions, map) => { - if (!isEqual(newOptions.features, oldOptions.features) - || newOptions.visibility !== oldOptions.visibility) { + if (!isEqual(newOptions.features, oldOptions.features)) { return createLayer(newOptions, map); } diff --git a/web/client/components/map/cesium/plugins/WFSLayer.js b/web/client/components/map/cesium/plugins/WFSLayer.js index d858f2d52a..dcf016c3ba 100644 --- a/web/client/components/map/cesium/plugins/WFSLayer.js +++ b/web/client/components/map/cesium/plugins/WFSLayer.js @@ -32,45 +32,54 @@ const requestFeatures = (options, params, cancelToken) => { const createLayer = (options, map) => { - let dataSource = new Cesium.GeoJsonDataSource(options?.id); + if (!options.visibility) { + return { + detached: true, + dataSource: undefined, + remove: () => {} + }; + } + let dataSource = new Cesium.GeoJsonDataSource(options?.id); + dataSource.loadingEvent.addEventListener(() => { + // ensure it updates render on every loading + map.scene.requestRender(); + }); const params = optionsToVendorParams(options); const cancelToken = axios.CancelToken; const source = cancelToken.source(); - - if (options.visibility) { - requestFeatures(options, params, source.token) - .then(({ data: collection }) => { - dataSource.load(collection, { - // ensure default style is not applied - stroke: new Cesium.Color(0, 0, 0, 0), - fill: new Cesium.Color(0, 0, 0, 0), - markerColor: new Cesium.Color(0, 0, 0, 0), - strokeWidth: 0, - markerSize: 0 - }).then(() => { - map.dataSources.add(dataSource); - dataSource['@wfsFeatureCollection'] = collection; - layerToGeoStylerStyle(options) - .then((style) => { - getStyle(applyDefaultStyleToVectorLayer({ ...options, style }), 'cesium') - .then((styleFunc) => { - if (styleFunc) { - styleFunc({ - entities: dataSource?.entities?.values, - map, - opacity: options.opacity ?? 1, - features: collection.features - }).then(() => { - map.scene.requestRender(); - }); - } - }); - }); - }); + requestFeatures(options, params, source.token) + .then(({ data: collection }) => { + dataSource.load(collection, { + // ensure default style is not applied + stroke: new Cesium.Color(0, 0, 0, 0), + fill: new Cesium.Color(0, 0, 0, 0), + markerColor: new Cesium.Color(0, 0, 0, 0), + strokeWidth: 0, + markerSize: 0 + }).then(() => { + map.dataSources.add(dataSource); + dataSource['@wfsFeatureCollection'] = collection; + layerToGeoStylerStyle(options) + .then((style) => { + getStyle(applyDefaultStyleToVectorLayer({ ...options, style }), 'cesium') + .then((styleFunc) => { + if (styleFunc) { + styleFunc({ + entities: dataSource?.entities?.values, + map, + opacity: options.opacity ?? 1, + features: collection.features + }).then(() => { + map.scene.requestRender(); + }); + } + }); + }); }); - } + }); + dataSource.show = !!options.visibility; dataSource.queryable = options.queryable === undefined || options.queryable; @@ -86,16 +95,14 @@ const createLayer = (options, map) => { map.dataSources.remove(dataSource); dataSource = undefined; } - }, - setVisible: () => {} + } }; }; Layers.registerType('wfs', { create: createLayer, update: (layer, newOptions, oldOptions, map) => { - if (needsReload(oldOptions, newOptions) - || newOptions.visibility !== oldOptions.visibility) { + if (needsReload(oldOptions, newOptions)) { return createLayer(newOptions, map); } if (layer?.dataSource?.entities?.values