Skip to content

Commit 41dd38f

Browse files
geosolutions-it#11995: Global setting for "Enable imagery layers overlay" 3D Tile (geosolutions-it#12001)
- handle adding new mapOption for 3d map in localConfig called 'enableImageryLayersOverlay' -> default is true - handle settings its value to 3d tile layer in adding to map - add jsdoc for Map plugin for the new prop mapOptions.cesium.enableImageryOverlay - add unit tests - add translations
1 parent 4e88ba7 commit 41dd38f

16 files changed

Lines changed: 268 additions & 15 deletions

File tree

web/client/api/catalog/ThreeDTiles.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,22 @@ function validateUrl(serviceUrl) {
2222
return false;
2323
}
2424

25-
const recordToLayer = (record) => {
25+
const recordToLayer = (record, options = {}) => {
2626
if (!record) {
2727
return null;
2828
}
2929
const { bbox, format, properties } = record;
30+
// extract 'enableImageryOverlay' of mapOptions
31+
const {enableImageryOverlay} = options;
3032
return {
3133
type: '3dtiles',
3234
url: record.url,
3335
title: record.title,
3436
visibility: true,
3537
...(bbox && { bbox }),
3638
...(format && { format }),
37-
...(properties && { properties })
39+
...(properties && { properties }),
40+
...(enableImageryOverlay && {enableImageryOverlay})
3841
};
3942
};
4043

web/client/api/catalog/__tests__/ThreeDTiles-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,47 @@ describe('Test 3D tiles catalog API', () => {
189189
}
190190
});
191191
});
192+
it('should extract the layer config from a catalog record with enableImageryOverlay option', () => {
193+
const catalogRecord = {
194+
serviceType: '3dtiles',
195+
isValid: true,
196+
description: 'v. 1.0',
197+
title: 'Title',
198+
identifier: 'http://service.org/tileset.json',
199+
url: 'http://service.org/tileset.json',
200+
thumbnail: null,
201+
bbox: {
202+
crs: 'EPSG:4326',
203+
bounds: {
204+
minx: -180,
205+
miny: -90,
206+
maxx: 180,
207+
maxy: 90
208+
}
209+
},
210+
references: []
211+
};
212+
const options = {
213+
enableImageryOverlay: true
214+
};
215+
const layer = getLayerFromRecord(catalogRecord, options);
216+
expect(layer).toEqual({
217+
type: '3dtiles',
218+
url: 'http://service.org/tileset.json',
219+
title: 'Title',
220+
visibility: true,
221+
enableImageryOverlay: true,
222+
bbox: {
223+
crs: 'EPSG:4326',
224+
bounds: {
225+
minx: -180,
226+
miny: -90,
227+
maxx: 180,
228+
maxy: 90
229+
}
230+
}
231+
});
232+
});
192233
it('should validate if the service url ends with .json', (done) => {
193234
const service = { title: '3D Tile Service', url: 'http://service.org/tileset.json' };
194235
validate(service)

web/client/components/catalog/Catalog.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ class Catalog extends React.Component {
8484
setNewServiceStatus: PropTypes.func,
8585
onShowSecurityModal: PropTypes.func,
8686
onSetProtectedServices: PropTypes.func,
87-
canEdit: PropTypes.func
87+
canEdit: PropTypes.func,
88+
globalEnableImageryOverlay: PropTypes.bool // mapOptions prop for 3D cesium map
8889
};
8990

9091
static contextTypes = {
@@ -282,6 +283,7 @@ class Catalog extends React.Component {
282283
onAdd={() => {
283284
this.search({ services: this.props.services, selectedService: this.props.selectedService });
284285
}}
286+
enableImageryOverlay={this.props.globalEnableImageryOverlay}
285287
/>
286288
</div>);
287289
};

web/client/components/catalog/RecordGrid.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ class RecordGrid extends React.Component {
4040
showTemplate: PropTypes.bool,
4141
service: PropTypes.object,
4242
defaultFormat: PropTypes.string,
43-
layerBaseConfig: PropTypes.object
43+
layerBaseConfig: PropTypes.object,
44+
enableImageryOverlay: PropTypes.bool // mapOptions prop for 3D cesium map
4445
};
4546

4647
static defaultProps = {
@@ -87,6 +88,7 @@ class RecordGrid extends React.Component {
8788
currentLocale={this.props.currentLocale}
8889
defaultFormat={this.props.defaultFormat}
8990
layerBaseConfig={this.props.layerBaseConfig}
91+
enableImageryOverlay={this.props.enableImageryOverlay}
9092
/>
9193
</Col>
9294
);

web/client/components/catalog/RecordItem.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ class RecordItem extends React.Component {
6161
service: PropTypes.object,
6262
selectedService: PropTypes.string,
6363
showTemplate: PropTypes.bool,
64-
defaultFormat: PropTypes.string
64+
defaultFormat: PropTypes.string,
65+
enableImageryOverlay: PropTypes.bool // mapOptions prop for 3D cesium map
6566
};
6667

6768
static defaultProps = {
@@ -159,7 +160,8 @@ class RecordItem extends React.Component {
159160
map: {
160161
projection: this.props.crs,
161162
resolutions: getResolutions()
162-
}
163+
},
164+
enableImageryOverlay: this.props.enableImageryOverlay
163165
}, true)
164166
.then((layer) => {
165167
if (layer) {

web/client/plugins/Map.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ import Spinner from '../components/layout/Spinner';
162162
* @prop {boolean} mapOptions.cesium.depthTestAgainstTerrain if true all primitive 3d features will be tested against the terrain while if false they will be drawn on top of the terrain even if hidden by it (default true)
163163
* @prop {number} mapOptions.cesium.maximumZoomDistance max zoom limit (in meter unit) to restrict the zoom out operation based on it
164164
* @prop {number} mapOptions.cesium.minimumZoomDistance min zoom limit (in meter unit) to restrict the zoom in operation based on it
165+
* @prop {boolean} mapOptions.cesium.enableImageryOverlay when true, enables draping of 2D imagery layers (WMS, TMS, WMTS) over 3D Tiles with sequential rendering in TOC order; this global setting is automatically applied to each 3D Tiles layer added to the map (default true)
165166
* @static
166167
* @example
167168
* // Adding a layer to be used as a source for the elevation (shown in the MousePosition plugin configured with showElevation = true)

web/client/plugins/MetadataExplorer.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ import { layersSelector } from '../selectors/layers';
8181
import { currentLocaleSelector, currentMessagesSelector } from '../selectors/locale';
8282
import {burgerMenuSelector} from "../selectors/controls";
8383
import { isLocalizedLayerStylesEnabledSelector } from '../selectors/localizedLayerStyles';
84-
import { projectionSelector } from '../selectors/map';
84+
import { mapEnableImageryOverlaySelector, projectionSelector } from '../selectors/map';
8585
import { mapLayoutValuesSelector } from '../selectors/maplayout';
8686
import ResponsivePanel from "../components/misc/panels/ResponsivePanel";
8787
import { DEFAULT_PANEL_WIDTH } from '../utils/LayoutUtils';
@@ -126,7 +126,8 @@ const metadataExplorerSelector = createStructuredSelector({
126126
formatOptions: getSupportedFormatsSelector,
127127
infoFormatOptions: getSupportedGFIFormatsSelector,
128128
isNewServiceAdded: getNewServiceStatusSelector,
129-
canEdit: canEditServiceSelector
129+
canEdit: canEditServiceSelector,
130+
globalEnableImageryOverlay: mapEnableImageryOverlaySelector
130131
});
131132

132133

@@ -176,6 +177,7 @@ class MetadataExplorerComponent extends React.Component {
176177
dockProps: PropTypes.object,
177178
zoomToLayer: PropTypes.bool,
178179
isLocalizedLayerStylesEnabled: PropTypes.bool,
180+
globalEnableImageryOverlay: PropTypes.bool,
179181

180182
// side panel properties
181183
width: PropTypes.number,
@@ -216,7 +218,8 @@ class MetadataExplorerComponent extends React.Component {
216218
group: null,
217219
services: {},
218220
servicesWithBackgrounds: {},
219-
editingAllowedRoles: ["ALL"]
221+
editingAllowedRoles: ["ALL"],
222+
globalEnableImageryOverlay: true
220223
};
221224

222225
componentDidMount() {
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright 2026, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import expect from 'expect';
9+
import React from 'react';
10+
import { Provider } from 'react-redux';
11+
import ReactDOM from 'react-dom';
12+
import MapSettings from '../mapsettings/MapSettings';
13+
import configureMockStore from 'redux-mock-store';
14+
15+
const mockStore = configureMockStore();
16+
17+
// Helper: render MapSettings inside a Provider with a Cesium store
18+
const renderCesium = (mapOptions = {}, extraProps = {}) => {
19+
const store = mockStore({
20+
map: { present: { mapOptions } },
21+
maptype: { mapType: 'cesium' }
22+
});
23+
ReactDOM.render(
24+
<Provider store={store}>
25+
<MapSettings {...extraProps} />
26+
</Provider>,
27+
document.getElementById('container')
28+
);
29+
};
30+
31+
// Helper: render MapSettings inside a Provider with an OpenLayers store
32+
const renderOL = (mapOptions = {}) => {
33+
const store = mockStore({
34+
map: { present: { mapOptions } },
35+
maptype: { mapType: 'openlayers' }
36+
});
37+
ReactDOM.render(
38+
<Provider store={store}><MapSettings /></Provider>,
39+
document.getElementById('container')
40+
);
41+
};
42+
43+
describe('MapSettings component', () => {
44+
beforeEach((done) => {
45+
document.body.innerHTML = '<div id="container"></div>';
46+
setTimeout(done);
47+
});
48+
49+
afterEach((done) => {
50+
ReactDOM.unmountComponentAtNode(document.getElementById('container'));
51+
document.body.innerHTML = '';
52+
setTimeout(done);
53+
});
54+
55+
it('renders nothing when isCesium is false (OL map)', () => {
56+
renderOL();
57+
const container = document.getElementById('container');
58+
expect(container.innerHTML).toBeFalsy();
59+
const form = document.querySelector('form');
60+
expect(form).toBeFalsy();
61+
});
62+
63+
it('renders a form when isCesium is true', () => {
64+
renderCesium();
65+
const form = document.querySelector('form');
66+
expect(form).toBeTruthy();
67+
});
68+
69+
it('renders all 6 Cesium checkboxes', () => {
70+
renderCesium();
71+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
72+
expect(checkboxes.length).toBe(6);
73+
});
74+
75+
it('renders the lighting select dropdown', () => {
76+
renderCesium();
77+
const selects = document.querySelectorAll('.Select');
78+
expect(selects.length).toBeGreaterThan(0);
79+
});
80+
81+
it('showSkyAtmosphere checkbox reflects mapOptions value when true', () => {
82+
renderCesium({ showSkyAtmosphere: true });
83+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
84+
expect(checkboxes[0].checked).toBe(true);
85+
});
86+
87+
it('showSkyAtmosphere checkbox reflects mapOptions value when false', () => {
88+
renderCesium({ showSkyAtmosphere: false });
89+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
90+
expect(checkboxes[0].checked).toBe(false);
91+
});
92+
93+
it('showGroundAtmosphere checkbox reflects mapOptions value', () => {
94+
renderCesium({ showGroundAtmosphere: true });
95+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
96+
expect(checkboxes[1].checked).toBe(true);
97+
});
98+
99+
it('enableFog checkbox reflects mapOptions value', () => {
100+
renderCesium({ enableFog: true });
101+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
102+
expect(checkboxes[2].checked).toBe(true);
103+
});
104+
105+
it('depthTestAgainstTerrain checkbox reflects mapOptions value', () => {
106+
renderCesium({ depthTestAgainstTerrain: true });
107+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
108+
expect(checkboxes[3].checked).toBe(true);
109+
});
110+
111+
it('enableCollisionDetection defaults to true when not set in mapOptions', () => {
112+
renderCesium();
113+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
114+
expect(checkboxes[4].checked).toBe(true);
115+
});
116+
117+
it('enableCollisionDetection reflects mapOptions value when explicitly set to false', () => {
118+
renderCesium({ enableCollisionDetection: false });
119+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
120+
expect(checkboxes[4].checked).toBe(false);
121+
});
122+
123+
it('enableImageryLayersOverlay defaults to true when not set in mapOptions', () => {
124+
renderCesium();
125+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
126+
expect(checkboxes[5].checked).toBe(true);
127+
});
128+
129+
it('enableImageryLayersOverlay reflects mapOptions value when explicitly set to false', () => {
130+
renderCesium({ enableImageryLayersOverlay: false });
131+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
132+
expect(checkboxes[5].checked).toBe(false);
133+
});
134+
135+
it('does not render DateTime picker when lighting is sunlight', () => {
136+
renderCesium({ lighting: { value: 'sunlight' } });
137+
const datePicker = document.querySelector('.lighting-dateTime-picker');
138+
expect(datePicker).toBeFalsy();
139+
});
140+
141+
it('does not render DateTime picker when lighting is flashlight', () => {
142+
renderCesium({ lighting: { value: 'flashlight' } });
143+
const datePicker = document.querySelector('.lighting-dateTime-picker');
144+
expect(datePicker).toBeFalsy();
145+
});
146+
147+
it('renders DateTime picker when lighting is set to dateTime', () => {
148+
renderCesium({ lighting: { value: 'dateTime', dateTime: new Date().toISOString() } });
149+
const datePicker = document.querySelector('.lighting-dateTime-picker');
150+
expect(datePicker).toBeTruthy();
151+
});
152+
153+
it('uses defaultMapOptions prop when map.mapOptions is empty', () => {
154+
renderCesium({}, { mapOptions: { cesium: { showSkyAtmosphere: true, enableFog: true } } });
155+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
156+
expect(checkboxes[0].checked).toBe(true);
157+
expect(checkboxes[2].checked).toBe(true);
158+
});
159+
160+
it('map.mapOptions overrides defaultMapOptions prop', () => {
161+
renderCesium(
162+
{ showSkyAtmosphere: false },
163+
{ mapOptions: { cesium: { showSkyAtmosphere: true } } }
164+
);
165+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
166+
expect(checkboxes[0].checked).toBe(false);
167+
});
168+
});

web/client/plugins/map/mapsettings/MapSettings.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ const Component = ({
9696
>
9797
<Message msgId="map.settings.collisionDetection" />
9898
</Checkbox>
99+
<Checkbox
100+
checked={mapOptions.enableImageryLayersOverlay !== undefined ? mapOptions.enableImageryLayersOverlay : true}
101+
onChange={() => handleConfigUpdate(mapOptions, 'enableImageryLayersOverlay')}
102+
>
103+
<Message msgId="map.settings.imageryLayersOverlay" />
104+
</Checkbox>
99105
<FormGroup>
100106
<ControlLabel><Message msgId="map.settings.lightings.title"/></ControlLabel>
101107
<SelectLocalized

web/client/selectors/__tests__/map-test.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import {
2727
isMouseMoveIdentifyActiveSelector,
2828
identifyFloatingToolSelector,
2929
mapInfoAttributesSelector,
30-
showEditableFeatureCheckboxSelector
30+
showEditableFeatureCheckboxSelector,
31+
mapOptionsSelector,
32+
mapEnableImageryOverlaySelector
3133
} from '../map';
3234

3335
const center = {x: 1, y: 1};
@@ -235,4 +237,16 @@ describe('Test map selectors', () => {
235237
expect(showEditableFeatureCheckboxSelector(_state)).toBeTruthy();
236238
});
237239
});
240+
it('test mapOptionsSelector', () => {
241+
const mapOptions = mapOptionsSelector({map: {present: {visualizationMode: "3D", mapOptions: {enableImageryLayersOverlay: true, showSkyAtmosphere: false}}}});
242+
expect(mapOptions).toBeTruthy();
243+
expect(mapOptions).toEqual({
244+
enableImageryLayersOverlay: true, showSkyAtmosphere: false
245+
});
246+
});
247+
it('test mapEnableImageryOverlaySelector', () => {
248+
const enableImageryOverlayOp = mapEnableImageryOverlaySelector({map: {present: {visualizationMode: "3D", mapOptions: {enableImageryLayersOverlay: true, showSkyAtmosphere: false}}}});
249+
expect(enableImageryOverlayOp).toBeTruthy();
250+
expect(enableImageryOverlayOp).toEqual(true);
251+
});
238252
});

0 commit comments

Comments
 (0)