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

iTowns / itowns / 15277986037

27 May 2025 02:26PM UTC coverage: 87.096% (-0.02%) from 87.111%
15277986037

Pull #2477

github

web-flow
Merge e3c1753b8 into ed0980450
Pull Request #2477: refactor(LayeredMaterial): migrate to TypeScript

2805 of 3747 branches covered (74.86%)

Branch coverage included in aggregate %.

515 of 587 new or added lines in 15 files covered. (87.73%)

11 existing lines in 2 files now uncovered.

26002 of 29328 relevant lines covered (88.66%)

1104.9 hits per line

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

78.79
/packages/Main/src/Layer/TiledGeometryLayer.js
1
import * as THREE from 'three';
1✔
2
import GeometryLayer from 'Layer/GeometryLayer';
1✔
3
import { InfoTiledGeometryLayer } from 'Layer/InfoLayer';
1✔
4
import Picking from 'Core/Picking';
1✔
5
import convertToTile from 'Converter/convertToTile';
1✔
6
import ObjectRemovalHelper from 'Process/ObjectRemovalHelper';
1✔
7
import { ImageryLayers } from 'Layer/Layer';
1✔
8
import { CACHE_POLICIES } from 'Core/Scheduler/Cache';
1✔
9

1✔
10
const subdivisionVector = new THREE.Vector3();
1✔
11
const boundingSphereCenter = new THREE.Vector3();
1✔
12

1✔
13
/**
1✔
14
 * @property {InfoTiledGeometryLayer} info - Status information of layer
1✔
15
 * @property {boolean} isTiledGeometryLayer - Used to checkout whether this
1✔
16
 * layer is a TiledGeometryLayer. Default is true. You should not change this,
1✔
17
 * as it is used internally for optimisation.
1✔
18
 * @property {boolean} hideSkirt (default false) - Used to hide the skirt (tile borders).
1✔
19
 * Useful when the layer opacity < 1
1✔
20
 *
1✔
21
 * @extends GeometryLayer
1✔
22
 */
1✔
23
class TiledGeometryLayer extends GeometryLayer {
1✔
24
    /**
1✔
25
     * A layer extending the {@link GeometryLayer}, but with a tiling notion.
1✔
26
     *
1✔
27
     * `TiledGeometryLayer` is the ground where `ColorLayer` and `ElevationLayer` are attached.
1✔
28
     * `TiledGeometryLayer` is a quadtree data structure. At zoom 0,
1✔
29
     * there is a single tile for the whole earth. At zoom level 1,
1✔
30
     * the single tile splits into 4 tiles (2x2 tile square).
1✔
31
     * Each zoom level quadtree divides the geometry tiles of the one before it.
1✔
32
     * The camera distance determines how the tiles are subdivided for optimal data display.
1✔
33
     *
1✔
34
     * Some `GeometryLayer` can also be attached to the `TiledGeometryLayer` if they want to take advantage of the quadtree.
1✔
35
     *
1✔
36
     * ![tiled geometry](/docs/static/images/tiledGeometry.jpeg)
1✔
37
     * _In `GlobeView`, **red lines** represents the **WGS84 grid** and **orange lines** the **Pseudo-mercator grid**._
1✔
38
     * _In this picture, there are tiles with 3 different zoom/levels._
1✔
39
     *
1✔
40
     * The zoom/level is based on [tiled web map](https://en.wikipedia.org/wiki/Tiled_web_map).
1✔
41
     * It corresponds at meters by pixel. If the projection tile exceeds a certain pixel size (on screen)
1✔
42
     * then it is subdivided into 4 tiles with a zoom greater than 1.
1✔
43
     *
1✔
44
     * @param {string} id - The id of the layer, that should be unique. It is
1✔
45
     * not mandatory, but an error will be emitted if this layer is added a
1✔
46
     * {@link View} that already has a layer going by that id.
1✔
47
     * @param {THREE.Object3D} object3d - The object3d used to contain the
1✔
48
     * geometry of the TiledGeometryLayer. It is usually a `THREE.Group`, but it
1✔
49
     * can be anything inheriting from a `THREE.Object3d`.
1✔
50
     * @param {Array} schemeTile - extents Array of root tiles
1✔
51
     * @param {Object} builder - builder geometry object
1✔
52
     * @param {Object} [config] - Optional configuration, all elements in it
1✔
53
     * will be merged as is in the layer. For example, if the configuration
1✔
54
     * contains three elements `name, protocol, extent`, these elements will be
1✔
55
     * available using `layer.name` or something else depending on the property
1✔
56
     * name.
1✔
57
     * @param {Source} [config.source] - Description and options of the source.
1✔
58
     *
1✔
59
     * @throws {Error} `object3d` must be a valid `THREE.Object3d`.
1✔
60
     */
1✔
61
    constructor(id, object3d, schemeTile, builder, config) {
1✔
62
        const {
31✔
63
            sseSubdivisionThreshold = 1.0,
31✔
64
            minSubdivisionLevel,
31✔
65
            maxSubdivisionLevel,
31✔
66
            maxDeltaElevationLevel,
31✔
67
            tileMatrixSets,
31✔
68
            diffuse,
31✔
69
            showOutline = false,
31✔
70
            segments,
31✔
71
            disableSkirt = false,
31✔
72
            materialOptions,
31✔
73
            ...configGeometryLayer
31✔
74
        } = config;
31✔
75

31✔
76
        super(id, object3d, {
31✔
77
            ...configGeometryLayer,
31✔
78
            // cacheLifeTime = CACHE_POLICIES.INFINITE because the cache is handled by the builder
31✔
79
            cacheLifeTime: CACHE_POLICIES.INFINITE,
31✔
80
            source: false,
31✔
81
        });
31✔
82

31✔
83
        /**
31✔
84
         * @type {boolean}
31✔
85
         * @readonly
31✔
86
         */
31✔
87
        this.isTiledGeometryLayer = true;
31✔
88

31✔
89
        this.protocol = 'tile';
31✔
90

31✔
91
        // TODO : this should be add in a preprocess method specific to GeoidLayer.
31✔
92
        this.object3d.geoidHeight = 0;
31✔
93

31✔
94
        /**
31✔
95
         * @type {boolean}
31✔
96
         */
31✔
97
        this.disableSkirt = disableSkirt;
31✔
98

31✔
99
        this._hideSkirt = !!config.hideSkirt;
31✔
100

31✔
101
        /**
31✔
102
         * @type {number}
31✔
103
         */
31✔
104
        this.sseSubdivisionThreshold = sseSubdivisionThreshold;
31✔
105

31✔
106
        /**
31✔
107
         * @type {number}
31✔
108
         */
31✔
109
        this.minSubdivisionLevel = minSubdivisionLevel;
31✔
110

31✔
111
        /**
31✔
112
         * @type {number}
31✔
113
         */
31✔
114
        this.maxSubdivisionLevel = maxSubdivisionLevel;
31✔
115

31✔
116
        /**
31✔
117
         * @type {number}
31✔
118
         * @deprecated
31✔
119
         */
31✔
120
        this.maxDeltaElevationLevel = maxDeltaElevationLevel;
31✔
121

31✔
122
        this.segments = segments;
31✔
123
        this.schemeTile = schemeTile;
31✔
124
        this.builder = builder;
31✔
125
        this.info = new InfoTiledGeometryLayer(this);
31✔
126

31✔
127
        if (!this.schemeTile) {
31!
128
            throw new Error(`Cannot init tiled layer without schemeTile for layer ${this.id}`);
×
129
        }
×
130

31✔
131
        if (!this.builder) {
31!
132
            throw new Error(`Cannot init tiled layer without builder for layer ${this.id}`);
×
133
        }
×
134

31✔
135
        this.maxScreenSizeNode = this.sseSubdivisionThreshold * (this.sizeDiagonalTexture * 2);
31✔
136

31✔
137
        this.tileMatrixSets = tileMatrixSets;
31✔
138

31✔
139
        this.materialOptions = materialOptions;
31✔
140

31✔
141
        /*
31✔
142
         * @type {boolean}
31✔
143
         */
31✔
144
        this.showOutline = showOutline;
31✔
145

31✔
146
        /**
31✔
147
         * @type {THREE.Vector3 | undefined}
31✔
148
         */
31✔
149
        this.diffuse = diffuse;
31✔
150

31✔
151
        this.level0Nodes = [];
31✔
152
        const promises = [];
31✔
153

31✔
154
        for (const root of this.schemeTile) {
31✔
155
            promises.push(this.convert(undefined, root));
53✔
156
        }
53✔
157

31✔
158
        this._promises.push(Promise.all(promises).then((level0s) => {
31✔
159
            this.level0Nodes = level0s;
31✔
160
            this.object3d.add(...level0s);
31✔
161
            this.object3d.updateMatrixWorld();
31✔
162
        }));
31✔
163
    }
31✔
164

1✔
165
    get hideSkirt() {
1✔
166
        return this._hideSkirt;
65✔
167
    }
65✔
168

1✔
169
    set hideSkirt(value) {
1✔
170
        if (!this.level0Nodes) {
×
171
            return;
×
172
        }
×
173
        this._hideSkirt = value;
×
174
        for (const node of this.level0Nodes) {
×
175
            node.traverse((obj) => {
×
176
                if (obj.isTileMesh) {
×
177
                    obj.geometry.hideSkirt = value;
×
178
                }
×
179
            });
×
180
        }
×
181
    }
×
182
    /**
1✔
183
     * Picking method for this layer. It uses the {@link Picking#pickTilesAt}
1✔
184
     * method.
1✔
185
     *
1✔
186
     * @param {View} view - The view instance.
1✔
187
     * @param {Object} coordinates - The coordinates to pick in the view. It
1✔
188
     * should have at least `x` and `y` properties.
1✔
189
     * @param {number} radius - Radius of the picking circle.
1✔
190
     * @param {Array} target - Array to push picking result.
1✔
191
     *
1✔
192
     * @return {Array} An array containing all targets picked under the
1✔
193
     * specified coordinates.
1✔
194
     */
1✔
195
    pickObjectsAt(view, coordinates, radius = this.options.defaultPickingRadius, target = []) {
1!
196
        return Picking.pickTilesAt(view, coordinates, radius, this, target);
1✔
197
    }
1✔
198

1✔
199
    /**
1✔
200
     * Does pre-update work on the context:
1✔
201
     * <ul>
1✔
202
     *  <li>update the `colorLayers` and `elevationLayers`</li>
1✔
203
     *  <li>update the `maxElevationLevel`</li>
1✔
204
     * </ul>
1✔
205
     *
1✔
206
     * Once this work is done, it returns a list of nodes to update. Depending
1✔
207
     * on the origin of `sources`, it can return a few things:
1✔
208
     * <ul>
1✔
209
     *  <li>if `sources` is empty, it returns the first node of the layer
1✔
210
     *  (stored as `level0Nodes`), which will trigger the update of the whole
1✔
211
     *  tree</li>
1✔
212
     *  <li>if the update is triggered by a camera move, the whole tree is
1✔
213
     *  returned too</li>
1✔
214
     *  <li>if `source.layer` is this layer, it means that `source` is a node; a
1✔
215
     *  common ancestor will be found if there are multiple sources, with the
1✔
216
     *  default common ancestor being the first source itself</li>
1✔
217
     *  <li>else it returns the whole tree</li>
1✔
218
     * </ul>
1✔
219
     *
1✔
220
     * @param {Object} context - The context of the update; see the {@link
1✔
221
     * MainLoop} for more informations.
1✔
222
     * @param {Set<GeometryLayer|TileMesh>} sources - A list of sources to
1✔
223
     * generate a list of nodes to update.
1✔
224
     *
1✔
225
     * @return {TileMesh[]} The array of nodes to update.
1✔
226
     */
1✔
227
    preUpdate(context, sources) {
1✔
228
        if (sources.has(undefined) || sources.size == 0) {
2!
229
            return this.level0Nodes;
×
230
        }
×
231

2✔
232
        if (__DEBUG__) {
2✔
233
            this._latestUpdateStartingLevel = 0;
2✔
234
        }
2✔
235

2✔
236
        context.colorLayers = context.view.getLayers(
2✔
237
            (l, a) => a && a.id == this.id && l.isColorLayer);
2!
238
        context.elevationLayers = context.view.getLayers(
2✔
239
            (l, a) => a && a.id == this.id && l.isElevationLayer);
2!
240

2✔
241
        context.maxElevationLevel = -1;
2✔
242
        for (const e of context.elevationLayers) {
2!
243
            context.maxElevationLevel = Math.max(e.source.zoom.max, context.maxElevationLevel);
×
244
        }
×
245
        if (context.maxElevationLevel == -1) {
2✔
246
            context.maxElevationLevel = Infinity;
2✔
247
        }
2✔
248

2✔
249
        // Prepare ColorLayer sequence order
2✔
250
        // In this moment, there is only one color layers sequence, because they are attached to tileLayer.
2✔
251
        // In future, the sequence must be returned by parent geometry layer.
2✔
252
        this.colorLayersOrder = ImageryLayers.getColorLayersIdOrderedBySequence(context.colorLayers);
2✔
253

2✔
254
        let commonAncestor;
2✔
255
        for (const source of sources.values()) {
2✔
256
            if (source.isCamera) {
2!
257
                // if the change is caused by a camera move, no need to bother
×
258
                // to find common ancestor: we need to update the whole tree:
×
259
                // some invisible tiles may now be visible
×
260
                return this.level0Nodes;
×
261
            }
×
262
            if (source.layer === this) {
2!
263
                if (!commonAncestor) {
×
264
                    commonAncestor = source;
×
265
                } else {
×
266
                    commonAncestor = source.findCommonAncestor(commonAncestor);
×
267
                    if (!commonAncestor) {
×
268
                        return this.level0Nodes;
×
269
                    }
×
270
                }
×
271
                if (commonAncestor.material == null) {
×
272
                    commonAncestor = undefined;
×
273
                }
×
274
            }
×
275
        }
2✔
276
        if (commonAncestor) {
2!
277
            if (__DEBUG__) {
×
278
                this._latestUpdateStartingLevel = commonAncestor.level;
×
279
            }
×
280
            return [commonAncestor];
×
281
        } else {
2✔
282
            return this.level0Nodes;
2✔
283
        }
2✔
284
    }
2✔
285

1✔
286
    /**
1✔
287
     * Update a node of this layer. The node will not be updated if:
1✔
288
     * <ul>
1✔
289
     *  <li>it does not have a parent, then it is removed</li>
1✔
290
     *  <li>its parent is being subdivided</li>
1✔
291
     *  <li>is not visible in the camera</li>
1✔
292
     * </ul>
1✔
293
     *
1✔
294
     * @param {Object} context - The context of the update; see the {@link
1✔
295
     * MainLoop} for more informations.
1✔
296
     * @param {Layer} layer - Parameter to be removed once all update methods
1✔
297
     * have been aligned.
1✔
298
     * @param {TileMesh} node - The node to update.
1✔
299
     *
1✔
300
     * @returns {Object}
1✔
301
     */
1✔
302
    update(context, layer, node) {
1✔
303
        if (!node.parent) {
5✔
304
            return ObjectRemovalHelper.removeChildrenAndCleanup(this, node);
4✔
305
        }
4✔
306
        // early exit if parent' subdivision is in progress
1✔
307
        if (node.parent.pendingSubdivision) {
5!
308
            node.visible = false;
×
309
            node.material.visible = false;
×
310
            this.info.update(node);
×
311
            return undefined;
×
312
        }
✔
313

1✔
314
        // do proper culling
1✔
315
        node.visible = !this.culling(node, context.camera);
1✔
316

1✔
317
        if (node.visible) {
1✔
318
            let requestChildrenUpdate = false;
1✔
319

1✔
320
            node.material.visible = true;
1✔
321
            this.info.update(node);
1✔
322

1✔
323
            if (node.pendingSubdivision || (TiledGeometryLayer.hasEnoughTexturesToSubdivide(context, node) && this.subdivision(context, this, node))) {
1✔
324
                this.subdivideNode(context, node);
1✔
325
                // display iff children aren't ready
1✔
326
                node.material.visible = node.pendingSubdivision;
1✔
327
                this.info.update(node);
1✔
328
                requestChildrenUpdate = true;
1✔
329
            }
1✔
330

1✔
331
            if (node.material.visible) {
1✔
332
                if (!requestChildrenUpdate) {
1!
333
                    return ObjectRemovalHelper.removeChildren(this, node);
×
334
                }
×
335
            }
1✔
336

1✔
337
            return requestChildrenUpdate ? node.children.filter(n => n.layer == this) : undefined;
1!
338
        }
1!
339

×
340
        node.material.visible = false;
×
341
        this.info.update(node);
×
342
        return ObjectRemovalHelper.removeChildren(this, node);
×
343
    }
×
344

1✔
345
    convert(requester, extent) {
1✔
346
        return convertToTile.convert(requester, extent, this);
65✔
347
    }
65✔
348

1✔
349
    countColorLayersTextures(...layers) {
1✔
350
        return layers.length;
×
351
    }
×
352

1✔
353
    // eslint-disable-next-line
1✔
354
    culling(node, camera) {
1✔
355
        return !camera.isBox3Visible(node.obb.box3D, node.matrixWorld);
1✔
356
    }
1✔
357

1✔
358
    /**
1✔
359
     * Tell if a node has enough elevation or color textures to subdivide.
1✔
360
     * Subdivision is prevented if:
1✔
361
     * <ul>
1✔
362
     *  <li>the node is covered by at least one elevation layer and if the node
1✔
363
     *  doesn't have an elevation texture yet</li>
1✔
364
     *  <li>a color texture is missing</li>
1✔
365
     * </ul>
1✔
366
     *
1✔
367
     * @param {Object} context - The context of the update; see the {@link
1✔
368
     * MainLoop} for more informations.
1✔
369
     * @param {TileMesh} node - The node to subdivide.
1✔
370
     *
1✔
371
     * @returns {boolean} False if the node can not be subdivided, true
1✔
372
     * otherwise.
1✔
373
     */
1✔
374
    static hasEnoughTexturesToSubdivide(context, node) {
1✔
375
        const layerUpdateState = node.layerUpdateState || {};
1!
376
        let nodeLayer = node.material.getElevationTile();
1✔
377

1✔
378
        for (const e of context.elevationLayers) {
1!
379
            const extents = node.getExtentsByProjection(e.crs);
×
380
            const zoom = extents[0].zoom;
×
381
            if (zoom > e.zoom.max || zoom < e.zoom.min) {
×
382
                continue;
×
383
            }
×
384
            if (!e.frozen && e.ready && e.source.extentInsideLimit(node.extent, zoom) && (!nodeLayer || nodeLayer.level < 0)) {
×
385
                // no stop subdivision in the case of a loading error
×
386
                if (layerUpdateState[e.id] && layerUpdateState[e.id].inError()) {
×
387
                    continue;
×
388
                }
×
389
                return false;
×
390
            }
×
391
        }
×
392

1✔
393
        for (const c of context.colorLayers) {
1!
394
            if (c.frozen || !c.visible || !c.ready) {
×
395
                continue;
×
396
            }
×
397
            const extents = node.getExtentsByProjection(c.crs);
×
398
            const zoom = extents[0].zoom;
×
399
            if (zoom > c.zoom.max || zoom < c.zoom.min) {
×
400
                continue;
×
401
            }
×
402
            // no stop subdivision in the case of a loading error
×
403
            if (layerUpdateState[c.id] && layerUpdateState[c.id].inError()) {
×
404
                continue;
×
405
            }
×
NEW
406
            nodeLayer = node.material.getColorTile(c.id);
×
407
            if (c.source.extentInsideLimit(node.extent, zoom) && (!nodeLayer || nodeLayer.level < 0)) {
×
408
                return false;
×
409
            }
×
410
        }
×
411
        return true;
1✔
412
    }
1✔
413

1✔
414
    /**
1✔
415
     * Subdivides a node of this layer. If the node is currently in the process
1✔
416
     * of subdivision, it will not do anything here. The subdivision of a node
1✔
417
     * will occur in four part, to create a quadtree. The extent of the node
1✔
418
     * will be divided in four parts: north-west, north-east, south-west and
1✔
419
     * south-east. Once all four nodes are created, they will be added to the
1✔
420
     * current node and the view of the context will be notified of this change.
1✔
421
     *
1✔
422
     * @param {Object} context - The context of the update; see the {@link
1✔
423
     * MainLoop} for more informations.
1✔
424
     * @param {TileMesh} node - The node to subdivide.
1✔
425
     * @return {Promise}  { description_of_the_return_value }
1✔
426
     */
1✔
427
    subdivideNode(context, node) {
1✔
428
        if (!node.pendingSubdivision && !node.children.some(n => n.layer == this)) {
4✔
429
            const extents = node.extent.subdivision();
4✔
430
            // TODO: pendingSubdivision mechanism is fragile, get rid of it
4✔
431
            node.pendingSubdivision = true;
4✔
432

4✔
433
            const command = {
4✔
434
                /* mandatory */
4✔
435
                view: context.view,
4✔
436
                requester: node,
4✔
437
                layer: this,
4✔
438
                priority: 10000,
4✔
439
                /* specific params */
4✔
440
                extentsSource: extents,
4✔
441
                redraw: false,
4✔
442
            };
4✔
443

4✔
444
            return context.scheduler.execute(command).then((children) => {
4✔
445
                for (const child of children) {
1✔
446
                    node.add(child);
4✔
447
                    child.updateMatrixWorld(true);
4✔
448
                }
4✔
449

1✔
450
                node.pendingSubdivision = false;
1✔
451
                context.view.notifyChange(node, false);
1✔
452
            }, (err) => {
4✔
453
                node.pendingSubdivision = false;
×
454
                if (!err.isCancelledCommandException) {
×
455
                    throw new Error(err);
×
456
                }
×
457
            });
4✔
458
        }
4✔
459
    }
4✔
460

1✔
461
    /**
1✔
462
     * Test the subdvision of a node, compared to this layer.
1✔
463
     *
1✔
464
     * @param {Object} context - The context of the update; see the {@link
1✔
465
     * MainLoop} for more informations.
1✔
466
     * @param {PlanarLayer} layer - This layer, parameter to be removed.
1✔
467
     * @param {TileMesh} node - The node to test.
1✔
468
     *
1✔
469
     * @return {boolean} - True if the node is subdivisable, otherwise false.
1✔
470
     */
1✔
471
    subdivision(context, layer, node) {
1✔
472
        if (node.level < this.minSubdivisionLevel) {
3✔
473
            return true;
1✔
474
        }
1✔
475

2✔
476
        if (this.maxSubdivisionLevel <= node.level) {
3!
477
            return false;
×
478
        }
✔
479
        subdivisionVector.setFromMatrixScale(node.matrixWorld);
2✔
480
        boundingSphereCenter.copy(node.boundingSphere.center).applyMatrix4(node.matrixWorld);
2✔
481
        const distance = Math.max(
2✔
482
            0.0,
2✔
483
            context.camera.camera3D.position.distanceTo(boundingSphereCenter) - node.boundingSphere.radius * subdivisionVector.x);
2✔
484

2✔
485
        // Size projection on pixel of bounding
2✔
486
        if (context.camera.camera3D.isOrthographicCamera) {
3!
487
            const camera3D = context.camera.camera3D;
×
488
            const preSSE = context.camera._preSSE * 2 * camera3D.zoom / (camera3D.top - camera3D.bottom);
×
489
            node.screenSize = preSSE * node.boundingSphere.radius * subdivisionVector.x;
×
490
        } else {
3✔
491
            node.screenSize = context.camera._preSSE * (2 * node.boundingSphere.radius * subdivisionVector.x) / distance;
2✔
492
        }
2✔
493

2✔
494
        // The screen space error is calculated to have a correct texture display.
2✔
495
        // For the projection of a texture's texel to be less than or equal to one pixel
2✔
496
        const sse = node.screenSize / (this.sizeDiagonalTexture * 2);
2✔
497

2✔
498
        return this.sseSubdivisionThreshold < sse;
2✔
499
    }
2✔
500
}
1✔
501

1✔
502
export default TiledGeometryLayer;
1✔
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

© 2026 Coveralls, Inc