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

iTowns / itowns / 11403513714

18 Oct 2024 12:16PM UTC coverage: 86.85% (-0.09%) from 86.935%
11403513714

push

github

web-flow
fix(OGC3DTilesLayer): handle multiple views (#2435)

fix(OGC3DTilesLayer): handle multiple views cache

2792 of 3708 branches covered (75.3%)

Branch coverage included in aggregate %.

22 of 41 new or added lines in 2 files covered. (53.66%)

8 existing lines in 1 file now uncovered.

24366 of 27562 relevant lines covered (88.4%)

1022.27 hits per line

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

69.72
/src/Layer/OGC3DTilesLayer.js
1
import * as THREE from 'three';
1✔
2
import {
1✔
3
    TilesRenderer,
1✔
4
    GLTFStructuralMetadataExtension,
1✔
5
    GLTFMeshFeaturesExtension,
1✔
6
    GLTFCesiumRTCExtension,
1✔
7
    CesiumIonAuthPlugin,
1✔
8
    GoogleCloudAuthPlugin,
1✔
9
    ImplicitTilingPlugin,
1✔
10
} from '3d-tiles-renderer';
1✔
11

1✔
12
import GeometryLayer from 'Layer/GeometryLayer';
1✔
13
import iGLTFLoader from 'Parser/iGLTFLoader';
1✔
14
import { DRACOLoader } from 'ThreeExtended/loaders/DRACOLoader';
1✔
15
import { KTX2Loader } from 'ThreeExtended/loaders/KTX2Loader';
1✔
16
import ReferLayerProperties from 'Layer/ReferencingLayerProperties';
1✔
17
import PointsMaterial, {
1✔
18
    PNTS_MODE,
1✔
19
    PNTS_SHAPE,
1✔
20
    PNTS_SIZE_MODE,
1✔
21
    ClassificationScheme,
1✔
22
} from 'Renderer/PointsMaterial';
1✔
23
import { VIEW_EVENTS } from 'Core/View';
1✔
24

1✔
25
const _raycaster = new THREE.Raycaster();
1✔
26

1✔
27
// Stores lruCache, downloadQueue and parseQueue for each id of view {@link View}
1✔
28
// every time a tileset has been added
1✔
29
// https://github.com/iTowns/itowns/issues/2426
1✔
30
const viewers = {};
1✔
31

1✔
32
// Internal instance of GLTFLoader, passed to 3d-tiles-renderer-js to support GLTF 1.0 and 2.0
1✔
33
// Temporary exported to be used in deprecated B3dmParser
1✔
34
export const itownsGLTFLoader = new iGLTFLoader();
1✔
35
itownsGLTFLoader.register(() => new GLTFMeshFeaturesExtension());
1✔
36
itownsGLTFLoader.register(() => new GLTFStructuralMetadataExtension());
1✔
37
itownsGLTFLoader.register(() => new GLTFCesiumRTCExtension());
1✔
38

1✔
39
export const OGC3DTILES_LAYER_EVENTS = {
1✔
40
    /**
1✔
41
     * Fired when a new root or child tile set is loaded
1✔
42
     * @event OGC3DTilesLayer#load-tile-set
1✔
43
     * @type {Object}
1✔
44
     * @property {Object} tileset - the tileset json parsed in an Object
1✔
45
     * @property {String} url - tileset url
1✔
46
     */
1✔
47
    LOAD_TILE_SET: 'load-tile-set',
1✔
48
    /**
1✔
49
     * Fired when a tile model is loaded
1✔
50
     * @event OGC3DTilesLayer#load-model
1✔
51
     * @type {Object}
1✔
52
     * @property {THREE.Group} scene - the model (tile content) parsed in a THREE.GROUP
1✔
53
     * @property {Object} tile - the tile metadata from the tileset
1✔
54
     */
1✔
55
    LOAD_MODEL: 'load-model',
1✔
56
    /**
1✔
57
     * Fired when a tile model is disposed
1✔
58
     * @event OGC3DTilesLayer#dispose-model
1✔
59
     * @type {Object}
1✔
60
     * @property {THREE.Group} scene - the model (tile content) that is disposed
1✔
61
     * @property {Object} tile - the tile metadata from the tileset
1✔
62
     */
1✔
63
    DISPOSE_MODEL: 'dispose-model',
1✔
64
    /**
1✔
65
     * Fired when a tiles visibility changes
1✔
66
     * @event OGC3DTilesLayer#tile-visibility-change
1✔
67
     * @type {Object}
1✔
68
     * @property {THREE.Group} scene - the model (tile content) parsed in a THREE.GROUP
1✔
69
     * @property {Object} tile - the tile metadata from the tileset
1✔
70
     * @property {boolean} visible - the tile visible state
1✔
71
     */
1✔
72
    TILE_VISIBILITY_CHANGE: 'tile-visibility-change',
1✔
73
    /**
1✔
74
     * Fired when a new batch of tiles start loading (can be fired multiple times, e.g. when the camera moves and new tiles
1✔
75
     * start loading)
1✔
76
     * @event OGC3DTilesLayer#tiles-load-start
1✔
77
     */
1✔
78
    TILES_LOAD_START: 'tiles-load-start',
1✔
79
    /**
1✔
80
     * Fired when all visible tiles are loaded (can be fired multiple times, e.g. when the camera moves and new tiles
1✔
81
     * are loaded)
1✔
82
     * @event OGC3DTilesLayer#tiles-load-end
1✔
83
     */
1✔
84
    TILES_LOAD_END: 'tiles-load-end',
1✔
85
};
1✔
86

1✔
87
/**
1✔
88
 * Enable loading 3D Tiles with [Draco](https://google.github.io/draco/) geometry extension.
1✔
89
 *
1✔
90
 * @param {String} path path to draco library folder containing the JS and WASM decoder libraries. They can be found in
1✔
91
 * [itowns examples](https://github.com/iTowns/itowns/tree/master/examples/libs/draco).
1✔
92
 * @param {Object} [config] optional configuration for Draco decoder (see threejs'
1✔
93
 * [setDecoderConfig](https://threejs.org/docs/index.html?q=draco#examples/en/loaders/DRACOLoader.setDecoderConfig) that
1✔
94
 * is called under the hood with this configuration for details.
1✔
95
 */
1✔
96
export function enableDracoLoader(path, config) {
1✔
97
    if (!path) {
3✔
98
        throw new Error('Path to draco folder is mandatory');
1✔
99
    }
1✔
100
    const dracoLoader = new DRACOLoader();
2✔
101
    dracoLoader.setDecoderPath(path);
2✔
102
    if (config) {
3✔
103
        dracoLoader.setDecoderConfig(config);
1✔
104
    }
1✔
105
    itownsGLTFLoader.setDRACOLoader(dracoLoader);
2✔
106
}
2✔
107

1✔
108
/**
1✔
109
 * Enable loading 3D Tiles with [KTX2](https://www.khronos.org/ktx/) texture extension.
1✔
110
 *
1✔
111
 * @param {String} path path to ktx2 library folder containing the JS and WASM decoder libraries. They can be found in
1✔
112
 * [itowns examples](https://github.com/iTowns/itowns/tree/master/examples/libs/basis).
1✔
113
 * @param {THREE.WebGLRenderer} renderer the threejs renderer
1✔
114
 */
1✔
115
export function enableKtx2Loader(path, renderer) {
1✔
116
    if (!path || !renderer) {
3✔
117
        throw new Error('Path to ktx2 folder and renderer are mandatory');
2✔
118
    }
2✔
119
    const ktx2Loader = new KTX2Loader();
1✔
120
    ktx2Loader.setTranscoderPath(path);
1✔
121
    ktx2Loader.detectSupport(renderer);
1✔
122
    itownsGLTFLoader.setKTX2Loader(ktx2Loader);
1✔
123
}
1✔
124

1✔
125
class OGC3DTilesLayer extends GeometryLayer {
1✔
126
    /**
1✔
127
     * Layer for [3D Tiles](https://www.ogc.org/standard/3dtiles/) datasets.
1✔
128
     *
1✔
129
     * Advanced configuration note: 3D Tiles rendering is delegated to 3DTilesRendererJS that exposes several
1✔
130
     * configuration options accessible through the tilesRenderer property of this class. see the
1✔
131
     * [3DTilesRendererJS doc](https://github.com/NASA-AMMOS/3DTilesRendererJS/blob/master/README.md). Also note that
1✔
132
     * the cache is shared amongst 3D tiles layers and can be configured through tilesRenderer.lruCache (see the
1✔
133
     * [following documentation](https://github.com/NASA-AMMOS/3DTilesRendererJS/blob/master/README.md#lrucache-1).
1✔
134
     *
1✔
135
     * @extends Layer
1✔
136
     *
1✔
137
     * @param {String} id - unique layer id.
1✔
138
     * @param {Object} config - layer specific configuration
1✔
139
     * @param {OGC3DTilesSource} config.source - data source configuration
1✔
140
     * @param {String} [config.pntsMode= PNTS_MODE.COLOR] Point cloud coloring mode (passed to {@link PointsMaterial}).
1✔
141
     *      Only 'COLOR' or 'CLASSIFICATION' are possible. COLOR uses RGB colors of the points,
1✔
142
     *      CLASSIFICATION uses a classification property of the batch table to color points.
1✔
143
     * @param {ClassificationScheme}  [config.classificationScheme]  {@link PointsMaterial} classification scheme
1✔
144
     * @param {String} [config.pntsShape= PNTS_SHAPE.CIRCLE] Point cloud point shape. Only 'CIRCLE' or 'SQUARE' are possible.
1✔
145
     * (passed to {@link PointsMaterial}).
1✔
146
     * @param {String} [config.pntsSizeMode= PNTS_SIZE_MODE.VALUE] {@link PointsMaterial} Point cloud size mode (passed to {@link PointsMaterial}).
1✔
147
     * Only 'VALUE' or 'ATTENUATED' are possible. VALUE use constant size, ATTENUATED compute size depending on distance
1✔
148
     * from point to camera.
1✔
149
     * @param {Number} [config.pntsMinAttenuatedSize=3] Minimum scale used by 'ATTENUATED' size mode.
1✔
150
     * @param {Number} [config.pntsMaxAttenuatedSize=10] Maximum scale used by 'ATTENUATED' size mode.
1✔
151
     */
1✔
152
    constructor(id, config) {
1✔
153
        super(id, new THREE.Group(), { source: config.source });
4✔
154
        this.isOGC3DTilesLayer = true;
4✔
155

4✔
156
        this._handlePointsMaterialConfig(config);
4✔
157

4✔
158
        this.tilesRenderer = new TilesRenderer(this.source.url);
4✔
159
        if (config.source.isOGC3DTilesIonSource) {
4✔
160
            this.tilesRenderer.registerPlugin(new CesiumIonAuthPlugin({
1✔
161
                apiToken: config.source.accessToken,
1✔
162
                assetId: config.source.assetId,
1✔
163
                autoRefreshToken: true,
1✔
164
            }));
1✔
165
        } else if (config.source.isOGC3DTilesGoogleSource) {
4✔
166
            this.tilesRenderer.registerPlugin(new GoogleCloudAuthPlugin({
1✔
167
                apiToken: config.source.key,
1✔
168
                autoRefreshToken: true,
1✔
169
            }));
1✔
170
        }
1✔
171
        this.tilesRenderer.registerPlugin(new ImplicitTilingPlugin());
4✔
172

4✔
173
        this.tilesRenderer.manager.addHandler(/\.gltf$/, itownsGLTFLoader);
4✔
174

4✔
175
        this.object3d.add(this.tilesRenderer.group);
4✔
176

4✔
177
        // Add an initialization step that is resolved when the root tileset is loaded (see this._setup below), meaning
4✔
178
        // that the layer will be marked ready when the tileset has been loaded.
4✔
179
        this._res = this.addInitializationStep();
4✔
180

4✔
181
        /**
4✔
182
         * @type {number}
4✔
183
         */
4✔
184
        this.sseThreshold = this.tilesRenderer.errorTarget;
4✔
185
        Object.defineProperty(this, 'sseThreshold', {
4✔
186
            get() { return this.tilesRenderer.errorTarget; },
4✔
187
            set(value) { this.tilesRenderer.errorTarget = value; },
4✔
188
        });
4✔
189

4✔
190
        if (config.sseThreshold) {
4✔
191
            this.sseThreshold = config.sseThreshold;
1✔
192
        }
1✔
193
    }
4✔
194

1✔
195
    /**
1✔
196
     * Store points material config so they can be used later to substitute points tiles material by our own PointsMaterial
1✔
197
     * These properties should eventually be managed through the Style API (see https://github.com/iTowns/itowns/issues/2336)
1✔
198
     * @param {Object} config - points material configuration as passed to the layer constructor.
1✔
199
     * @private
1✔
200
     */
1✔
201
    _handlePointsMaterialConfig(config) {
1✔
202
        this.pntsMode = config.pntsMode ?? PNTS_MODE.COLOR;
4✔
203
        this.pntsShape = config.pntsShape ?? PNTS_SHAPE.CIRCLE;
4✔
204
        this.classification = config.classification ?? ClassificationScheme.DEFAULT;
4✔
205
        this.pntsSizeMode = config.pntsSizeMode ?? PNTS_SIZE_MODE.VALUE;
4✔
206
        this.pntsMinAttenuatedSize = config.pntsMinAttenuatedSize || 3;
4✔
207
        this.pntsMaxAttenuatedSize = config.pntsMaxAttenuatedSize || 10;
4✔
208
    }
4✔
209

1✔
210

1✔
211
    /**
1✔
212
     * Sets the lruCache and download and parse queues so they are shared amongst
1✔
213
     * all tilesets from a same {@link View} view.
1✔
214
     * @param {View} view - view associated to this layer.
1✔
215
     * @private
1✔
216
     */
1✔
217
    _setupCacheAndQueues(view) {
1✔
NEW
218
        const id = view.id;
×
NEW
219
        if (viewers[id]) {
×
NEW
220
            this.tilesRenderer.lruCache = viewers[id].lruCache;
×
NEW
221
            this.tilesRenderer.downloadQueue = viewers[id].downloadQueue;
×
NEW
222
            this.tilesRenderer.parseQueue = viewers[id].parseQueue;
×
UNCOV
223
        } else {
×
NEW
224
            viewers[id] = {
×
NEW
225
                lruCache: this.tilesRenderer.lruCache,
×
NEW
226
                downloadQueue: this.tilesRenderer.downloadQueue,
×
NEW
227
                parseQueue: this.tilesRenderer.parseQueue,
×
NEW
228
            };
×
NEW
229
            view.addEventListener(VIEW_EVENTS.DISPOSED, (evt) => {
×
NEW
230
                delete viewers[evt.target.id];
×
NEW
231
            });
×
UNCOV
232
        }
×
UNCOV
233
    }
×
234

1✔
235
    /**
1✔
236
     * Binds 3d-tiles-renderer events to this layer.
1✔
237
     * @private
1✔
238
     */
1✔
239
    _setupEvents() {
1✔
UNCOV
240
        for (const ev of Object.values(OGC3DTILES_LAYER_EVENTS)) {
×
UNCOV
241
            this.tilesRenderer.addEventListener(ev, (e) => {
×
242
                this.dispatchEvent(e);
×
UNCOV
243
            });
×
UNCOV
244
        }
×
UNCOV
245
    }
×
246

1✔
247
    /**
1✔
248
     * Setup 3D tiles renderer js TilesRenderer with the camera, binds events and start updating. Executed when the
1✔
249
     * layer has been added to the view.
1✔
250
     * @param {View} view - the view the layer has been added to.
1✔
251
     * @private
1✔
252
     */
1✔
253
    _setup(view) {
1✔
254
        this.tilesRenderer.setCamera(view.camera3D);
×
255
        this.tilesRenderer.setResolutionFromRenderer(view.camera3D, view.renderer);
×
256
        // Setup whenReady to be fullfiled when the root tileset has been loaded
×
257
        let rootTilesetLoaded = false;
×
258
        this.tilesRenderer.addEventListener('load-tile-set', () => {
×
259
            view.notifyChange(this);
×
260
            if (!rootTilesetLoaded) {
×
261
                rootTilesetLoaded = true;
×
262
                this._res();
×
263
            }
×
264
        });
×
265
        this.tilesRenderer.addEventListener('load-model', ({ scene }) => {
×
266
            scene.traverse((obj) => {
×
267
                this._assignFinalMaterial(obj);
×
268
                this._assignFinalAttributes(obj);
×
269
            });
×
270
            view.notifyChange(this);
×
271
        });
×
NEW
272

×
NEW
273

×
NEW
274
        this._setupCacheAndQueues(view);
×
NEW
275
        this._setupEvents();
×
NEW
276

×
NEW
277

×
278
        // Start loading tileset and tiles
×
279
        this.tilesRenderer.update();
×
280
    }
×
281

1✔
282
    /**
1✔
283
     * Replace materials from GLTFLoader by our own custom materials. Note that
1✔
284
     * the replaced materials are not compiled yet and will be disposed by the
1✔
285
     * GC.
1✔
286
     * @param {Object3D} model
1✔
287
     * @private
1✔
288
     */
1✔
289
    _assignFinalMaterial(model) {
1✔
290
        let material = model.material;
×
291

×
292
        if (model.isPoints) {
×
293
            const pointsMaterial = new PointsMaterial({
×
294
                mode: this.pntsMode,
×
295
                shape: this.pntsShape,
×
296
                classificationScheme: this.classification,
×
297
                sizeMode: this.pntsSizeMode,
×
298
                minAttenuatedSize: this.pntsMinAttenuatedSize,
×
299
                maxAttenuatedSize: this.pntsMaxAttenuatedSize,
×
300
            });
×
301
            pointsMaterial.copy(material);
×
302

×
303
            material = pointsMaterial;
×
304
        }
×
305

×
306
        if (material) {
×
307
            ReferLayerProperties(material, this);
×
308
        }
×
309

×
310
        model.material = material;
×
311
    }
×
312

1✔
313
    /**
1✔
314
     * @param {Object3D} model
1✔
315
     * @private
1✔
316
     */
1✔
317
    _assignFinalAttributes(model) {
1✔
318
        const geometry = model.geometry;
×
319
        const batchTable = model.batchTable;
×
320

×
321
        // Setup classification bufferAttribute
×
322
        if (model.isPoints) {
×
323
            const classificationData = batchTable?.getPropertyArray('Classification');
×
324
            if (classificationData) {
×
325
                geometry.setAttribute('classification',
×
326
                    new THREE.BufferAttribute(classificationData, 1),
×
327
                );
×
328
            }
×
329
        }
×
330
    }
×
331

1✔
332
    preUpdate(context) {
1✔
333
        this.scale = context.camera._preSSE;
×
334
        this.tilesRenderer.update();
×
335
        return null; // don't return any element because 3d-tiles-renderer already updates them
×
336
    }
×
337

1✔
338
    update() {
1✔
339
        // empty, elements are updated by 3d-tiles-renderer
×
340
    }
1✔
341

1✔
342
    /**
1✔
343
     * Deletes the layer and frees associated memory
1✔
344
     */
1✔
345
    delete() {
1✔
346
        this.tilesRenderer.dispose();
×
347
    }
×
348

1✔
349
    /**
1✔
350
     * Get the attributes for the closest intersection from a list of
1✔
351
     * intersects.
1✔
352
     * @param {Array} intersects -  An array containing all
1✔
353
     * objects picked under mouse coordinates computed with view.pickObjectsAt(..).
1✔
354
     * @returns {Object | null} - An object containing
1✔
355
     */
1✔
356
    getC3DTileFeatureFromIntersectsArray(intersects) {
1✔
357
        if (!intersects.length) { return null; }
×
358

×
359
        const { face, index, object } = intersects[0];
×
360

×
361
        /** @type{number|null} */
×
362
        let batchId;
×
363
        if (object.isPoints && index) {
×
364
            batchId = object.geometry.getAttribute('_BATCHID')?.getX(index) ?? index;
×
365
        } else if (object.isMesh && face) {
×
366
            batchId = object.geometry.getAttribute('_BATCHID')?.getX(face.a);
×
367
        }
×
368

×
369
        if (batchId === undefined) {
×
370
            return null;
×
371
        }
×
372

×
373
        let tileObject = object;
×
374
        while (!tileObject.batchTable) {
×
375
            tileObject = tileObject.parent;
×
376
        }
×
377

×
378
        return tileObject.batchTable.getDataFromId(batchId);
×
379
    }
×
380

1✔
381
    /**
1✔
382
     * Get all 3D objects (mesh and points primitives) as intersects at the
1✔
383
     * given non-normalized screen coordinates.
1✔
384
     * @param {View} view - The view instance.
1✔
385
     * @param {THREE.Vector2} coords - The coordinates to pick in the view. It
1✔
386
     * should have at least `x` and `y` properties.
1✔
387
     * @param {number} radius - Radius of the picking circle.
1✔
388
     * @param {Array} [target=[]] - Target array to push results too
1✔
389
     * @returns {Array} Array containing all target objects picked under the
1✔
390
     * specified coordinates.
1✔
391
     */
1✔
392
    pickObjectsAt(view, coords, radius, target = []) {
1✔
393
        const camera = view.camera.camera3D;
×
394
        _raycaster.setFromCamera(view.viewToNormalizedCoords(coords), camera);
×
395
        _raycaster.near = camera.near;
×
396
        _raycaster.far = camera.far;
×
397

×
398
        _raycaster.firstHitOnly = true;
×
399
        const picked = _raycaster.intersectObject(this.tilesRenderer.group, true);
×
400
        // Store the layer of the picked object to conform to the interface of what's returned by Picking.js (used for
×
401
        // other GeometryLayers
×
402
        picked.forEach((p) => { p.layer = this; });
×
403
        target.push(...picked);
×
404

×
405
        return target;
×
406
    }
×
407

1✔
408
    // eslint-disable-next-line no-unused-vars
1✔
409
    attach(layer) {
1✔
410
        console.warn('[OGC3DTilesLayer]: Attaching / detaching layers is not yet implemented for OGC3DTilesLayer.');
×
411
    }
×
412

1✔
413
    // eslint-disable-next-line no-unused-vars
1✔
414
    detach(layer) {
1✔
415
        console.warn('[OGC3DTilesLayer]: Attaching / detaching layers is not yet implemented for OGC3DTilesLayer.');
×
416
        return true;
×
417
    }
×
418

1✔
419
    // eslint-disable-next-line no-unused-vars
1✔
420
    getObjectToUpdateForAttachedLayers(obj) {
1✔
421
        return null;
×
422
    }
×
423

1✔
424
    /**
1✔
425
     * Executes a callback for each tile of this layer tileset.
1✔
426
     *
1✔
427
     * @param {Function} callback The callback to execute for each tile. Has the following two parameters:
1✔
428
     *  1. tile (Object) - the JSON tile
1✔
429
     *  2. scene (THREE.Object3D | null) - The tile content. Contains a `batchTable` property. Can be null if the tile
1✔
430
     *  has not yet been loaded.
1✔
431
    */
1✔
432
    forEachTile(callback) {
1✔
433
        this.tilesRenderer.traverse((tile) => {
×
434
            callback(tile, tile.cached.scene);
×
435
        });
×
436
    }
×
437
}
1✔
438

1✔
439
export default OGC3DTilesLayer;
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

© 2025 Coveralls, Inc