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

iTowns / itowns / 10902507646

17 Sep 2024 11:56AM UTC coverage: 86.931% (-0.03%) from 86.964%
10902507646

push

github

Desplandis
release v2.44.2

2791 of 3694 branches covered (75.55%)

Branch coverage included in aggregate %.

24241 of 27402 relevant lines covered (88.46%)

1027.5 hits per line

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

75.35
/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
class TiledGeometryLayer extends GeometryLayer {
1✔
22
    /**
1✔
23
     * A layer extending the {@link GeometryLayer}, but with a tiling notion.
1✔
24
     *
1✔
25
     * `TiledGeometryLayer` is the ground where `ColorLayer` and `ElevationLayer` are attached.
1✔
26
     * `TiledGeometryLayer` is a quadtree data structure. At zoom 0,
1✔
27
     * there is a single tile for the whole earth. At zoom level 1,
1✔
28
     * the single tile splits into 4 tiles (2x2 tile square).
1✔
29
     * Each zoom level quadtree divides the geometry tiles of the one before it.
1✔
30
     * The camera distance determines how the tiles are subdivided for optimal data display.
1✔
31
     *
1✔
32
     * Some `GeometryLayer` can also be attached to the `TiledGeometryLayer` if they want to take advantage of the quadtree.
1✔
33
     *
1✔
34
     * ![tiled geometry](/docs/static/images/tiledGeometry.jpeg)
1✔
35
     * _In `GlobeView`, **red lines** represents the **WGS84 grid** and **orange lines** the **Pseudo-mercator grid**._
1✔
36
     * _In this picture, there are tiles with 3 different zoom/levels._
1✔
37
     *
1✔
38
     * The zoom/level is based on [tiled web map](https://en.wikipedia.org/wiki/Tiled_web_map).
1✔
39
     * It corresponds at meters by pixel. If the projection tile exceeds a certain pixel size (on screen)
1✔
40
     * then it is subdivided into 4 tiles with a zoom greater than 1.
1✔
41
     *
1✔
42
     * @extends GeometryLayer
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
        // cacheLifeTime = CACHE_POLICIES.INFINITE because the cache is handled by the builder
30✔
63
        config.cacheLifeTime = CACHE_POLICIES.INFINITE;
30✔
64
        config.source = false;
30✔
65
        super(id, object3d, config);
30✔
66

30✔
67
        this.isTiledGeometryLayer = true;
30✔
68
        // TODO : this should be add in a preprocess method specific to GeoidLayer.
30✔
69
        this.object3d.geoidHeight = 0;
30✔
70

30✔
71
        this.protocol = 'tile';
30✔
72
        this._hideSkirt = !!config.hideSkirt;
30✔
73

30✔
74
        this.sseSubdivisionThreshold = this.sseSubdivisionThreshold || 1.0;
30✔
75

30✔
76
        this.schemeTile = schemeTile;
30✔
77
        this.builder = builder;
30✔
78
        this.info = new InfoTiledGeometryLayer(this);
30✔
79

30✔
80
        if (!this.schemeTile) {
30!
81
            throw new Error(`Cannot init tiled layer without schemeTile for layer ${this.id}`);
×
82
        }
×
83

30✔
84
        if (!this.builder) {
30!
85
            throw new Error(`Cannot init tiled layer without builder for layer ${this.id}`);
×
86
        }
×
87

30✔
88
        this.level0Nodes = [];
30✔
89
        const promises = [];
30✔
90

30✔
91
        for (const root of this.schemeTile) {
30✔
92
            promises.push(this.convert(undefined, root));
51✔
93
        }
51✔
94

30✔
95
        this._promises.push(Promise.all(promises).then((level0s) => {
30✔
96
            this.level0Nodes = level0s;
30✔
97
            this.object3d.add(...level0s);
30✔
98
            this.object3d.updateMatrixWorld();
30✔
99
        }));
30✔
100

30✔
101
        this.maxScreenSizeNode = this.sseSubdivisionThreshold * (this.sizeDiagonalTexture * 2);
30✔
102
    }
30✔
103

1✔
104
    get hideSkirt() {
1✔
105
        return this._hideSkirt;
63✔
106
    }
63✔
107
    set hideSkirt(value) {
1✔
108
        if (!this.level0Nodes) {
2✔
109
            return;
2✔
110
        }
2!
111
        this._hideSkirt = value;
×
112
        for (const node of this.level0Nodes) {
×
113
            node.traverse((obj) => {
×
114
                if (obj.isTileMesh) {
×
115
                    obj.geometry.hideSkirt = value;
×
116
                }
×
117
            });
×
118
        }
×
119
    }
×
120
    /**
1✔
121
     * Picking method for this layer. It uses the {@link Picking#pickTilesAt}
1✔
122
     * method.
1✔
123
     *
1✔
124
     * @param {View} view - The view instance.
1✔
125
     * @param {Object} coordinates - The coordinates to pick in the view. It
1✔
126
     * should have at least `x` and `y` properties.
1✔
127
     * @param {number} radius - Radius of the picking circle.
1✔
128
     * @param {Array} target - Array to push picking result.
1✔
129
     *
1✔
130
     * @return {Array} An array containing all targets picked under the
1✔
131
     * specified coordinates.
1✔
132
     */
1✔
133
    pickObjectsAt(view, coordinates, radius = this.options.defaultPickingRadius, target = []) {
1!
134
        return Picking.pickTilesAt(view, coordinates, radius, this, target);
1✔
135
    }
1✔
136

1✔
137
    /**
1✔
138
     * Does pre-update work on the context:
1✔
139
     * <ul>
1✔
140
     *  <li>update the `colorLayers` and `elevationLayers`</li>
1✔
141
     *  <li>update the `maxElevationLevel`</li>
1✔
142
     * </ul>
1✔
143
     *
1✔
144
     * Once this work is done, it returns a list of nodes to update. Depending
1✔
145
     * on the origin of `sources`, it can return a few things:
1✔
146
     * <ul>
1✔
147
     *  <li>if `sources` is empty, it returns the first node of the layer
1✔
148
     *  (stored as `level0Nodes`), which will trigger the update of the whole
1✔
149
     *  tree</li>
1✔
150
     *  <li>if the update is triggered by a camera move, the whole tree is
1✔
151
     *  returned too</li>
1✔
152
     *  <li>if `source.layer` is this layer, it means that `source` is a node; a
1✔
153
     *  common ancestor will be found if there are multiple sources, with the
1✔
154
     *  default common ancestor being the first source itself</li>
1✔
155
     *  <li>else it returns the whole tree</li>
1✔
156
     * </ul>
1✔
157
     *
1✔
158
     * @param {Object} context - The context of the update; see the {@link
1✔
159
     * MainLoop} for more informations.
1✔
160
     * @param {Set<GeometryLayer|TileMesh>} sources - A list of sources to
1✔
161
     * generate a list of nodes to update.
1✔
162
     *
1✔
163
     * @return {TileMesh[]} The array of nodes to update.
1✔
164
     */
1✔
165
    preUpdate(context, sources) {
1✔
166
        if (sources.has(undefined) || sources.size == 0) {
2!
167
            return this.level0Nodes;
×
168
        }
×
169

2✔
170
        if (__DEBUG__) {
2✔
171
            this._latestUpdateStartingLevel = 0;
2✔
172
        }
2✔
173

2✔
174
        context.colorLayers = context.view.getLayers(
2✔
175
            (l, a) => a && a.id == this.id && l.isColorLayer);
2!
176
        context.elevationLayers = context.view.getLayers(
2✔
177
            (l, a) => a && a.id == this.id && l.isElevationLayer);
2!
178

2✔
179
        context.maxElevationLevel = -1;
2✔
180
        for (const e of context.elevationLayers) {
2!
181
            context.maxElevationLevel = Math.max(e.source.zoom.max, context.maxElevationLevel);
×
182
        }
×
183
        if (context.maxElevationLevel == -1) {
2✔
184
            context.maxElevationLevel = Infinity;
2✔
185
        }
2✔
186

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

2✔
192
        let commonAncestor;
2✔
193
        for (const source of sources.values()) {
2✔
194
            if (source.isCamera) {
2!
195
                // if the change is caused by a camera move, no need to bother
×
196
                // to find common ancestor: we need to update the whole tree:
×
197
                // some invisible tiles may now be visible
×
198
                return this.level0Nodes;
×
199
            }
×
200
            if (source.layer === this) {
2!
201
                if (!commonAncestor) {
×
202
                    commonAncestor = source;
×
203
                } else {
×
204
                    commonAncestor = source.findCommonAncestor(commonAncestor);
×
205
                    if (!commonAncestor) {
×
206
                        return this.level0Nodes;
×
207
                    }
×
208
                }
×
209
                if (commonAncestor.material == null) {
×
210
                    commonAncestor = undefined;
×
211
                }
×
212
            }
×
213
        }
2✔
214
        if (commonAncestor) {
2!
215
            if (__DEBUG__) {
×
216
                this._latestUpdateStartingLevel = commonAncestor.level;
×
217
            }
×
218
            return [commonAncestor];
×
219
        } else {
2✔
220
            return this.level0Nodes;
2✔
221
        }
2✔
222
    }
2✔
223

1✔
224
    /**
1✔
225
     * Update a node of this layer. The node will not be updated if:
1✔
226
     * <ul>
1✔
227
     *  <li>it does not have a parent, then it is removed</li>
1✔
228
     *  <li>its parent is being subdivided</li>
1✔
229
     *  <li>is not visible in the camera</li>
1✔
230
     * </ul>
1✔
231
     *
1✔
232
     * @param {Object} context - The context of the update; see the {@link
1✔
233
     * MainLoop} for more informations.
1✔
234
     * @param {Layer} layer - Parameter to be removed once all update methods
1✔
235
     * have been aligned.
1✔
236
     * @param {TileMesh} node - The node to update.
1✔
237
     *
1✔
238
     * @returns {Object}
1✔
239
     */
1✔
240
    update(context, layer, node) {
1✔
241
        if (!node.parent) {
5✔
242
            return ObjectRemovalHelper.removeChildrenAndCleanup(this, node);
4✔
243
        }
4✔
244
        // early exit if parent' subdivision is in progress
1✔
245
        if (node.parent.pendingSubdivision) {
5!
246
            node.visible = false;
×
247
            node.material.visible = false;
×
248
            this.info.update(node);
×
249
            return undefined;
×
250
        }
✔
251

1✔
252
        // do proper culling
1✔
253
        node.visible = !this.culling(node, context.camera);
1✔
254

1✔
255
        if (node.visible) {
1✔
256
            let requestChildrenUpdate = false;
1✔
257

1✔
258
            node.material.visible = true;
1✔
259
            this.info.update(node);
1✔
260

1✔
261
            if (node.pendingSubdivision || (TiledGeometryLayer.hasEnoughTexturesToSubdivide(context, node) && this.subdivision(context, this, node))) {
1✔
262
                this.subdivideNode(context, node);
1✔
263
                // display iff children aren't ready
1✔
264
                node.material.visible = node.pendingSubdivision;
1✔
265
                this.info.update(node);
1✔
266
                requestChildrenUpdate = true;
1✔
267
            }
1✔
268

1✔
269
            if (node.material.visible) {
1✔
270
                if (!requestChildrenUpdate) {
1!
271
                    return ObjectRemovalHelper.removeChildren(this, node);
×
272
                }
×
273
            }
1✔
274

1✔
275
            return requestChildrenUpdate ? node.children.filter(n => n.layer == this) : undefined;
1!
276
        }
1!
277

×
278
        node.material.visible = false;
×
279
        this.info.update(node);
×
280
        return ObjectRemovalHelper.removeChildren(this, node);
×
281
    }
×
282

1✔
283
    convert(requester, extent) {
1✔
284
        return convertToTile.convert(requester, extent, this);
63✔
285
    }
63✔
286

1✔
287
    countColorLayersTextures(...layers) {
1✔
288
        return layers.length;
×
289
    }
×
290

1✔
291
    // eslint-disable-next-line
1✔
292
    culling(node, camera) {
1✔
293
        return !camera.isBox3Visible(node.obb.box3D, node.matrixWorld);
1✔
294
    }
1✔
295

1✔
296
    /**
1✔
297
     * Tell if a node has enough elevation or color textures to subdivide.
1✔
298
     * Subdivision is prevented if:
1✔
299
     * <ul>
1✔
300
     *  <li>the node is covered by at least one elevation layer and if the node
1✔
301
     *  doesn't have an elevation texture yet</li>
1✔
302
     *  <li>a color texture is missing</li>
1✔
303
     * </ul>
1✔
304
     *
1✔
305
     * @param {Object} context - The context of the update; see the {@link
1✔
306
     * MainLoop} for more informations.
1✔
307
     * @param {TileMesh} node - The node to subdivide.
1✔
308
     *
1✔
309
     * @returns {boolean} False if the node can not be subdivided, true
1✔
310
     * otherwise.
1✔
311
     */
1✔
312
    static hasEnoughTexturesToSubdivide(context, node) {
1✔
313
        const layerUpdateState = node.layerUpdateState || {};
1!
314
        let nodeLayer = node.material.getElevationLayer();
1✔
315

1✔
316
        for (const e of context.elevationLayers) {
1!
317
            const extents = node.getExtentsByProjection(e.crs);
×
318
            const zoom = extents[0].zoom;
×
319
            if (zoom > e.zoom.max || zoom < e.zoom.min) {
×
320
                continue;
×
321
            }
×
322
            if (!e.frozen && e.ready && e.source.extentInsideLimit(node.extent, zoom) && (!nodeLayer || nodeLayer.level < 0)) {
×
323
                // no stop subdivision in the case of a loading error
×
324
                if (layerUpdateState[e.id] && layerUpdateState[e.id].inError()) {
×
325
                    continue;
×
326
                }
×
327
                return false;
×
328
            }
×
329
        }
×
330

1✔
331
        for (const c of context.colorLayers) {
1!
332
            if (c.frozen || !c.visible || !c.ready) {
×
333
                continue;
×
334
            }
×
335
            const extents = node.getExtentsByProjection(c.crs);
×
336
            const zoom = extents[0].zoom;
×
337
            if (zoom > c.zoom.max || zoom < c.zoom.min) {
×
338
                continue;
×
339
            }
×
340
            // no stop subdivision in the case of a loading error
×
341
            if (layerUpdateState[c.id] && layerUpdateState[c.id].inError()) {
×
342
                continue;
×
343
            }
×
344
            nodeLayer = node.material.getLayer(c.id);
×
345
            if (c.source.extentInsideLimit(node.extent, zoom) && (!nodeLayer || nodeLayer.level < 0)) {
×
346
                return false;
×
347
            }
×
348
        }
×
349
        return true;
1✔
350
    }
1✔
351

1✔
352
    /**
1✔
353
     * Subdivides a node of this layer. If the node is currently in the process
1✔
354
     * of subdivision, it will not do anything here. The subdivision of a node
1✔
355
     * will occur in four part, to create a quadtree. The extent of the node
1✔
356
     * will be divided in four parts: north-west, north-east, south-west and
1✔
357
     * south-east. Once all four nodes are created, they will be added to the
1✔
358
     * current node and the view of the context will be notified of this change.
1✔
359
     *
1✔
360
     * @param {Object} context - The context of the update; see the {@link
1✔
361
     * MainLoop} for more informations.
1✔
362
     * @param {TileMesh} node - The node to subdivide.
1✔
363
     * @return {Promise}  { description_of_the_return_value }
1✔
364
     */
1✔
365
    subdivideNode(context, node) {
1✔
366
        if (!node.pendingSubdivision && !node.children.some(n => n.layer == this)) {
4✔
367
            const extents = node.extent.subdivision();
4✔
368
            // TODO: pendingSubdivision mechanism is fragile, get rid of it
4✔
369
            node.pendingSubdivision = true;
4✔
370

4✔
371
            const command = {
4✔
372
                /* mandatory */
4✔
373
                view: context.view,
4✔
374
                requester: node,
4✔
375
                layer: this,
4✔
376
                priority: 10000,
4✔
377
                /* specific params */
4✔
378
                extentsSource: extents,
4✔
379
                redraw: false,
4✔
380
            };
4✔
381

4✔
382
            return context.scheduler.execute(command).then((children) => {
4✔
383
                for (const child of children) {
1✔
384
                    node.add(child);
4✔
385
                    child.updateMatrixWorld(true);
4✔
386
                }
4✔
387

1✔
388
                node.pendingSubdivision = false;
1✔
389
                context.view.notifyChange(node, false);
1✔
390
            }, (err) => {
4✔
391
                node.pendingSubdivision = false;
×
392
                if (!err.isCancelledCommandException) {
×
393
                    throw new Error(err);
×
394
                }
×
395
            });
4✔
396
        }
4✔
397
    }
4✔
398

1✔
399
    /**
1✔
400
     * Test the subdvision of a node, compared to this layer.
1✔
401
     *
1✔
402
     * @param {Object} context - The context of the update; see the {@link
1✔
403
     * MainLoop} for more informations.
1✔
404
     * @param {PlanarLayer} layer - This layer, parameter to be removed.
1✔
405
     * @param {TileMesh} node - The node to test.
1✔
406
     *
1✔
407
     * @return {boolean} - True if the node is subdivisable, otherwise false.
1✔
408
     */
1✔
409
    subdivision(context, layer, node) {
1✔
410
        if (node.level < this.minSubdivisionLevel) {
3✔
411
            return true;
1✔
412
        }
1✔
413

2✔
414
        if (this.maxSubdivisionLevel <= node.level) {
3!
415
            return false;
×
416
        }
✔
417

2✔
418
        // Prevent to subdivise the node if the current elevation level
2✔
419
        // we must avoid a tile, with level 20, inherits a level 3 elevation texture.
2✔
420
        // The induced geometric error is much too large and distorts the SSE
2✔
421
        const nodeLayer = node.material.getElevationLayer();
2✔
422
        if (nodeLayer) {
3!
423
            const currentTexture = nodeLayer.textures[0];
×
424
            if (currentTexture && currentTexture.extent) {
×
425
                const offsetScale = nodeLayer.offsetScales[0];
×
426
                const ratio = offsetScale.z;
×
427
                // ratio is node size / texture size
×
428
                if (ratio < 1 / 2 ** this.maxDeltaElevationLevel) {
×
429
                    return false;
×
430
                }
×
431
            }
×
432
        }
✔
433

2✔
434
        subdivisionVector.setFromMatrixScale(node.matrixWorld);
2✔
435
        boundingSphereCenter.copy(node.boundingSphere.center).applyMatrix4(node.matrixWorld);
2✔
436
        const distance = Math.max(
2✔
437
            0.0,
2✔
438
            context.camera.camera3D.position.distanceTo(boundingSphereCenter) - node.boundingSphere.radius * subdivisionVector.x);
2✔
439

2✔
440
        // Size projection on pixel of bounding
2✔
441
        if (context.camera.camera3D.isOrthographicCamera) {
3!
442
            const camera3D = context.camera.camera3D;
×
443
            const preSSE = context.camera._preSSE * 2 * camera3D.zoom / (camera3D.top - camera3D.bottom);
×
444
            node.screenSize = preSSE * node.boundingSphere.radius * subdivisionVector.x;
×
445
        } else {
3✔
446
            node.screenSize = context.camera._preSSE * (2 * node.boundingSphere.radius * subdivisionVector.x) / distance;
2✔
447
        }
2✔
448

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

2✔
453
        return this.sseSubdivisionThreshold < sse;
2✔
454
    }
2✔
455
}
1✔
456

1✔
457
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