• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 19668982994

25 Nov 2025 12:08PM UTC coverage: 76.663% (-0.1%) from 76.758%
19668982994

Pull #11725

github

web-flow
Merge b651fa3a4 into 15cf36181
Pull Request #11725: Disable tests that randomly fail on jenkins CI

32265 of 50209 branches covered (64.26%)

40156 of 52380 relevant lines covered (76.66%)

37.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.2
/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js
1
/**
2
 * Copyright 2022, 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

9
import Layers from '../../../../utils/cesium/Layers';
10
import * as Cesium from 'cesium';
11
import isEqual from 'lodash/isEqual';
12
import isNumber from 'lodash/isNumber';
13
import isNaN from 'lodash/isNaN';
14
import { getProxyUrl } from "../../../../utils/ProxyUtils";
15
import { getStyleParser } from '../../../../utils/VectorStyleUtils';
16
import tinycolor from 'tinycolor2';
17
import googleOnWhiteLogo from '../img/google_on_white_hdpi.png';
18
import googleOnNonWhiteLogo from '../img/google_on_non_white_hdpi.png';
19
import { createClippingPolygonsFromGeoJSON, applyClippingPolygons } from '../../../../utils/cesium/PrimitivesUtils';
20

21
const cleanStyle = (style, options) => {
1✔
22
    if (style && options?.pointCloudShading?.attenuation) {
×
23
        // remove pointSize if attenuation are applied
24
        const { pointSize, ...others } = style;
×
25
        return others;
×
26
    }
27
    return style;
×
28
};
29

30
function getStyle({ style, pointCloudShading = {} }) {
×
31
    const { format, body } = style || {};
×
32
    if (!format || !body) {
×
33
        return Promise.resolve(null);
×
34
    }
35
    if (format === '3dtiles') {
×
36
        return Promise.resolve(cleanStyle(body, { pointCloudShading }));
×
37
    }
38
    if (format === 'geostyler') {
×
39
        return getStyleParser('3dtiles')
×
40
            .then((parser) => parser.writeStyle(body))
×
41
            .then((parsedStyle) => cleanStyle(parsedStyle, { pointCloudShading }));
×
42
    }
43
    return Promise.all([
×
44
        getStyleParser(format),
45
        getStyleParser('3dtiles')
46
    ])
47
        .then(([parser, threeDTilesParser]) =>
48
            parser
×
49
                .readStyle(body)
50
                .then(parsedStyle => threeDTilesParser.writeStyle(parsedStyle))
×
51
                .then((parsedStyle) => cleanStyle(parsedStyle, { pointCloudShading }))
×
52
        );
53
}
54

55
function updateModelMatrix(tileSet, { heightOffset }) {
56
    if (!isNaN(heightOffset) && isNumber(heightOffset)) {
×
57
        const boundingSphere = tileSet.boundingSphere;
×
58
        const cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center);          // undefined if the cartesian is at the center of the ellipsoid
×
59
        const surface = Cesium.Cartesian3.fromRadians(cartographic?.longitude || 0, cartographic?.latitude || 0, 0.0);
×
60
        const offset = Cesium.Cartesian3.fromRadians(cartographic?.longitude || 0, cartographic?.latitude || 0, heightOffset);
×
61
        const translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());
×
62
        tileSet.modelMatrix =  Cesium.Matrix4.fromTranslation(translation);
×
63
    }
64
}
65

66
function clip3DTiles(tileSet, options, map) {
67
    const polygons = createClippingPolygonsFromGeoJSON(options.clippingPolygon);
×
68
    applyClippingPolygons({
×
69
        target: tileSet,
70
        polygons: polygons,
71
        inverse: !!options.clippingPolygonUnion,
72
        scene: map.scene
73
    });
74
}
75

76
const applyImageryLayers = (tileSet, options, map) => {
1✔
77
    if (!options.enableImageryOverlay || !tileSet || tileSet.isDestroyed()) return;
×
78
    // Collect map layers that should be applied to primitive
79
    const mapLayers = [];
×
80
    for (let i = 0; i < map.imageryLayers.length; i++) {
×
81
        const layer = map.imageryLayers.get(i);
×
82
        if (layer._position > options.position) {
×
83
            mapLayers.push(layer);
×
84
        }
85
    }
86
    // Add layers in the correct order
87
    mapLayers.forEach((layer, idx) => {
×
88
        const current = tileSet.imageryLayers.get(idx);
×
89
        if (current !== layer) {
×
90
            tileSet.imageryLayers.add(layer);
×
91
        }
92
    });
93
    map.scene.requestRender();
×
94
};
95

96
let pendingCallbacks = {};
1✔
97

98
function ensureReady(layer, callback, eventKey) {
99
    const tileSet = layer.getTileSet();
×
100
    if (!tileSet && eventKey) {
×
101
        pendingCallbacks[eventKey] = callback;
×
102
        return;
×
103
    }
104
    if (tileSet) {
×
105
        callback(tileSet);
×
106
    }
107
}
108

109
// Google Photorealistic 3D Tiles requires both attribution and brand logo (see https://cloud.google.com/blog/products/maps-platform/commonly-asked-questions-about-our-recently-launched-photorealistic-3d-tiles)
110
// The attribution are dynamic and updated directly with the `showCreditsOnScreen` property (see https://developers.google.com/maps/documentation/tile/policies#3d_tiles)
111
// The brand logo instead is not managed by the Cesium3DTileset class and must to be included in the credits
112
function updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet) {
113
    if ((options?.url || '').includes('https://tile.googleapis.com')) {
×
114
        if (!tileSet._googleCredit) {
×
115
            const bodyStyle = window?.getComputedStyle ? window.getComputedStyle(document.body, null) : null;
×
116
            const bodyBackgroundColor = bodyStyle?.getPropertyValue ? bodyStyle.getPropertyValue('background-color') : '#ffffff';
×
117
            const src = tinycolor(bodyBackgroundColor).isDark()
×
118
                ? googleOnNonWhiteLogo
119
                : googleOnWhiteLogo;
120
            tileSet._googleCredit = new Cesium.Credit(`<img src="${src}" title="Google" style="padding:0 0.5rem"/>`, true);
×
121
            return map.creditDisplay.addStaticCredit(tileSet._googleCredit);
×
122
        }
123
        return map.creditDisplay.removeStaticCredit(tileSet._googleCredit);
×
124
    }
125
    return null;
×
126
}
127

128
function updateShading(tileSet, options, map) {
129
    // point cloud
130
    tileSet.pointCloudShading.attenuation = !!options?.pointCloudShading?.attenuation;
×
131
    tileSet.pointCloudShading.maximumAttenuation = options?.pointCloudShading?.maximumAttenuation ?? 4;
×
132
    tileSet.pointCloudShading.eyeDomeLighting = !!options?.pointCloudShading?.eyeDomeLighting;
×
133
    tileSet.pointCloudShading.eyeDomeLightingStrength = options?.pointCloudShading?.eyeDomeLightingStrength ?? 1.0;
×
134
    tileSet.pointCloudShading.eyeDomeLightingRadius = options?.pointCloudShading?.eyeDomeLightingRadius ?? 1.0;
×
135
    setTimeout(() => map.scene.requestRender());
×
136
}
137

138
const createLayer = (options, map) => {
1✔
139
    if (!options.visibility) {
5✔
140
        return {
1✔
141
            detached: true,
142
            getResource: () => undefined,
×
143
            getTileSet: () => undefined,
1✔
144
            add: () => {},
145
            remove: () => {}
146
        };
147
    }
148
    let tileSet;
149
    let resource;
150
    let promise;
151
    const removeTileset = () => {
4✔
152
        updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet);
×
153
        if (tileSet?.imageryLayers) {
×
154
            tileSet.imageryLayers.removeAll(false);
×
155
        }
156
        map.scene.primitives.remove(tileSet);
×
157
        tileSet = undefined;
×
158
    };
159
    const layer = {
4✔
160
        getTileSet: () => tileSet,
×
161
        getResource: () => resource
16✔
162
    };
163

164
    let timeout = undefined;
4✔
165

166
    return {
4✔
167
        detached: true,
168
        ...layer,
169
        add: () => {
170
            // delay creation of tileset when frequents recreation are requested
171
            timeout = setTimeout(() => {
2✔
172
                timeout = undefined;
2✔
173
                resource = new Cesium.Resource({
2✔
174
                    url: options.url,
175
                    proxy: options.forceProxy ? new Cesium.DefaultProxy(getProxyUrl()) : undefined
2✔
176
                    // TODO: axios supports also adding access tokens or credentials (e.g. authkey, Authentication header ...).
177
                    // if we want to use internal cesium functionality to retrieve data
178
                    // we need to create a utility to set a CesiumResource that applies also this part.
179
                    // in addition to this proxy.
180
                });
181
                promise = Cesium.Cesium3DTileset.fromUrl(resource,
2✔
182
                    {
183
                        showCreditsOnScreen: true
184
                    }
185
                ).then((_tileSet) => {
186
                    tileSet = _tileSet;
×
187
                    updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet);
×
188
                    map.scene.primitives.add(tileSet);
×
189
                    // assign the original mapstore id of the layer
190
                    tileSet.msId = options.id;
×
191
                    ensureReady(layer, () => {
×
192
                        updateModelMatrix(tileSet, options);
×
193
                        clip3DTiles(tileSet, options, map);
×
194
                        updateShading(tileSet, options, map);
×
195
                        getStyle(options)
×
196
                            .then((style) => {
197
                                if (style) {
×
198
                                    tileSet.style = new Cesium.Cesium3DTileStyle(style);
×
199
                                }
200
                                Object.keys(pendingCallbacks).forEach((eventKey) => {
×
201
                                    pendingCallbacks[eventKey](tileSet);
×
202
                                });
203
                                pendingCallbacks = {};
×
204
                                applyImageryLayers(tileSet, options, map);
×
205
                            });
206
                    });
207
                });
208
            }, 50);
209
        },
210
        remove: () => {
211
            if (timeout) {
4!
212
                clearTimeout(timeout);
×
213
                timeout = undefined;
×
214
            }
215
            if (tileSet) {
4!
216
                removeTileset();
×
217
                return;
×
218
            }
219
            if (promise) {
4✔
220
                promise.then(() => {
2✔
221
                    removeTileset();
×
222
                });
223
            }
224
            return;
4✔
225
        }
226
    };
227
};
228

229
Layers.registerType('3dtiles', {
1✔
230
    create: createLayer,
231
    update: function(layer, newOptions, oldOptions, map) {
232
        if (newOptions.forceProxy !== oldOptions.forceProxy
2!
233
            // recreate the tileset when the imagery has been updated and the layer has enableImageryOverlay set to true
234
            || newOptions.enableImageryOverlay && (newOptions.imageryLayersTreeUpdatedCount !== oldOptions.imageryLayersTreeUpdatedCount)
235
            || (newOptions.enableImageryOverlay !== oldOptions.enableImageryOverlay)
236
        ) {
237
            return createLayer(newOptions, map);
2✔
238
        }
239
        if (
×
240
            (!isEqual(newOptions.clippingPolygon, oldOptions.clippingPolygon)
×
241
            || newOptions.clippingPolygonUnion !== oldOptions.clippingPolygonUnion
242
            || newOptions.clipOriginalGeometry !== oldOptions.clipOriginalGeometry)
243
        ) {
244
            ensureReady(layer, (tileSet) => {
×
245
                clip3DTiles(tileSet, newOptions, map);
×
246
            }, 'clip');
247
        }
248
        if ((
×
249
            !isEqual(newOptions.style, oldOptions.style)
×
250
            || newOptions?.pointCloudShading?.attenuation !== oldOptions?.pointCloudShading?.attenuation
251
        )) {
252
            ensureReady(layer, (tileSet) => {
×
253
                getStyle(newOptions)
×
254
                    .then((style) => {
255
                        if (style && tileSet) {
×
256
                            tileSet.makeStyleDirty();
×
257
                            tileSet.style = new Cesium.Cesium3DTileStyle(style);
×
258
                        }
259
                    });
260
            }, 'style');
261
        }
262
        if (!isEqual(newOptions.pointCloudShading, oldOptions.pointCloudShading)) {
×
263
            ensureReady(layer, (tileSet) => {
×
264
                updateShading(tileSet, newOptions, map);
×
265
            }, 'shading');
266
        }
267
        if (newOptions.heightOffset !== oldOptions.heightOffset) {
×
268
            ensureReady(layer, (tileSet) => {
×
269
                updateModelMatrix(tileSet, newOptions);
×
270
            }, 'matrix');
271
        }
272
        return null;
×
273
    }
274
});
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc