• 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.02
/packages/Main/src/Core/View.js
1
import * as THREE from 'three';
1✔
2
import { CRS, Coordinates } from '@itowns/geographic';
1✔
3
import Camera from 'Renderer/Camera';
1✔
4
import MainLoop, { MAIN_LOOP_EVENTS, RENDERING_PAUSED } from 'Core/MainLoop';
1✔
5
import Capabilities from 'Core/System/Capabilities';
1✔
6
import { COLOR_LAYERS_ORDER_CHANGED } from 'Renderer/ColorLayersOrdering';
1✔
7
import c3DEngine from 'Renderer/c3DEngine';
1✔
8
import RenderMode from 'Renderer/RenderMode';
1✔
9
import FeaturesUtils from 'Utils/FeaturesUtils';
1✔
10
import { getMaxColorSamplerUnitsCount } from 'Renderer/LayeredMaterial';
1✔
11
import Scheduler from 'Core/Scheduler/Scheduler';
1✔
12
import Picking from 'Core/Picking';
1✔
13
import LabelLayer from 'Layer/LabelLayer';
1✔
14
import ObjectRemovalHelper from 'Process/ObjectRemovalHelper';
1✔
15

1✔
16
export const VIEW_EVENTS = {
1✔
17
    /**
1✔
18
     * Fires when all the layers of the view are considered initialized.
1✔
19
     * Initialized in this context means: all layers are ready to be
1✔
20
     * displayed (no pending network access, no visual improvement to be
1✔
21
     * expected, ...).
1✔
22
     * If you add new layers, the event will be fired again when all
1✔
23
     * layers are ready.
1✔
24
     * @event View#layers-initialized
1✔
25
     * @property type {string} layers-initialized
1✔
26
     */
1✔
27
    LAYERS_INITIALIZED: 'layers-initialized',
1✔
28
    LAYER_REMOVED: 'layer-removed',
1✔
29
    LAYER_ADDED: 'layer-added',
1✔
30
    INITIALIZED: 'initialized',
1✔
31
    COLOR_LAYERS_ORDER_CHANGED,
1✔
32
    CAMERA_MOVED: 'camera-moved',
1✔
33
    DISPOSED: 'disposed',
1✔
34
};
1✔
35

1✔
36
/**
1✔
37
 * Fired on current view's domElement when double right-clicking it. Copies all properties of the second right-click
1✔
38
 * MouseEvent (such as cursor position).
1✔
39
 * @event View#dblclick-right
1✔
40
 * @property {string} type  dblclick-right
1✔
41
 */
1✔
42

1✔
43
function _preprocessLayer(view, layer, parentLayer) {
88✔
44
    const source = layer.source;
88✔
45
    if (parentLayer && !layer.extent) {
88✔
46
        layer.extent = parentLayer.extent;
37✔
47
        if (source && !source.extent) {
37✔
48
            source.extent = parentLayer.extent;
32✔
49
        }
32✔
50
    }
37✔
51

88✔
52
    if (layer.isGeometryLayer && !layer.isLabelLayer) {
88✔
53
        // Find crs projection layer, this is projection destination
78✔
54
        layer.crs = view.referenceCrs;
78✔
55
    } else if (!layer.crs) {
88✔
56
        if (parentLayer && parentLayer.tileMatrixSets && parentLayer.tileMatrixSets.includes(source.crs)) {
4✔
57
            layer.crs = source.crs;
4✔
58
        } else {
4!
59
            layer.crs = parentLayer && parentLayer.extent.crs;
×
60
        }
×
61
    }
4✔
62

88✔
63
    if (layer.isLabelLayer) {
88✔
64
        view.mainLoop.gfxEngine.label2dRenderer.registerLayer(layer);
3✔
65
    } else if (layer.labelEnabled || layer.addLabelLayer) {
88✔
66
        if (layer.labelEnabled) {
3!
67
            // eslint-disable-next-line no-console
×
68
            console.info('layer.labelEnabled is deprecated use addLabelLayer, instead of');
×
69
        }
×
70
        // Because the features are shared between layer and labelLayer.
3✔
71
        layer.buildExtent = true;
3✔
72
        // label layer needs 3d data structure.
3✔
73
        layer.structure = '3d';
3✔
74
        const labelLayer = new LabelLayer(`${layer.id}-label`, {
3✔
75
            source,
3✔
76
            style: layer.style,
3✔
77
            zoom: layer.zoom,
3✔
78
            performance: layer.addLabelLayer.performance,
3✔
79
            crs: source.crs,
3✔
80
            visible: layer.visible,
3✔
81
            margin: 15,
3✔
82
            forceClampToTerrain: layer.addLabelLayer.forceClampToTerrain,
3✔
83
        });
3✔
84

3✔
85
        layer.addEventListener('visible-property-changed', () => {
3✔
86
            labelLayer.visible = layer.visible;
×
87
        });
3✔
88

3✔
89
        const removeLabelLayer = (e) => {
3✔
90
            if (e.layerId === layer.id) {
×
91
                view.removeLayer(labelLayer.id);
×
92
            }
×
93
            view.removeEventListener(VIEW_EVENTS.LAYER_REMOVED, removeLabelLayer);
×
94
        };
3✔
95

3✔
96
        view.addEventListener(VIEW_EVENTS.LAYER_REMOVED, removeLabelLayer);
3✔
97

3✔
98
        layer.whenReady = layer.whenReady.then(() => {
3✔
99
            view.addLayer(labelLayer);
3✔
100
            return layer;
3✔
101
        });
3✔
102
    }
3✔
103

88✔
104
    if (layer.isOGC3DTilesLayer) {
88!
105
        layer._setup(view);
×
106
    }
×
107

88✔
108
    return layer;
88✔
109
}
88✔
110
const _eventCoords = new THREE.Vector2();
1✔
111
const matrix = new THREE.Matrix4();
1✔
112
const screen = new THREE.Vector2();
1✔
113
const ray = new THREE.Ray();
1✔
114
const direction = new THREE.Vector3();
1✔
115
const positionVector = new THREE.Vector3();
1✔
116
const coordinates = new Coordinates('EPSG:4326');
1✔
117
const viewers = [];
1✔
118
// Size of the camera frustrum, in meters
1✔
119
let screenMeters;
1✔
120

1✔
121
let id = 0;
1✔
122

1✔
123
/**
1✔
124
 * @property {number} id - The id of the view. It's incremented at each new view instance, starting at 0.
1✔
125
 * @property {HTMLElement} domElement - The domElement holding the canvas where the view is displayed
1✔
126
 * @property {String} referenceCrs - The coordinate reference system of the view
1✔
127
 * @property {MainLoop} mainLoop - itowns mainloop scheduling the operations
1✔
128
 * @property {THREE.Scene} scene - threejs scene of the view
1✔
129
 * @property {Camera} camera - itowns camera (that holds a threejs camera that is directly accessible with View.camera3D)
1✔
130
 * @property {THREE.Camera} camera3D - threejs camera that is stored in itowns camera
1✔
131
 * @property {THREE.WebGLRenderer} renderer - threejs webglrenderer rendering this view
1✔
132
 */
1✔
133
class View extends THREE.EventDispatcher {
1✔
134
    #layers = [];
44✔
135
    #pixelDepthBuffer = new Uint8Array(4);
44✔
136
    #fullSizeDepthBuffer;
44✔
137
    /**
44✔
138
     * Constructs an Itowns View instance
44✔
139
     *
44✔
140
     * @example <caption><b>Create a view with a custom Three.js camera.</b></caption>
44✔
141
     * var viewerDiv = document.getElementById('viewerDiv');
44✔
142
     * var customCamera = itowns.THREE.PerspectiveCamera();
44✔
143
     * var view = itowns.View('EPSG:4326', viewerDiv, { camera: { cameraThree: customCamera } });
44✔
144
     *
44✔
145
     * @example <caption><b>Create a view with an orthographic camera, and grant it with Three.js custom controls.</b></caption>
44✔
146
     * var viewerDiv = document.getElementById('viewerDiv');
44✔
147
     * var view = itowns.View('EPSG:4326', viewerDiv, { camera: { type: itowns.CAMERA_TYPE.ORTHOGRAPHIC } });
44✔
148
     * var customControls = itowns.THREE.OrbitControls(view.camera3D, viewerDiv);
44✔
149
     *
44✔
150
     * @param {String} crs - The default CRS of Three.js coordinates. Should be a cartesian CRS.
44✔
151
     * @param {HTMLElement} viewerDiv - Where to instanciate the Three.js scene in the DOM
44✔
152
     * @param {Object} [options] - Optional properties.
44✔
153
     * @param {Object} [options.camera] - Options for the camera associated to the view. See {@link Camera} options.
44✔
154
     * @param {MainLoop} [options.mainLoop] - {@link MainLoop} instance to use, otherwise a default one will be constructed
44✔
155
     * @param {WebGLRenderer|Object} [options.renderer] - {@link WebGLRenderer} instance to use, otherwise
44✔
156
     * a default one will be constructed. In this case, if options.renderer is an object, it will be used to
44✔
157
     * configure the renderer (see {@link c3DEngine}.  If not present, a new &lt;canvas> will be created and
44✔
158
     * added to viewerDiv (mutually exclusive with mainLoop)
44✔
159
     * @param {Scene} [options.scene3D] - [THREE.Scene](https://threejs.org/docs/#api/en/scenes/Scene) instance to use, otherwise a default one will be constructed
44✔
160
     * @param {Color} [options.diffuse] - [THREE.Color](https://threejs.org/docs/?q=color#api/en/math/Color) Diffuse color terrain material.
44✔
161
     * This color is applied to terrain if there isn't color layer on terrain extent (by example on pole).
44✔
162
     * @param {boolean} [options.enableFocusOnStart=true] - enable focus on dom element on start.
44✔
163
     */
44✔
164
    constructor(crs, viewerDiv, options = {}) {
44!
165
        if (!viewerDiv) {
44!
166
            throw new Error('Invalid viewerDiv parameter (must non be null/undefined)');
×
167
        }
×
168

44✔
169
        super();
44✔
170

44✔
171
        this.domElement = viewerDiv;
44✔
172
        this.id = id++;
44✔
173

44✔
174
        this.referenceCrs = crs;
44✔
175

44✔
176
        let engine;
44✔
177
        // options.renderer can be 2 separate things:
44✔
178
        //   - an actual renderer (in this case we don't use viewerDiv)
44✔
179
        //   - options for the renderer to be created
44✔
180
        if (options.renderer && options.renderer.domElement) {
44✔
181
            engine = new c3DEngine(options.renderer);
44✔
182
        } else {
44!
183
            engine = new c3DEngine(viewerDiv, options.renderer);
×
184
        }
×
185

44✔
186
        this.mainLoop = options.mainLoop || new MainLoop(new Scheduler(), engine);
44✔
187

44✔
188
        this.scene = options.scene3D || new THREE.Scene();
44✔
189
        if (!options.scene3D) {
44✔
190
            this.scene.matrixWorldAutoUpdate = false;
44✔
191
        }
44✔
192

44✔
193
        this.camera = new Camera(
44✔
194
            this.referenceCrs,
44✔
195
            this.mainLoop.gfxEngine.getWindowSize().x,
44✔
196
            this.mainLoop.gfxEngine.getWindowSize().y,
44✔
197
            options.camera);
44✔
198

44✔
199
        this._frameRequesters = {};
44✔
200

44✔
201
        this._resizeListener = () => this.resize();
44✔
202
        window.addEventListener('resize', this._resizeListener, false);
44✔
203

44✔
204
        this._changeSources = new Set();
44✔
205

44✔
206
        if (__DEBUG__) {
44✔
207
            this.isDebugMode = true;
44✔
208
        }
44✔
209

44✔
210
        this._delayedFrameRequesterRemoval = [];
44✔
211

44✔
212
        this._allLayersAreReadyCallback = () => {
44✔
213
            // all layers must be ready
×
214
            const allReady = this.getLayers().every(layer => layer.ready);
×
215
            if (allReady &&
×
NEW
216
                this.mainLoop.scheduler.commandsWaitingExecutionCount() == 0 &&
×
NEW
217
                this.mainLoop.renderingState == RENDERING_PAUSED) {
×
218
                this.dispatchEvent({ type: VIEW_EVENTS.LAYERS_INITIALIZED });
×
219
                this.removeFrameRequester(MAIN_LOOP_EVENTS.UPDATE_END, this._allLayersAreReadyCallback);
×
220
            }
×
221
        };
44✔
222

44✔
223
        this.camera.resize(this.domElement.clientWidth, this.domElement.clientHeight);
44✔
224

44✔
225
        const fn = () => {
44✔
226
            this.removeEventListener(VIEW_EVENTS.LAYERS_INITIALIZED, fn);
×
227
            this.dispatchEvent({ type: VIEW_EVENTS.INITIALIZED });
×
228
        };
44✔
229

44✔
230
        this.addEventListener(VIEW_EVENTS.LAYERS_INITIALIZED, fn);
44✔
231

44✔
232
        this.#fullSizeDepthBuffer = new Uint8Array(4 * this.camera.width * this.camera.height);
44✔
233

44✔
234
        // Indicates that view's domElement can be focused (the negative value indicates that domElement can't be
44✔
235
        // focused sequentially using tab key). Focus is needed to capture some key events.
44✔
236
        this.domElement.tabIndex = -1;
44✔
237
        // Set focus on view's domElement.
44✔
238
        if (!options.disableFocusOnStart) {
44✔
239
            this.domElement.focus();
44✔
240
        }
44✔
241

44✔
242
        // Create a custom `dblclick-right` event that is triggered when double right-clicking
44✔
243
        let rightClickTimeStamp;
44✔
244
        this.domElement.addEventListener('mouseup', (event) => {
44✔
245
            if (event.button === 2) {  // If pressed mouse button is right button
×
246
                // If time between two right-clicks is bellow 500 ms, triggers a `dblclick-right` event
×
247
                if (rightClickTimeStamp && event.timeStamp - rightClickTimeStamp < 500) {
×
248
                    this.domElement.dispatchEvent(new MouseEvent('dblclick-right', event));
×
249
                }
×
250
                rightClickTimeStamp = event.timeStamp;
×
251
            }
×
252
        });
44✔
253

44✔
254

44✔
255
        // push all viewer to keep source.cache
44✔
256
        viewers.push(this);
44✔
257
    }
44✔
258

44✔
259
    /**
44✔
260
     * Get the Threejs renderer used to render this view.
44✔
261
     * @returns {THREE.WebGLRenderer} the WebGLRenderer used to render this view.
44✔
262
     */
44✔
263
    get renderer() {
44✔
264
        return this.mainLoop?.gfxEngine?.getRenderer();
7✔
265
    }
7✔
266

44✔
267
    /**
44✔
268
     * Get the threejs Camera of this view
44✔
269
     * @returns {THREE.Camera} the threejs camera of this view
44✔
270
     */
44✔
271
    get camera3D() {
44✔
272
        return this.camera?.camera3D;
752✔
273
    }
752✔
274

44✔
275
    /**
44✔
276
     * Dispose viewer before delete it.
44✔
277
     *
44✔
278
     * Method dispose all viewer objects
44✔
279
     * - remove control
44✔
280
     * - remove all layers
44✔
281
     * - remove all frame requester
44✔
282
     * - remove all events
44✔
283
     * @param {boolean} [clearCache=false] Whether to clear all the caches or not (layers cache, style cache, tilesCache)
44✔
284
     */
44✔
285
    dispose(clearCache = false) {
44!
286
        const id = viewers.indexOf(this);
1✔
287
        if (id == -1) {
1!
288
            console.warn('View already disposed');
×
289
            return;
×
290
        }
×
291

1✔
292
        window.removeEventListener('resize', this._resizeListener);
1✔
293

1✔
294
        // controls dispose
1✔
295
        if (this.controls) {
1!
296
            if (typeof this.controls.dispose === 'function') {
×
297
                this.controls.dispose();
×
298
            }
×
299
            delete this.controls;
×
300
        }
×
301
        // remove alls frameRequester
1✔
302
        this.removeAllFrameRequesters();
1✔
303
        // remove all layers
1✔
304
        const layers = this.getLayers(l => !l.isTiledGeometryLayer && !l.isAtmosphere);
1!
305
        for (const layer of layers) {
1!
306
            this.removeLayer(layer.id, clearCache);
×
307
        }
×
308
        const atmospheres = this.getLayers(l => l.isAtmosphere);
1✔
309
        for (const atmosphere of atmospheres) {
1!
310
            this.removeLayer(atmosphere.id, clearCache);
×
311
        }
×
312
        const tileLayers = this.getLayers(l => l.isTiledGeometryLayer);
1✔
313
        for (const tileLayer of tileLayers) {
1✔
314
            this.removeLayer(tileLayer.id, clearCache);
1✔
315
        }
1✔
316
        viewers.splice(id, 1);
1✔
317
        // Remove remaining objects in the scene (e.g. helpers, debug, etc.)
1✔
318
        this.scene.traverse(ObjectRemovalHelper.cleanup);
1✔
319
        this.dispatchEvent({ type: VIEW_EVENTS.DISPOSED });
1✔
320
        // remove alls events
1✔
321
        this.removeAllEvents();
1✔
322
    }
1✔
323

44✔
324
    /**
44✔
325
     * Add layer in viewer.
44✔
326
     * The layer id must be unique.
44✔
327
     *
44✔
328
     * The `layer.whenReady` is a promise that resolves when
44✔
329
     * the layer is done. This promise is also returned by
44✔
330
     * `addLayer` allowing to chain call.
44✔
331
     *
44✔
332
     * @param {LayerOptions|Layer|GeometryLayer} layer The layer to add in view.
44✔
333
     * @param {Layer=} parentLayer it's the layer to which the layer will be attached.
44✔
334
     * @return {Promise} a promise resolved with the new layer object when it is fully initialized or rejected if any error occurred.
44✔
335
     */
44✔
336
    addLayer(layer, parentLayer) {
44✔
337
        if (!layer || !layer.isLayer) {
88!
338
            return Promise.reject(new Error('Add Layer type object'));
×
339
        }
×
340
        const duplicate = this.getLayerById(layer.id);
88✔
341
        if (duplicate) {
88!
342
            return layer._reject(new Error(`Invalid id '${layer.id}': id already used`));
×
343
        }
×
344

88✔
345
        layer = _preprocessLayer(this, layer, parentLayer);
88✔
346

88✔
347
        if (parentLayer) {
88✔
348
            if (layer.isColorLayer) {
38✔
349
                const layerColors = this.getLayers(l => l.isColorLayer);
5✔
350
                layer.sequence = layerColors.length;
5✔
351

5✔
352
                const sumColorLayers = parentLayer.countColorLayersTextures(...layerColors, layer);
5✔
353

5✔
354
                if (sumColorLayers <= getMaxColorSamplerUnitsCount()) {
5✔
355
                    parentLayer.attach(layer);
5✔
356
                } else {
5!
357
                    return layer._reject(new Error(`Cant add color layer ${layer.id}: the maximum layer is reached`));
×
358
                }
×
359
            } else {
38✔
360
                parentLayer.attach(layer);
33✔
361
            }
33✔
362
        } else {
88✔
363
            if (typeof (layer.update) !== 'function') {
50!
364
                return layer._reject(new Error('Cant add GeometryLayer: missing a update function'));
×
365
            }
×
366
            if (typeof (layer.preUpdate) !== 'function') {
50!
367
                return layer._reject(new Error('Cant add GeometryLayer: missing a preUpdate function'));
×
368
            }
×
369

50✔
370
            this.#layers.push(layer);
50✔
371
        }
50✔
372

88✔
373
        if (layer.object3d && !layer.object3d.parent && layer.object3d !== this.scene) {
88✔
374
            this.scene.add(layer.object3d);
76✔
375
        }
76✔
376

88✔
377
        Promise.all(layer._promises).then(() => {
88✔
378
            layer._resolve();
88✔
379
            this.notifyChange(parentLayer || layer, false);
88✔
380
            if (!this._frameRequesters[MAIN_LOOP_EVENTS.UPDATE_END] ||
88✔
381
                !this._frameRequesters[MAIN_LOOP_EVENTS.UPDATE_END].includes(this._allLayersAreReadyCallback)) {
88✔
382
                this.addFrameRequester(MAIN_LOOP_EVENTS.UPDATE_END, this._allLayersAreReadyCallback);
36✔
383
            }
36✔
384
            this.dispatchEvent({
88✔
385
                type: VIEW_EVENTS.LAYER_ADDED,
88✔
386
                layerId: layer.id,
88✔
387
            });
88✔
388
        }, layer._reject);
88✔
389

88✔
390
        return layer.whenReady;
88✔
391
    }
88✔
392

44✔
393
    /**
44✔
394
     * Removes a specific imagery layer from the current layer list. This removes layers inserted with attach().
44✔
395
     * @example
44✔
396
     * view.removeLayer('layerId');
44✔
397
     * @param {string} layerId The identifier
44✔
398
     * @param {boolean} [clearCache=false] Whether to clear all the layer cache or not
44✔
399
     * @return {boolean}
44✔
400
     */
44✔
401
    removeLayer(layerId, clearCache) {
44✔
402
        const layer = this.getLayerById(layerId);
2✔
403
        if (layer) {
2✔
404
            const parentLayer = layer.parent;
2✔
405

2✔
406
            // Remove and dispose all nodes
2✔
407
            layer.delete(clearCache);
2✔
408

2✔
409
            // Detach layer if it's attached
2✔
410
            if (parentLayer && !parentLayer.detach(layer)) {
2!
411
                throw new Error(`Error to detach ${layerId} from ${parentLayer.id}`);
×
412
            } else if (parentLayer == undefined) {
2✔
413
                // Remove layer from viewer
2✔
414
                this.#layers.splice(this.#layers.findIndex(l => l.id == layerId), 1);
2✔
415
            }
2✔
416
            if (layer.isColorLayer) {
2!
417
                // Update color layers sequence
×
418
                const imageryLayers = this.getLayers(l => l.isColorLayer);
×
419
                for (const color of imageryLayers) {
×
420
                    if (color.sequence > layer.sequence) {
×
421
                        color.sequence--;
×
422
                    }
×
423
                }
×
424
            }
×
425

2✔
426
            // Remove unused cache in all viewers
2✔
427

2✔
428
            // count of times the source is used in all viewer
2✔
429
            let sharedSourceCount = 0;
2✔
430
            for (const view of viewers) {
2✔
431
                // add count of times the source is used in other layers
70✔
432
                sharedSourceCount += view.getLayers(l => l.source.uid == layer.source.uid && l.crs == layer.crs).length;
70✔
433
            }
70✔
434
            // if sharedSourceCount equals to 0 so remove unused cache for this CRS
2✔
435
            layer.source.onLayerRemoved({ unusedCrs: sharedSourceCount == 0 ? layer.crs : undefined });
2!
436

2✔
437
            this.notifyChange(this.camera);
2✔
438

2✔
439
            this.dispatchEvent({
2✔
440
                type: VIEW_EVENTS.LAYER_REMOVED,
2✔
441
                layerId,
2✔
442
            });
2✔
443

2✔
444
            return true;
2✔
445
        } else {
2!
446
            throw new Error(`${layerId} doesn't exist`);
×
447
        }
×
448
    }
2✔
449

44✔
450
    /**
44✔
451
     * Notifies the scene it needs to be updated due to changes exterior to the
44✔
452
     * scene itself (e.g. camera movement).
44✔
453
     * non-interactive events (e.g: texture loaded)
44✔
454
     * @param {*} changeSource
44✔
455
     * @param {boolean} needsRedraw - indicates if notified change requires a full scene redraw.
44✔
456
     */
44✔
457
    notifyChange(changeSource = undefined, needsRedraw = true) {
44✔
458
        if (changeSource) {
198✔
459
            this._changeSources.add(changeSource);
192✔
460
            if (!this.mainLoop.gfxEngine.renderer.xr.isPresenting
192✔
461
                && (changeSource.isTileMesh || changeSource.isCamera)) {
192✔
462
                this.#fullSizeDepthBuffer.needsUpdate = true;
90✔
463
            }
90✔
464
        }
192✔
465
        this.mainLoop.scheduleViewUpdate(this, needsRedraw);
198✔
466
    }
198✔
467

44✔
468
    /**
44✔
469
     * Get all layers, with an optionnal filter applied.
44✔
470
     * The filter method will be called with 2 args:
44✔
471
     *   - 1st: current layer
44✔
472
     *   - 2nd: (optional) the geometry layer to which the current layer is attached
44✔
473
     * @example
44✔
474
     * // get all layers
44✔
475
     * view.getLayers();
44✔
476
     * // get all color layers
44✔
477
     * view.getLayers(layer => layer.isColorLayer);
44✔
478
     * // get all elevation layers
44✔
479
     * view.getLayers(layer => layer.isElevationLayer);
44✔
480
     * // get all geometry layers
44✔
481
     * view.getLayers(layer => layer.isGeometryLayer);
44✔
482
     * // get one layer with id
44✔
483
     * view.getLayers(layer => layer.id === 'itt');
44✔
484
     * @param {function(Layer):boolean} filter
44✔
485
     * @returns {Array<Layer>}
44✔
486
     */
44✔
487
    getLayers(filter) {
44✔
488
        const result = [];
560✔
489
        for (const layer of this.#layers) {
560✔
490
            if (!filter || filter(layer)) {
566✔
491
                result.push(layer);
388✔
492
            }
388✔
493
            if (layer.attachedLayers) {
566✔
494
                for (const attached of layer.attachedLayers) {
566✔
495
                    if (!filter || filter(attached, layer)) {
333✔
496
                        result.push(attached);
11✔
497
                    }
11✔
498
                }
333✔
499
            }
566✔
500
        }
566✔
501
        return result;
560✔
502
    }
560✔
503

44✔
504
    /**
44✔
505
     * Gets the layer by identifier.
44✔
506
     *
44✔
507
     * @param {String}  layerId  The layer identifier
44✔
508
     * @return {Layer}  The layer by identifier.
44✔
509
     */
44✔
510

44✔
511
    getLayerById(layerId) {
44✔
512
        return this.getLayers(l => l.id === layerId)[0];
95✔
513
    }
95✔
514

44✔
515
    /**
44✔
516
     * @name FrameRequester
44✔
517
     * @function
44✔
518
     *
44✔
519
     * @description
44✔
520
     * Method that will be called each time the `MainLoop` updates. This function
44✔
521
     * will be given as parameter the delta (in ms) between this update and the
44✔
522
     * previous one, and whether or not we just started to render again. This update
44✔
523
     * is considered as the "next" update if `view.notifyChange` was called during a
44✔
524
     * precedent update. If `view.notifyChange` has been called by something else
44✔
525
     * (other micro/macrotask, UI events etc...), then this update is considered as
44✔
526
     * being the "first". It can also receive optional arguments, depending on the
44✔
527
     * attach point of this function. Currently only `BEFORE_LAYER_UPDATE /
44✔
528
     * AFTER_LAYER_UPDATE` attach points provide an additional argument: the layer
44✔
529
     * being updated.
44✔
530
     * <br><br>
44✔
531
     *
44✔
532
     * This means that if a `frameRequester` function wants to animate something, it
44✔
533
     * should keep on calling `view.notifyChange` until its task is done.
44✔
534
     * <br><br>
44✔
535
     *
44✔
536
     * Implementors of `frameRequester` should keep in mind that this function will
44✔
537
     * be potentially called at each frame, thus care should be given about
44✔
538
     * performance.
44✔
539
     * <br><br>
44✔
540
     *
44✔
541
     * Typical frameRequesters are controls, module wanting to animate moves or UI
44✔
542
     * elements etc... Basically anything that would want to call
44✔
543
     * requestAnimationFrame.
44✔
544
     *
44✔
545
     * @param {number} dt
44✔
546
     * @param {boolean} updateLoopRestarted
44✔
547
     * @param {...*} args
44✔
548
     */
44✔
549
    /**
44✔
550
     * Add a frame requester to this view.
44✔
551
     *
44✔
552
     * FrameRequesters can activate the MainLoop update by calling view.notifyChange.
44✔
553
     *
44✔
554
     * @param {String} when - decide when the frameRequester should be called during
44✔
555
     * the update cycle. Can be any of {@link MAIN_LOOP_EVENTS}.
44✔
556
     * @param {FrameRequester} frameRequester - this function will be called at each
44✔
557
     * MainLoop update with the time delta between last update, or 0 if the MainLoop
44✔
558
     * has just been relaunched.
44✔
559
     */
44✔
560
    addFrameRequester(when, frameRequester) {
44✔
561
        if (typeof frameRequester !== 'function') {
84!
562
            throw new Error('frameRequester must be a function');
×
563
        }
×
564

84✔
565
        if (!this._frameRequesters[when]) {
84✔
566
            this._frameRequesters[when] = [frameRequester];
70✔
567
        } else {
84✔
568
            this._frameRequesters[when].push(frameRequester);
14✔
569
        }
14✔
570
    }
84✔
571

44✔
572
    /**
44✔
573
     * Remove a frameRequester.
44✔
574
     * The effective removal will happen either later; at worst it'll be at
44✔
575
     * the beginning of the next frame.
44✔
576
     *
44✔
577
     * @param {String} when - attach point of this requester. Can be any of
44✔
578
     * {@link MAIN_LOOP_EVENTS}.
44✔
579
     * @param {FrameRequester} frameRequester
44✔
580
     */
44✔
581
    removeFrameRequester(when, frameRequester) {
44✔
582
        if (this._frameRequesters[when].includes(frameRequester)) {
16✔
583
            this._delayedFrameRequesterRemoval.push({ when, frameRequester });
16✔
584
        } else {
16!
585
            console.error('Invalid call to removeFrameRequester: frameRequester isn\'t registered');
×
586
        }
×
587
    }
16✔
588

44✔
589
    /**
44✔
590
     * Removes all frame requesters.
44✔
591
     */
44✔
592
    removeAllFrameRequesters() {
44✔
593
        for (const when in this._frameRequesters) {
1!
594
            if (Object.prototype.hasOwnProperty.call(this._frameRequesters, when)) {
×
595
                const frameRequesters = this._frameRequesters[when];
×
596
                for (const frameRequester of frameRequesters) {
×
597
                    this.removeFrameRequester(when, frameRequester);
×
598
                }
×
599
            }
×
600
        }
×
601
        this._executeFrameRequestersRemovals();
1✔
602
    }
1✔
603

44✔
604
    /**
44✔
605
     * Removes all viewer events.
44✔
606
     */
44✔
607
    removeAllEvents() {
44✔
608
        if (this._listeners === undefined) {
1!
609
            return;
×
610
        }
×
611

1✔
612
        for (const type in this._listeners) {
1✔
613
            if (Object.prototype.hasOwnProperty.call(this._listeners, type)) {
1✔
614
                delete this._listeners[type];
1✔
615
            }
1✔
616
        }
1✔
617

1✔
618
        this._listeners = undefined;
1✔
619
    }
1✔
620

44✔
621
    _executeFrameRequestersRemovals() {
44✔
622
        for (const toDelete of this._delayedFrameRequesterRemoval) {
3!
623
            const index = this._frameRequesters[toDelete.when].indexOf(toDelete.frameRequester);
×
624
            if (index >= 0) {
×
625
                this._frameRequesters[toDelete.when].splice(index, 1);
×
626
            } else {
×
627
                console.warn('FrameReq has already been removed');
×
628
            }
×
629
        }
×
630
        this._delayedFrameRequesterRemoval.length = 0;
3✔
631
    }
3✔
632

44✔
633
    /**
44✔
634
     * Execute a frameRequester.
44✔
635
     *
44✔
636
     * @param {String} when - attach point of this (these) requester(s). Can be any
44✔
637
     * of {@link MAIN_LOOP_EVENTS}.
44✔
638
     * @param {Number} dt - delta between this update and the previous one
44✔
639
     * @param {boolean} updateLoopRestarted
44✔
640
     * @param {...*} args - optional arguments
44✔
641
     */
44✔
642
    execFrameRequesters(when, dt, updateLoopRestarted, ...args) {
44!
643
        if (!this._frameRequesters[when]) {
1✔
644
            return;
1✔
645
        }
1✔
646

1✔
647
        if (this._delayedFrameRequesterRemoval.length > 0) {
1!
648
            this._executeFrameRequestersRemovals();
1✔
649
        }
1✔
650

1✔
651
        for (const frameRequester of this._frameRequesters[when]) {
1✔
652
            if (frameRequester.update) {
1!
653
                frameRequester.update(dt, updateLoopRestarted, args);
×
654
            } else {
1✔
655
                frameRequester(dt, updateLoopRestarted, args);
1✔
656
            }
1✔
657
        }
1✔
658
    }
1✔
659

44✔
660
    /**
44✔
661
     * Extract view coordinates from a mouse-event / touch-event
44✔
662
     * @param {event} event - event can be a MouseEvent or a TouchEvent
44✔
663
     * @param {THREE.Vector2} target - the target to set the view coords in
44✔
664
     * @param {number} [touchIdx=0] - finger index when using a TouchEvent
44✔
665
     * @return {THREE.Vector2|undefined} - view coordinates (in pixels, 0-0 = top-left of the View).
44✔
666
     * If the event is neither a `MouseEvent` nor a `TouchEvent`, the return is `undefined`.
44✔
667
     */
44✔
668
    eventToViewCoords(event, target = _eventCoords, touchIdx = 0) {
44!
669
        const br = this.domElement.getBoundingClientRect();
49✔
670

49✔
671
        if (event.touches && event.touches.length) {
49✔
672
            return target.set(event.touches[touchIdx].clientX - br.x,
30✔
673
                event.touches[touchIdx].clientY - br.y);
30✔
674
        } else if (event.offsetX !== undefined && event.offsetY !== undefined) {
49✔
675
            const targetBoundingRect = event.target.getBoundingClientRect();
19✔
676
            return target.set(targetBoundingRect.x + event.offsetX - br.x,
19✔
677
                targetBoundingRect.y + event.offsetY - br.y);
19✔
678
        }
19✔
679
    }
49✔
680

44✔
681
    /**
44✔
682
     * Extract normalized coordinates (NDC) from a mouse-event / touch-event
44✔
683
     * @param {event} event - event can be a MouseEvent or a TouchEvent
44✔
684
     * @param {number} touchIdx - finger index when using a TouchEvent (default: 0)
44✔
685
     * @return {THREE.Vector2} - NDC coordinates (x and y are [-1, 1])
44✔
686
     */
44✔
687
    eventToNormalizedCoords(event, touchIdx = 0) {
44✔
688
        return this.viewToNormalizedCoords(this.eventToViewCoords(event, _eventCoords, touchIdx));
×
689
    }
×
690

44✔
691
    /**
44✔
692
     * Convert view coordinates to normalized coordinates (NDC)
44✔
693
     * @param {THREE.Vector2} viewCoords (in pixels, 0-0 = top-left of the View)
44✔
694
     * @param {THREE.Vector2} target
44✔
695
     * @return {THREE.Vector2} - NDC coordinates (x and y are [-1, 1])
44✔
696
     */
44✔
697
    viewToNormalizedCoords(viewCoords, target = _eventCoords) {
44✔
698
        target.x = 2 * (viewCoords.x / this.camera.width) - 1;
5✔
699
        target.y = -2 * (viewCoords.y / this.camera.height) + 1;
5✔
700
        return target;
5✔
701
    }
5✔
702

44✔
703
    /**
44✔
704
     * Convert NDC coordinates to view coordinates
44✔
705
     * @param {THREE.Vector2} ndcCoords
44✔
706
     * @return {THREE.Vector2} - view coordinates (in pixels, 0-0 = top-left of the View)
44✔
707
     */
44✔
708
    normalizedToViewCoords(ndcCoords) {
44✔
709
        _eventCoords.x = (ndcCoords.x + 1) * 0.5 * this.camera.width;
×
710
        _eventCoords.y = (ndcCoords.y - 1) * -0.5 * this.camera.height;
×
711
        return _eventCoords;
×
712
    }
×
713

44✔
714
    /**
44✔
715
     * Searches for objects in {@link GeometryLayer} and specified
44✔
716
     * `THREE.Object3D`, under the mouse or at a specified coordinates, in this
44✔
717
     * view.
44✔
718
     *
44✔
719
     * @param {Object} mouseOrEvt - Mouse position in window coordinates (from
44✔
720
     * the top left corner of the window) or `MouseEvent` or `TouchEvent`.
44✔
721
     * @param {number} [radius=0] - The picking will happen in a circle centered
44✔
722
     * on mouseOrEvt. This is the radius of this circle, in pixels.
44✔
723
     * @param {GeometryLayer|string|Object3D|Array<GeometryLayer|string|Object3D>} [where] - Where to look for
44✔
724
     * objects. It can be a single {@link GeometryLayer}, `THREE.Object3D`, ID of a layer or an array of one of these or
44✔
725
     * of a mix of these. If no location is specified, it will query on all {@link GeometryLayer} present in this `View`.
44✔
726
     *
44✔
727
     * @return {Object[]} - An array of objects. Each element contains at least
44✔
728
     * an object property which is the `THREE.Object3D` under the cursor. Then
44✔
729
     * depending on the queried layer/source, there may be additionnal
44✔
730
     * properties (coming from `THREE.Raycaster` for instance).
44✔
731
     *
44✔
732
     * @example
44✔
733
     * view.pickObjectsAt({ x, y })
44✔
734
     * view.pickObjectsAt({ x, y }, 1, 'wfsBuilding')
44✔
735
     * view.pickObjectsAt({ x, y }, 3, 'wfsBuilding', myLayer)
44✔
736
     */
44✔
737
    pickObjectsAt(mouseOrEvt, radius = 0, where) {
44!
738
        const sources = [];
1✔
739

1✔
740
        if (!where || where.length === 0) {
1!
741
            where = this.getLayers(l => l.isGeometryLayer);
1✔
742
        }
1✔
743
        if (!Array.isArray(where)) {
1!
744
            where = [where];
×
745
        }
×
746
        where.forEach((l) => {
1✔
747
            if (typeof l === 'string') {
1!
748
                l = this.getLayerById(l);
×
749
            }
×
750

1✔
751
            if (l && (l.isGeometryLayer || l.isObject3D)) {
1!
752
                sources.push(l);
1✔
753
            }
1✔
754
        });
1✔
755

1✔
756
        if (sources.length == 0) {
1!
757
            return [];
×
758
        }
×
759

1✔
760
        const results = [];
1✔
761
        const mouse = (mouseOrEvt instanceof Event) ? this.eventToViewCoords(mouseOrEvt) : mouseOrEvt;
1!
762

1✔
763
        for (const source of sources) {
1✔
764
            if (source.isAtmosphere) {
1!
765
                continue;
×
766
            }
×
767
            if (source.isGeometryLayer) {
1✔
768
                if (!source.ready) {
1!
769
                    console.warn('view.pickObjectAt : layer is not ready : ', source);
×
770
                    continue;
×
771
                }
×
772

1✔
773
                source.pickObjectsAt(this, mouse, radius, results);
1✔
774
            } else {
1!
775
                Picking.pickObjectsAt(this, mouse, radius, source, results);
×
776
            }
×
777
        }
1✔
778

1✔
779
        return results;
1✔
780
    }
1✔
781

44✔
782
    /**
44✔
783
     * Return the current zoom scale at the central point of the view. This
44✔
784
     * function compute the scale of a map.
44✔
785
     *
44✔
786
     * @param {number} pitch - Screen pitch, in millimeters ; 0.28 by default
44✔
787
     *
44✔
788
     * @return {number} The zoom scale.
44✔
789
     */
44✔
790
    getScale(pitch = 0.28) {
44!
791
        if (this.camera3D.isOrthographicCamera) {
2!
792
            return pitch * 1E-3 / this.getPixelsToMeters();
×
793
        }
×
794
        return this.getScaleFromDistance(pitch, this.getDistanceFromCamera());
2✔
795
    }
2✔
796

44✔
797
    getScaleFromDistance(pitch = 0.28, distance = 1) {
44!
798
        pitch /= 1000;
3✔
799
        const fov = THREE.MathUtils.degToRad(this.camera3D.fov);
3✔
800
        const unit = this.camera.height / (2 * distance * Math.tan(fov * 0.5));
3✔
801
        return pitch * unit;
3✔
802
    }
3✔
803

44✔
804
    /**
44✔
805
     * Given a screen coordinates, get the distance between the projected
44✔
806
     * coordinates and the camera associated to this view.
44✔
807
     *
44✔
808
     * @param {THREE.Vector2} [screenCoord] - The screen coordinate to get the
44✔
809
     * distance at. By default this is the middle of the screen.
44✔
810
     *
44✔
811
     * @return {number} The distance in meters.
44✔
812
     */
44✔
813
    getDistanceFromCamera(screenCoord) {
44✔
814
        this.getPickingPositionFromDepth(screenCoord, positionVector);
7✔
815
        return this.camera3D.position.distanceTo(positionVector);
7✔
816
    }
7✔
817

44✔
818
    /**
44✔
819
     * Get, for a specific screen coordinate, the projected distance on the
44✔
820
     * surface of the main layer of the view.
44✔
821
     *
44✔
822
     * @param {number} [pixels=1] - The size, in pixels, to get in meters.
44✔
823
     * @param {THREE.Vector2} [screenCoord] - The screen coordinate to get the
44✔
824
     * projected distance at. By default, this is the middle of the screen.
44✔
825
     *
44✔
826
     * @return {number} The projected distance in meters.
44✔
827
     */
44✔
828
    getPixelsToMeters(pixels = 1, screenCoord) {
44!
829
        if (this.camera3D.isOrthographicCamera) {
7✔
830
            screenMeters = (this.camera3D.right - this.camera3D.left) / this.camera3D.zoom;
4✔
831
            return pixels * screenMeters / this.camera.width;
4✔
832
        }
4✔
833
        return this.getPixelsToMetersFromDistance(pixels, this.getDistanceFromCamera(screenCoord));
3✔
834
    }
3✔
835

44✔
836
    getPixelsToMetersFromDistance(pixels = 1, distance = 1) {
44!
837
        return pixels * distance / this.camera._preSSE;
3✔
838
    }
3✔
839

44✔
840
    /**
44✔
841
     * Get, for a specific screen coordinate, the size in pixels of a projected
44✔
842
     * distance on the surface of the main layer of the view.
44✔
843
     *
44✔
844
     * @param {number} [meters=1] - The size, in meters, to get in pixels.
44✔
845
     * @param {THREE.Vector2} [screenCoord] - The screen coordinate to get the
44✔
846
     * projected distance at. By default, this is the middle of the screen.
44✔
847
     *
44✔
848
     * @return {number} The projected distance in pixels.
44✔
849
     */
44✔
850
    getMetersToPixels(meters = 1, screenCoord) {
44!
851
        if (this.camera3D.isOrthographicCamera) {
1!
852
            screenMeters = (this.camera3D.right - this.camera3D.left) / this.camera3D.zoom;
×
853
            return meters * this.camera.width / screenMeters;
×
854
        }
×
855
        return this.getMetersToPixelsFromDistance(meters, this.getDistanceFromCamera(screenCoord));
1✔
856
    }
1✔
857

44✔
858
    getMetersToPixelsFromDistance(meters = 1, distance = 1) {
44!
859
        return this.camera._preSSE * meters / distance;
1✔
860
    }
1✔
861

44✔
862
    /**
44✔
863
     * Searches for {@link FeatureGeometry} in {@link ColorLayer}, under the mouse or at
44✔
864
     * the specified coordinates, in this view. Combining them per layer and in a Feature
44✔
865
     * like format.
44✔
866
     *
44✔
867
     * @param {Object} mouseOrEvt - Mouse position in window coordinates (from
44✔
868
     * the top left corner of the window) or `MouseEvent` or `TouchEvent`.
44✔
869
     * @param {number} [radius=3] - The picking will happen in a circle centered
44✔
870
     * on mouseOrEvt. This is the radius of this circle, in pixels.
44✔
871
     * @param {...ColorLayer|GeometryLayer|string} [where] - The layers to look
44✔
872
     * into. If not specified, all {@link ColorLayer} and {@link GeometryLayer}
44✔
873
     * layers of this view will be looked in.
44✔
874
     *
44✔
875
     * @return {Object} - An object, having one property per layer.
44✔
876
     * For example, looking for features on layers `wfsBuilding` and `wfsRoads`
44✔
877
     * will give an object like `{ wfsBuilding: [...], wfsRoads: [] }`.
44✔
878
     * Each property is made of an array, that can be empty or filled with
44✔
879
     * Feature like objects composed of:
44✔
880
     * - the FeatureGeometry
44✔
881
     * - the feature type
44✔
882
     * - the style
44✔
883
     * - the coordinate if the FeatureGeometry is a point
44✔
884
     *
44✔
885
     * @example
44✔
886
     * view.pickFeaturesAt({ x, y });
44✔
887
     * view.pickFeaturesAt({ x, y }, 1, 'wfsBuilding');
44✔
888
     * view.pickFeaturesAt({ x, y }, 3, 'wfsBuilding', myLayer);
44✔
889
     */
44✔
890
    pickFeaturesAt(mouseOrEvt, radius = 3, ...where) {
44✔
891
        if (Array.isArray(where[0])) {
×
892
            console.warn('Deprecated: the ...where argument of View#pickFeaturesAt should not be an array anymore, but a list: use the spread operator if needed.');
×
893
            where = where[0];
×
894
        }
×
895

×
896
        const layers = [];
×
897
        const result = {};
×
898

×
899
        where = where.length == 0 ? this.getLayers(l => l.isColorLayer || l.isGeometryLayer) : where;
×
900
        where.forEach((l) => {
×
901
            if (typeof l === 'string') {
×
902
                l = this.getLayerById(l);
×
903
            }
×
904

×
905
            if (l && l.isLayer) {
×
906
                result[l.id] = [];
×
907
                if (l.isColorLayer) { layers.push(l.id); }
×
908
            }
×
909
        });
×
910

×
911
        // Get the mouse coordinates to the correct system
×
912
        const mouse = (mouseOrEvt instanceof Event) ? this.eventToViewCoords(mouseOrEvt, _eventCoords) : mouseOrEvt;
×
913
        const objects = this.pickObjectsAt(mouse, radius, ...where);
×
914

×
915
        if (objects.length > 0) {
×
916
            objects.forEach(o => result[o.layer.id].push(o));
×
917
        }
×
918

×
919
        if (layers.length == 0) {
×
920
            return result;
×
921
        }
×
922

×
923
        this.getPickingPositionFromDepth(mouse, positionVector);
×
924
        coordinates.crs = this.referenceCrs;
×
925
        coordinates.setFromVector3(positionVector);
×
926

×
927
        // Get the correct precision; the position variable will be set in this
×
928
        // function.
×
929
        let precision;
×
930
        const precisions = {
×
931
            M: this.getPixelsToMeters(radius, mouse),
×
932
            D: 0.001 * radius,
×
933
        };
×
934

×
935
        if (this.isPlanarView) {
×
936
            precisions.D = precisions.M;
×
937
        } else if (this.getPixelsToDegrees) {
×
938
            precisions.D = this.getMetersToDegrees(precisions.M);
×
939
        }
×
940

×
941
        // Get the tile corresponding to where the cursor is
×
942
        const tiles = Picking.pickTilesAt(this, mouse, radius, this.tileLayer);
×
943

×
944
        for (const tile of tiles) {
×
945
            if (!tile.object.material) {
×
946
                continue;
×
947
            }
×
948

×
NEW
949
            for (const materialLayer of tile.object.material.getTiles(layers)) {
×
950
                for (const texture of materialLayer.textures) {
×
951
                    if (!texture.features) {
×
952
                        continue;
×
953
                    }
×
954

×
955
                    precision = CRS.isMetricUnit(texture.features.crs) ? precisions.M : precisions.D;
×
956

×
957
                    const featuresUnderCoor = FeaturesUtils.filterFeaturesUnderCoordinate(coordinates, texture.features, precision);
×
958
                    featuresUnderCoor.forEach((feature) => {
×
959
                        if (!result[materialLayer.id].find(f => f.geometry === feature.geometry)) {
×
960
                            result[materialLayer.id].push(feature);
×
961
                        }
×
962
                    });
×
963
                }
×
964
            }
×
965
        }
×
966

×
967
        return result;
×
968
    }
×
969

44✔
970
    readDepthBuffer(x, y, width, height, buffer) {
44✔
971
        const g = this.mainLoop.gfxEngine;
83✔
972
        const currentWireframe = this.tileLayer.wireframe;
83✔
973
        const currentOpacity = this.tileLayer.opacity;
83✔
974
        const currentVisibility = this.tileLayer.visible;
83✔
975
        if (currentWireframe) {
83!
976
            this.tileLayer.wireframe = false;
×
977
        }
×
978
        if (currentOpacity < 1.0) {
83!
979
            this.tileLayer.opacity = 1.0;
×
980
        }
×
981
        if (!currentVisibility) {
83✔
982
            this.tileLayer.visible = true;
1✔
983
        }
1✔
984

83✔
985
        const restore = this.tileLayer.level0Nodes.map(n => RenderMode.push(n, RenderMode.MODES.DEPTH));
83✔
986
        buffer = g.renderViewToBuffer(
83✔
987
            { camera: this.camera, scene: this.tileLayer.object3d },
83✔
988
            { x, y, width, height, buffer });
83✔
989
        restore.forEach(r => r());
83✔
990

83✔
991
        if (this.tileLayer.wireframe !== currentWireframe) {
83!
992
            this.tileLayer.wireframe = currentWireframe;
×
993
        }
×
994
        if (this.tileLayer.opacity !== currentOpacity) {
83!
995
            this.tileLayer.opacity = currentOpacity;
×
996
        }
×
997
        if (this.tileLayer.visible !== currentVisibility) {
83✔
998
            this.tileLayer.visible = currentVisibility;
1✔
999
        }
1✔
1000

83✔
1001
        return buffer;
83✔
1002
    }
83✔
1003

44✔
1004
    /**
44✔
1005
     * Returns the world position on the terrain (view's crs: referenceCrs) under view coordinates.
44✔
1006
     * This position is computed with depth buffer.
44✔
1007
     *
44✔
1008
     * @param      {THREE.Vector2}  mouse  position in view coordinates (in pixel), if it's null so it's view's center.
44✔
1009
     * @param      {THREE.Vector3}  [target=THREE.Vector3()] target. the result will be copied into this Vector3. If not present a new one will be created.
44✔
1010
     * @return     {THREE.Vector3}  the world position on the terrain in view's crs: referenceCrs.
44✔
1011
     */
44✔
1012

44✔
1013
    getPickingPositionFromDepth(mouse, target = new THREE.Vector3()) {
44✔
1014
        if (!this.tileLayer || this.tileLayer.level0Nodes.length == 0 || (!this.tileLayer.level0Nodes[0])) {
129✔
1015
            target = undefined;
46✔
1016
            return;
46✔
1017
        }
46✔
1018
        const l = this.mainLoop;
83✔
1019
        const viewPaused = l.scheduler.commandsWaitingExecutionCount() == 0 && l.renderingState == RENDERING_PAUSED;
129✔
1020
        const g = l.gfxEngine;
129✔
1021
        const dim = g.getWindowSize();
129✔
1022

129✔
1023
        mouse = mouse || dim.clone().multiplyScalar(0.5);
129✔
1024
        mouse.x = Math.floor(mouse.x);
129✔
1025
        mouse.y = Math.floor(mouse.y);
129✔
1026

129✔
1027
        // Render/Read to buffer
129✔
1028
        let buffer;
129✔
1029
        if (viewPaused) {
129✔
1030
            if (this.#fullSizeDepthBuffer.needsUpdate) {
1!
1031
                this.readDepthBuffer(0, 0, dim.x, dim.y, this.#fullSizeDepthBuffer);
×
1032
                this.#fullSizeDepthBuffer.needsUpdate = false;
×
1033
            }
×
1034
            const id = ((dim.y - mouse.y - 1) * dim.x + mouse.x) * 4;
1✔
1035
            buffer = this.#fullSizeDepthBuffer.slice(id, id + 4);
1✔
1036
        } else {
129✔
1037
            buffer = this.readDepthBuffer(mouse.x, mouse.y, 1, 1, this.#pixelDepthBuffer);
82✔
1038
        }
82✔
1039

83✔
1040
        screen.x = (mouse.x / dim.x) * 2 - 1;
83✔
1041
        screen.y = -(mouse.y / dim.y) * 2 + 1;
83✔
1042

83✔
1043
        if (Capabilities.isLogDepthBufferSupported() && this.camera3D.type == 'PerspectiveCamera') {
129✔
1044
            // TODO: solve this part with gl_FragCoord_Z and unproject
79✔
1045
            // Origin
79✔
1046
            ray.origin.copy(this.camera3D.position);
79✔
1047

79✔
1048
            // Direction
79✔
1049
            ray.direction.set(screen.x, screen.y, 0.5);
79✔
1050
            // Unproject
79✔
1051
            matrix.multiplyMatrices(this.camera3D.matrixWorld, matrix.copy(this.camera3D.projectionMatrix).invert());
79✔
1052
            ray.direction.applyMatrix4(matrix);
79✔
1053
            ray.direction.sub(ray.origin);
79✔
1054

79✔
1055
            direction.set(0, 0, 1.0);
79✔
1056
            direction.applyMatrix4(matrix);
79✔
1057
            direction.sub(ray.origin);
79✔
1058

79✔
1059
            const angle = direction.angleTo(ray.direction);
79✔
1060
            const orthoZ = g.depthBufferRGBAValueToOrthoZ(buffer, this.camera3D);
79✔
1061
            const length = orthoZ / Math.cos(angle);
79✔
1062
            target.addVectors(this.camera3D.position, ray.direction.setLength(length));
79✔
1063
        } else {
129✔
1064
            const gl_FragCoord_Z = g.depthBufferRGBAValueToOrthoZ(buffer, this.camera3D);
4✔
1065

4✔
1066
            target.set(screen.x, screen.y, gl_FragCoord_Z);
4✔
1067
            target.unproject(this.camera3D);
4✔
1068
        }
4✔
1069

83✔
1070
        if (target.length() > 10000000) { return undefined; }
129!
1071

83✔
1072
        return target;
83✔
1073
    }
83✔
1074

44✔
1075
    /**
44✔
1076
     * Returns the world {@link Coordinates} of the terrain at given view coordinates.
44✔
1077
     *
44✔
1078
     * @param {THREE.Vector2|event} [mouse] The view coordinates at which the world coordinates must be returned. This
44✔
1079
     * parameter can also be set to a mouse event from which the view coordinates will be deducted. If not specified,
44✔
1080
     * it will be defaulted to the view's center coordinates.
44✔
1081
     * @param {Coordinates} [target] The result will be copied into this {@link Coordinates} in the coordinate reference
44✔
1082
     * system of the given coordinate. If not specified, a new {@link Coordinates} instance will be created (in the
44✔
1083
     * view referenceCrs).
44✔
1084
     *
44✔
1085
     * @returns {Coordinates}   The world {@link Coordinates} of the terrain at the given view coordinates in the
44✔
1086
     * coordinate reference system of the target or in the view referenceCrs if no target is specified.
44✔
1087
     */
44✔
1088
    pickTerrainCoordinates(mouse, target = new Coordinates(this.referenceCrs)) {
44✔
1089
        if (mouse instanceof Event) {
×
1090
            this.eventToViewCoords(mouse);
×
1091
        } else if (mouse && mouse.x !== undefined && mouse.y !== undefined) {
×
1092
            _eventCoords.copy(mouse);
×
1093
        } else {
×
1094
            _eventCoords.set(
×
1095
                this.mainLoop.gfxEngine.width / 2,
×
1096
                this.mainLoop.gfxEngine.height / 2,
×
1097
            );
×
1098
        }
×
1099

×
1100
        this.getPickingPositionFromDepth(_eventCoords, positionVector);
×
1101
        coordinates.crs = this.referenceCrs;
×
1102
        coordinates.setFromVector3(positionVector);
×
1103
        coordinates.as(target.crs, target);
×
1104

×
1105
        return target;
×
1106
    }
×
1107

44✔
1108
    /**
44✔
1109
     * Returns the world {@link Coordinates} of the terrain at given view coordinates.
44✔
1110
     *
44✔
1111
     * @param   {THREE.Vector2|event}   [mouse]     The view coordinates at which the world coordinates must be
44✔
1112
                                                    * returned. This parameter can also be set to a mouse event from
44✔
1113
                                                    * which the view coordinates will be deducted. If not specified, it
44✔
1114
                                                    * will be defaulted to the view's center coordinates.
44✔
1115
     * @param   {Coordinates}           [target]    The result will be copied into this {@link Coordinates}. If not
44✔
1116
                                                    * specified, a new {@link Coordinates} instance will be created.
44✔
1117
     *
44✔
1118
     * @returns {Coordinates}   The world {@link Coordinates} of the terrain at the given view coordinates.
44✔
1119
     *
44✔
1120
     * @deprecated Use View#pickTerrainCoordinates instead.
44✔
1121
     */
44✔
1122
    pickCoordinates(mouse, target = new Coordinates(this.referenceCrs)) {
44✔
1123
        console.warn('Deprecated, use View#pickTerrainCoordinates instead.');
×
1124
        return this.pickTerrainCoordinates(mouse, target);
×
1125
    }
×
1126

44✔
1127
    /**
44✔
1128
     * Resize the viewer.
44✔
1129
     *
44✔
1130
     * @param {number} [width=viewerDiv.clientWidth] - The width to resize the
44✔
1131
     * viewer with. By default it is the `clientWidth` of the `viewerDiv`.
44✔
1132
     * @param {number} [height=viewerDiv.clientHeight] - The height to resize
44✔
1133
     * the viewer with. By default it is the `clientHeight` of the `viewerDiv`.
44✔
1134
     */
44✔
1135
    resize(width, height) {
44✔
1136
        if (width < 0 || height < 0) {
1!
1137
            console.warn(`Trying to resize the View with negative height (${height}) or width (${width}). Skipping resize.`);
×
1138
            return;
×
1139
        }
×
1140

1✔
1141
        if (width == undefined) {
1!
1142
            width = this.domElement.clientWidth;
×
1143
        }
×
1144

1✔
1145
        if (height == undefined) {
1!
1146
            height = this.domElement.clientHeight;
×
1147
        }
×
1148

1✔
1149
        this.#fullSizeDepthBuffer = new Uint8Array(4 * width * height);
1✔
1150
        this.mainLoop.gfxEngine.onWindowResize(width, height);
1✔
1151
        if (width !== 0 && height !== 0) {
1✔
1152
            this.camera.resize(width, height);
1✔
1153
            this.notifyChange(this.camera3D);
1✔
1154
        }
1✔
1155
    }
1✔
1156
}
44✔
1157

1✔
1158
export default View;
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