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

iTowns / itowns / 11341675326

15 Oct 2024 07:53AM UTC coverage: 86.851% (-0.1%) from 86.948%
11341675326

Pull #2435

github

web-flow
Merge 93963e8fd into cfb9d0f51
Pull Request #2435: fix(OGC3DTilesLayer): handle multiple views

2794 of 3710 branches covered (75.31%)

Branch coverage included in aggregate %.

36 of 41 new or added lines in 2 files covered. (87.8%)

148 existing lines in 3 files now uncovered.

24366 of 27562 relevant lines covered (88.4%)

1022.16 hits per line

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

86.8
/src/Controls/GlobeControls.js
1
import * as THREE from 'three';
1✔
2
import AnimationPlayer from 'Core/AnimationPlayer';
1✔
3
import Coordinates from 'Core/Geographic/Coordinates';
1✔
4
import { ellipsoidSizes } from 'Core/Math/Ellipsoid';
1✔
5
import CameraUtils from 'Utils/CameraUtils';
1✔
6
import StateControl from 'Controls/StateControl';
1✔
7
import { VIEW_EVENTS } from 'Core/View';
1✔
8

1✔
9
// private members
1✔
10
const EPS = 0.000001;
1✔
11

1✔
12
const direction = {
1✔
13
    up: new THREE.Vector2(0, 1),
1✔
14
    bottom: new THREE.Vector2(0, -1),
1✔
15
    left: new THREE.Vector2(1, 0),
1✔
16
    right: new THREE.Vector2(-1, 0),
1✔
17
};
1✔
18

1✔
19
// Orbit
1✔
20
const rotateStart = new THREE.Vector2();
1✔
21
const rotateEnd = new THREE.Vector2();
1✔
22
const rotateDelta = new THREE.Vector2();
1✔
23
const spherical = new THREE.Spherical(1.0, 0.01, 0);
1✔
24
const sphericalDelta = new THREE.Spherical(1.0, 0, 0);
1✔
25
let orbitScale = 1.0;
1✔
26

1✔
27
// Pan
1✔
28
const panStart = new THREE.Vector2();
1✔
29
const panEnd = new THREE.Vector2();
1✔
30
const panDelta = new THREE.Vector2();
1✔
31
const panOffset = new THREE.Vector3();
1✔
32

1✔
33
// Dolly
1✔
34
const dollyStart = new THREE.Vector2();
1✔
35
const dollyEnd = new THREE.Vector2();
1✔
36
const dollyDelta = new THREE.Vector2();
1✔
37
let dollyScale;
1✔
38

1✔
39
// Globe move
1✔
40
const moveAroundGlobe = new THREE.Quaternion();
1✔
41
const cameraTarget = new THREE.Object3D();
1✔
42
const coordCameraTarget = new Coordinates('EPSG:4978');
1✔
43
cameraTarget.matrixWorldInverse = new THREE.Matrix4();
1✔
44

1✔
45
const xyz = new Coordinates('EPSG:4978', 0, 0, 0);
1✔
46
const c = new Coordinates('EPSG:4326', 0, 0, 0);
1✔
47
// Position object on globe
1✔
48
function positionObject(newPosition, object) {
28✔
49
    xyz.setFromVector3(newPosition).as('EPSG:4326', c);
28✔
50
    object.position.copy(newPosition);
28✔
51
    object.lookAt(c.geodesicNormal.add(newPosition));
28✔
52
    object.rotateX(Math.PI * 0.5);
28✔
53
    object.updateMatrixWorld(true);
28✔
54
}
28✔
55

1✔
56
// Save the last time of mouse move for damping
1✔
57
let lastTimeMouseMove = 0;
1✔
58

1✔
59
// Animations and damping
1✔
60
let enableAnimation = true;
1✔
61
const dampingFactorDefault = 0.25;
1✔
62
const dampingMove = new THREE.Quaternion(0, 0, 0, 1);
1✔
63
const durationDampingMove = 120;
1✔
64
const durationDampingOrbital = 60;
1✔
65

1✔
66
// Pan Move
1✔
67
const panVector = new THREE.Vector3();
1✔
68

1✔
69
// Save last transformation
1✔
70
const lastPosition = new THREE.Vector3();
1✔
71
const lastQuaternion = new THREE.Quaternion();
1✔
72

1✔
73
// Tangent sphere to ellipsoid
1✔
74
const pickSphere = new THREE.Sphere();
1✔
75
const pickingPoint = new THREE.Vector3();
1✔
76

1✔
77
// Sphere intersection
1✔
78
const intersection = new THREE.Vector3();
1✔
79

1✔
80
// Set to true to enable target helper
1✔
81
const enableTargetHelper = false;
1✔
82
const helpers = {};
1✔
83

1✔
84
if (enableTargetHelper) {
1✔
85
    helpers.picking = new THREE.AxesHelper(500000);
1✔
86
    helpers.target = new THREE.AxesHelper(500000);
1✔
87
}
1✔
88

1✔
89
/**
1✔
90
 * Globe control pan event. Fires after camera pan
1✔
91
 * @event GlobeControls#pan-changed
1✔
92
 * @property target {GlobeControls} dispatched on controls
1✔
93
 * @property type {string} orientation-changed
1✔
94
 */
1✔
95
/**
1✔
96
 * Globe control orientation event. Fires when camera's orientation change
1✔
97
 * @event GlobeControls#orientation-changed
1✔
98
 * @property new {object}
1✔
99
 * @property new.tilt {number} the new value of the tilt of the camera
1✔
100
 * @property new.heading {number} the new value of the heading of the camera
1✔
101
 * @property previous {object}
1✔
102
 * @property previous.tilt {number} the previous value of the tilt of the camera
1✔
103
 * @property previous.heading {number} the previous value of the heading of the camera
1✔
104
 * @property target {GlobeControls} dispatched on controls
1✔
105
 * @property type {string} orientation-changed
1✔
106
 */
1✔
107
/**
1✔
108
 * Globe control range event. Fires when camera's range to target change
1✔
109
 * @event GlobeControls#range-changed
1✔
110
 * @property new {number} the new value of the range
1✔
111
 * @property previous {number} the previous value of the range
1✔
112
 * @property target {GlobeControls} dispatched on controls
1✔
113
 * @property type {string} range-changed
1✔
114
 */
1✔
115
/**
1✔
116
 * Globe control camera's target event. Fires when camera's target change
1✔
117
 * @event GlobeControls#camera-target-changed
1✔
118
 * @property new {object}
1✔
119
 * @property new {Coordinates} the new camera's target coordinates
1✔
120
 * @property previous {Coordinates} the previous camera's target coordinates
1✔
121
 * @property target {GlobeControls} dispatched on controls
1✔
122
 * @property type {string} camera-target-changed
1✔
123
 */
1✔
124

1✔
125
/**
1✔
126
 * globe controls events
1✔
127
 * @property PAN_CHANGED {string} Fires after camera pan
1✔
128
 * @property ORIENTATION_CHANGED {string} Fires when camera's orientation change
1✔
129
 * @property RANGE_CHANGED {string} Fires when camera's range to target change
1✔
130
 * @property CAMERA_TARGET_CHANGED {string} Fires when camera's target change
1✔
131
 */
1✔
132

1✔
133
export const CONTROL_EVENTS = {
1✔
134
    PAN_CHANGED: 'pan-changed',
1✔
135
    ORIENTATION_CHANGED: 'orientation-changed',
1✔
136
    RANGE_CHANGED: 'range-changed',
1✔
137
    CAMERA_TARGET_CHANGED: 'camera-target-changed',
1✔
138
};
1✔
139

1✔
140
const quaterPano = new THREE.Quaternion();
1✔
141
const quaterAxis = new THREE.Quaternion();
1✔
142
const axisX = new THREE.Vector3(1, 0, 0);
1✔
143
let minDistanceZ = Infinity;
1✔
144
const lastNormalizedIntersection = new THREE.Vector3();
1✔
145
const normalizedIntersection = new THREE.Vector3();
1✔
146
const raycaster = new THREE.Raycaster();
1✔
147
const targetPosition = new THREE.Vector3();
1✔
148
const pickedPosition = new THREE.Vector3();
1✔
149
const sphereCamera = new THREE.Sphere();
1✔
150

1✔
151
let previous;
1✔
152
/**
1✔
153
 * GlobeControls is a camera controller
1✔
154
 *
1✔
155
 * @class      GlobeControls
1✔
156
 * @param      {GlobeView}  view the view where the control will be used
1✔
157
 * @param      {CameraTransformOptions|Extent} placement   the {@link CameraTransformOptions} to apply to view's camera
1✔
158
 * or the extent it must display at initialisation, see {@link CameraTransformOptions} in {@link CameraUtils}.
1✔
159
 * @param      {object}  [options] An object with one or more configuration properties. Any property of GlobeControls
1✔
160
 * can be passed in this object.
1✔
161
 * @property      {number}  zoomFactor The factor the scale is multiplied by when dollying (zooming) in or
1✔
162
 * divided by when dollying out. Default is 1.1.
1✔
163
 * @property      {number}  rotateSpeed Speed camera rotation in orbit and panoramic mode. Default is 0.25.
1✔
164
 * @property      {number}  minDistance Minimum distance between ground and camera in meters (Perspective Camera only).
1✔
165
 * Default is 250.
1✔
166
 * @property      {number}  maxDistance Maximum distance between ground and camera in meters
1✔
167
 * (Perspective Camera only). Default is ellipsoid radius * 8.
1✔
168
 * @property      {number}  minZoom How far you can zoom in, in meters (Orthographic Camera only). Default is 0.
1✔
169
 * @property      {number}  maxZoom How far you can zoom out, in meters (Orthographic Camera only). Default
1✔
170
 * is Infinity.
1✔
171
 * @property      {number}  keyPanSpeed Number of pixels moved per push on array key. Default is 7.
1✔
172
 * @property      {number}  minPolarAngle Minimum vertical orbit angle (in degrees). Default is 0.5.
1✔
173
 * @property      {number}  maxPolarAngle Maximum vertical orbit angle (in degrees). Default is 86.
1✔
174
 * @property      {number}  minAzimuthAngle Minimum horizontal orbit angle (in degrees). If modified,
1✔
175
 * should be in [-180,0]. Default is -Infinity.
1✔
176
 * @property      {number}  maxAzimuthAngle Maximum horizontal orbit angle (in degrees). If modified,
1✔
177
 * should be in [0,180]. Default is Infinity.
1✔
178
 * @property      {boolean} handleCollision Handle collision between camera and ground or not, i.e. whether
1✔
179
 * you can zoom underground or not. Default is true.
1✔
180
 * @property      {boolean} enableDamping Enable damping or not (simulates the lag that a real camera
1✔
181
 * operator introduces while operating a heavy physical camera). Default is true.
1✔
182
 * @property      {boolean} dampingMoveFactor the damping move factor. Default is 0.25.
1✔
183
 * @property      {StateControl~State} stateControl redefining which controls state is triggered by the keyboard/mouse
1✔
184
 * event (For example, rewrite the PAN movement to be triggered with the 'left' mouseButton instead of 'right').
1✔
185
 */
1✔
186
class GlobeControls extends THREE.EventDispatcher {
1✔
187
    constructor(view, placement, options = {}) {
1!
188
        super();
16✔
189
        this.player = new AnimationPlayer();
16✔
190
        this.view = view;
16✔
191
        this.camera = view.camera3D;
16✔
192

16✔
193
        // State control
16✔
194
        this.states = new StateControl(this.view, options.stateControl);
16✔
195

16✔
196
        // this.enabled property has moved to StateControl
16✔
197
        Object.defineProperty(this, 'enabled', {
16✔
198
            get: () => this.states.enabled,
16✔
199
            set: (value) => {
16✔
200
                console.warn(
×
201
                    'GlobeControls.enabled property is deprecated. Use StateControl.enabled instead ' +
×
202
                    '- which you can access with GlobeControls.states.enabled.',
×
203
                );
×
204
                this.states.enabled = value;
×
205
            },
×
206
        });
16✔
207

16✔
208
        // These options actually enables dollying in and out; left as "zoom" for
16✔
209
        // backwards compatibility
16✔
210
        if (options.zoomSpeed) {
16!
211
            console.warn('Controls zoomSpeed parameter is deprecated. Use zoomFactor instead.');
×
212
            options.zoomFactor = options.zoomFactor || options.zoomSpeed;
×
213
        }
×
214
        this.zoomFactor = options.zoomFactor || 1.1;
16✔
215

16✔
216
        // Limits to how far you can dolly in and out ( PerspectiveCamera only )
16✔
217
        this.minDistance = options.minDistance || 250;
16✔
218
        this.maxDistance = options.maxDistance || ellipsoidSizes.x * 8.0;
16✔
219

16✔
220
        // Limits to how far you can zoom in and out ( OrthographicCamera only )
16✔
221
        this.minZoom = options.minZoom || 0;
16✔
222
        this.maxZoom = options.maxZoom || Infinity;
16✔
223

16✔
224
        // Set to true to disable this control
16✔
225
        this.rotateSpeed = options.rotateSpeed || 0.25;
16✔
226

16✔
227
        // Set to true to disable this control
16✔
228
        this.keyPanSpeed = options.keyPanSpeed || 7.0; // pixels moved per arrow key push
16✔
229

16✔
230
        // How far you can orbit vertically, upper and lower limits.
16✔
231
        // Range is 0 to Math.PI radians.
16✔
232
        // TODO Warning minPolarAngle = 0.01 -> it isn't possible to be perpendicular on Globe
16✔
233
        this.minPolarAngle = THREE.MathUtils.degToRad(options.minPolarAngle ?? 0.5);
16✔
234
        this.maxPolarAngle = THREE.MathUtils.degToRad(options.minPolarAngle ?? 86);
16✔
235

16✔
236
        // How far you can orbit horizontally, upper and lower limits.
16✔
237
        // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
16✔
238
        this.minAzimuthAngle = options.minAzimuthAngle ? THREE.MathUtils.degToRad(options.minAzimuthAngle) : -Infinity; // radians
16!
239
        this.maxAzimuthAngle = options.maxAzimuthAngle ? THREE.MathUtils.degToRad(options.maxAzimuthAngle) : Infinity; // radians
16!
240

16✔
241
        // Set collision options
16✔
242
        this.handleCollision = typeof (options.handleCollision) !== 'undefined' ? options.handleCollision : true;
16!
243
        this.minDistanceCollision = 60;
16✔
244

16✔
245
        // this.enableKeys property has moved to StateControl
16✔
246
        Object.defineProperty(this, 'enableKeys', {
16✔
247
            get: () => this.states.enableKeys,
16✔
248
            set: (value) => {
16✔
249
                console.warn(
×
250
                    'GlobeControls.enableKeys property is deprecated. Use StateControl.enableKeys instead ' +
×
251
                    '- which you can access with GlobeControls.states.enableKeys.',
×
252
                );
×
253
                this.states.enableKeys = value;
×
254
            },
×
255
        });
16✔
256

16✔
257
        // Enable Damping
16✔
258
        this.enableDamping = options.enableDamping !== false;
16✔
259
        this.dampingMoveFactor = options.dampingMoveFactor != undefined ? options.dampingMoveFactor : dampingFactorDefault;
16!
260

16✔
261
        this.startEvent = {
16✔
262
            type: 'start',
16✔
263
        };
16✔
264
        this.endEvent = {
16✔
265
            type: 'end',
16✔
266
        };
16✔
267
        // Update helper
16✔
268
        this.updateHelper = enableTargetHelper ? (position, helper) => {
16!
269
            positionObject(position, helper);
×
270
            view.notifyChange(this.camera);
×
271
        } : function empty() { };
16✔
272

16✔
273
        this._onEndingMove = null;
16✔
274
        this._onTravel = this.travel.bind(this);
16✔
275
        this._onTouchStart = this.onTouchStart.bind(this);
16✔
276
        this._onTouchEnd = this.onTouchEnd.bind(this);
16✔
277
        this._onTouchMove = this.onTouchMove.bind(this);
16✔
278

16✔
279
        this._onStateChange = this.onStateChange.bind(this);
16✔
280

16✔
281
        this._onRotation = this.handleRotation.bind(this);
16✔
282
        this._onDrag = this.handleDrag.bind(this);
16✔
283
        this._onDolly = this.handleDolly.bind(this);
16✔
284
        this._onPan = this.handlePan.bind(this);
16✔
285
        this._onPanoramic = this.handlePanoramic.bind(this);
16✔
286

16✔
287
        this._onZoom = this.handleZoom.bind(this);
16✔
288

16✔
289
        this.states.addEventListener('state-changed', this._onStateChange, false);
16✔
290

16✔
291
        this.states.addEventListener(this.states.ORBIT._event, this._onRotation, false);
16✔
292
        this.states.addEventListener(this.states.MOVE_GLOBE._event, this._onDrag, false);
16✔
293
        this.states.addEventListener(this.states.DOLLY._event, this._onDolly, false);
16✔
294
        this.states.addEventListener(this.states.PAN._event, this._onPan, false);
16✔
295
        this.states.addEventListener(this.states.PANORAMIC._event, this._onPanoramic, false);
16✔
296

16✔
297
        this.states.addEventListener('zoom', this._onZoom, false);
16✔
298

16✔
299
        this.view.domElement.addEventListener('touchstart', this._onTouchStart, false);
16✔
300
        this.view.domElement.addEventListener('touchend', this._onTouchEnd, false);
16✔
301
        this.view.domElement.addEventListener('touchmove', this._onTouchMove, false);
16✔
302

16✔
303
        this.states.addEventListener(this.states.TRAVEL_IN._event, this._onTravel, false);
16✔
304
        this.states.addEventListener(this.states.TRAVEL_OUT._event, this._onTravel, false);
16✔
305

16✔
306
        view.scene.add(cameraTarget);
16✔
307
        if (enableTargetHelper) {
16!
308
            cameraTarget.add(helpers.target);
×
309
            view.scene.add(helpers.picking);
×
310
        }
×
311

16✔
312
        if (placement.isExtent) {
16✔
313
            placement.center().as('EPSG:4978', xyz);
1✔
314
        } else {
16✔
315
            placement.coord.as('EPSG:4978', xyz);
15✔
316

15✔
317
            placement.tilt = placement.tilt || 89.5;
15!
318
            placement.heading = placement.heading || 0;
15✔
319
        }
15✔
320
        positionObject(xyz, cameraTarget);
16✔
321
        this.lookAtCoordinate(placement, false);
16✔
322

16✔
323
        coordCameraTarget.crs = this.view.referenceCrs;
16✔
324
    }
16✔
325

1✔
326
    get zoomInScale() {
1✔
327
        return this.zoomFactor;
3✔
328
    }
3✔
329
    get zoomOutScale() {
1✔
330
        return 1 / this.zoomFactor;
3✔
331
    }
3✔
332

1✔
333
    get isPaused() {
1✔
334
        // TODO : also check if CameraUtils is performing an animation
2✔
335
        return this.states.currentState === this.states.NONE
2✔
336
            && !this.player.isPlaying();
2✔
337
    }
2✔
338

1✔
339
    onEndingMove(current) {
1✔
340
        if (this._onEndingMove) {
20✔
341
            this.player.removeEventListener('animation-stopped', this._onEndingMove);
4✔
342
            this._onEndingMove = null;
4✔
343
        }
4✔
344
        this.handlingEvent(current);
20✔
345
    }
20✔
346

1✔
347
    rotateLeft(angle = 0) {
1!
348
        sphericalDelta.theta -= angle;
1✔
349
    }
1✔
350

1✔
351
    rotateUp(angle = 0) {
1!
352
        sphericalDelta.phi -= angle;
1✔
353
    }
1✔
354

1✔
355
    // pass in distance in world space to move left
1✔
356
    panLeft(distance) {
1✔
357
        const te = this.camera.matrix.elements;
6✔
358
        // get X column of matrix
6✔
359
        panOffset.fromArray(te);
6✔
360
        panOffset.multiplyScalar(-distance);
6✔
361
        panVector.add(panOffset);
6✔
362
    }
6✔
363

1✔
364
    // pass in distance in world space to move up
1✔
365
    panUp(distance) {
1✔
366
        const te = this.camera.matrix.elements;
6✔
367
        // get Y column of matrix
6✔
368
        panOffset.fromArray(te, 4);
6✔
369
        panOffset.multiplyScalar(distance);
6✔
370
        panVector.add(panOffset);
6✔
371
    }
6✔
372

1✔
373
    // pass in x,y of change desired in pixel space,
1✔
374
    // right and down are positive
1✔
375
    mouseToPan(deltaX, deltaY) {
1✔
376
        const gfx = this.view.mainLoop.gfxEngine;
6✔
377
        if (this.camera.isPerspectiveCamera) {
6✔
378
            let targetDistance = this.camera.position.distanceTo(this.getCameraTargetPosition());
6✔
379
            // half of the fov is center to top of screen
6✔
380
            targetDistance *= 2 * Math.tan(THREE.MathUtils.degToRad(this.camera.fov * 0.5));
6✔
381

6✔
382
            // we actually don't use screenWidth, since perspective camera is fixed to screen height
6✔
383
            this.panLeft(deltaX * targetDistance / gfx.width * this.camera.aspect);
6✔
384
            this.panUp(deltaY * targetDistance / gfx.height);
6✔
385
        } else if (this.camera.isOrthographicCamera) {
6!
386
            // orthographic
×
387
            this.panLeft(deltaX * (this.camera.right - this.camera.left) / gfx.width);
×
388
            this.panUp(deltaY * (this.camera.top - this.camera.bottom) / gfx.height);
×
389
        }
×
390
    }
6✔
391

1✔
392
    // For Mobile
1✔
393
    dolly(delta) {
1✔
394
        if (delta === 0) { return; }
3✔
395
        dollyScale = delta > 0 ? this.zoomInScale : this.zoomOutScale;
3✔
396

3✔
397
        if (this.camera.isPerspectiveCamera) {
3✔
398
            orbitScale /= dollyScale;
2✔
399
        } else if (this.camera.isOrthographicCamera) {
3!
400
            this.camera.zoom = THREE.MathUtils.clamp(this.camera.zoom * dollyScale, this.minZoom, this.maxZoom);
×
401
            this.camera.updateProjectionMatrix();
×
402
            this.view.notifyChange(this.camera);
×
UNCOV
403
        }
×
404
    }
3✔
405

1✔
406
    getMinDistanceCameraBoundingSphereObbsUp(tile) {
1✔
407
        if (tile.level > 10 && tile.children.length == 1 && tile.geometry) {
266!
408
            const obb = tile.obb;
×
409
            sphereCamera.center.copy(this.camera.position);
×
410
            sphereCamera.radius = this.minDistanceCollision;
×
411
            if (obb.isSphereAboveXYBox(sphereCamera)) {
×
412
                minDistanceZ = Math.min(sphereCamera.center.z - obb.box3D.max.z, minDistanceZ);
×
413
            }
×
UNCOV
414
        }
×
415
    }
266✔
416

1✔
417
    update(state = this.states.currentState) {
1✔
418
        // We compute distance between camera's bounding sphere and geometry's obb up face
133✔
419
        minDistanceZ = Infinity;
133✔
420
        if (this.handleCollision) { // We check distance to the ground/surface geometry
133✔
421
            // add minDistanceZ between camera's bounding and tiles's oriented bounding box (up face only)
133✔
422
            // Depending on the distance of the camera with obbs, we add a slowdown or constrain to the movement.
133✔
423
            // this constraint or deceleration is suitable for two types of movement MOVE_GLOBE and ORBIT.
133✔
424
            // This constraint or deceleration inversely proportional to the camera/obb distance
133✔
425
            if (this.view.tileLayer) {
133✔
426
                for (const tile of this.view.tileLayer.level0Nodes) {
133✔
427
                    tile.traverse(this.getMinDistanceCameraBoundingSphereObbsUp.bind(this));
266✔
428
                }
266✔
429
            }
133✔
430
        }
133✔
431
        switch (state) {
133✔
432
            // MOVE_GLOBE Rotate globe with mouse
133✔
433
            case this.states.MOVE_GLOBE:
133✔
434
                if (minDistanceZ < 0) {
123!
435
                    cameraTarget.translateY(-minDistanceZ);
×
UNCOV
436
                    this.camera.position.setLength(this.camera.position.length() - minDistanceZ);
×
437
                } else if (minDistanceZ < this.minDistanceCollision) {
123!
438
                    const translate = this.minDistanceCollision * (1.0 - minDistanceZ / this.minDistanceCollision);
×
439
                    cameraTarget.translateY(translate);
×
440
                    this.camera.position.setLength(this.camera.position.length() + translate);
×
UNCOV
441
                }
×
442
                lastNormalizedIntersection.copy(normalizedIntersection).applyQuaternion(moveAroundGlobe);
123✔
443
                cameraTarget.position.applyQuaternion(moveAroundGlobe);
123✔
444
                this.camera.position.applyQuaternion(moveAroundGlobe);
123✔
445
                break;
123✔
446
            // PAN Move camera in projection plan
133✔
447
            case this.states.PAN:
133✔
448
                this.camera.position.add(panVector);
6✔
449
                cameraTarget.position.add(panVector);
6✔
450
                break;
6✔
451
            // PANORAMIC Move target camera
133✔
452
            case this.states.PANORAMIC: {
133✔
453
                this.camera.worldToLocal(cameraTarget.position);
1✔
454
                const normal = this.camera.position.clone().normalize().applyQuaternion(this.camera.quaternion.clone().invert());
1✔
455
                quaterPano.setFromAxisAngle(normal, sphericalDelta.theta).multiply(quaterAxis.setFromAxisAngle(axisX, sphericalDelta.phi));
1✔
456
                cameraTarget.position.applyQuaternion(quaterPano);
1✔
457
                this.camera.localToWorld(cameraTarget.position);
1✔
458
                break;
1✔
459
            }
1✔
460
            // ZOOM/ORBIT Move Camera around the target camera
133✔
461
            default: {
133✔
462
                // get camera position in local space of target
3✔
463
                this.camera.position.applyMatrix4(cameraTarget.matrixWorldInverse);
3✔
464

3✔
465
                // angle from z-axis around y-axis
3✔
466
                if (sphericalDelta.theta || sphericalDelta.phi) {
3!
UNCOV
467
                    spherical.setFromVector3(this.camera.position);
×
UNCOV
468
                }
×
469
                // far underground
3✔
470
                const dynamicRadius = spherical.radius * Math.sin(this.minPolarAngle);
3✔
471
                const slowdownLimit = dynamicRadius * 8;
3✔
472
                const contraryLimit = dynamicRadius * 2;
3✔
473
                const minContraintPhi = -0.01;
3✔
474

3✔
475
                if (this.handleCollision) {
3✔
476
                    if (minDistanceZ < slowdownLimit && minDistanceZ > contraryLimit && sphericalDelta.phi > 0) {
3!
477
                        // slowdown zone : slowdown sphericalDelta.phi
×
478
                        const slowdownZone = slowdownLimit - contraryLimit;
×
479
                        // the deeper the camera is in this zone, the bigger the factor is
×
480
                        const slowdownFactor = 1 - (slowdownZone - (minDistanceZ - contraryLimit)) / slowdownZone;
×
UNCOV
481
                        // apply slowdown factor on tilt mouvement
×
482
                        sphericalDelta.phi *= slowdownFactor * slowdownFactor;
×
483
                    } else if (minDistanceZ < contraryLimit && minDistanceZ > -contraryLimit && sphericalDelta.phi > minContraintPhi) {
3!
484
                        // contraint zone : contraint sphericalDelta.phi
×
485
                        const contraryZone = 2 * contraryLimit;
×
486
                        // calculation of the angle of rotation which allows to leave this zone
×
487
                        let contraryPhi = -Math.asin((contraryLimit - minDistanceZ) * 0.25 / spherical.radius);
×
488
                        // clamp contraryPhi to make a less brutal exit
×
489
                        contraryPhi = THREE.MathUtils.clamp(contraryPhi, minContraintPhi, 0);
×
490
                        // the deeper the camera is in this zone, the bigger the factor is
×
491
                        const contraryFactor = 1 - (contraryLimit - minDistanceZ) / contraryZone;
×
492
                        sphericalDelta.phi = THREE.MathUtils.lerp(sphericalDelta.phi, contraryPhi, contraryFactor);
×
UNCOV
493
                        minDistanceZ -= Math.sin(sphericalDelta.phi) * spherical.radius;
×
UNCOV
494
                    }
×
495
                }
3✔
496

3✔
497
                spherical.theta += sphericalDelta.theta;
3✔
498
                spherical.phi += sphericalDelta.phi;
3✔
499

3✔
500
                // restrict spherical.theta to be between desired limits
3✔
501
                spherical.theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, spherical.theta));
3✔
502

3✔
503
                // restrict spherical.phi to be between desired limits
3✔
504
                spherical.phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, spherical.phi));
3✔
505
                spherical.radius = this.camera.position.length() * orbitScale;
3✔
506

3✔
507
                // restrict spherical.phi to be betwee EPS and PI-EPS
3✔
508
                spherical.makeSafe();
3✔
509

3✔
510
                // restrict radius to be between desired limits
3✔
511
                spherical.radius = Math.max(this.minDistance, Math.min(this.maxDistance, spherical.radius));
3✔
512

3✔
513
                this.camera.position.setFromSpherical(spherical);
3✔
514

3✔
515
                // if camera is underground, so move up camera
3✔
516
                if (minDistanceZ < 0) {
3!
517
                    this.camera.position.y -= minDistanceZ;
×
518
                    spherical.setFromVector3(this.camera.position);
×
UNCOV
519
                    sphericalDelta.phi = 0;
×
UNCOV
520
                }
×
521

3✔
522
                cameraTarget.localToWorld(this.camera.position);
3✔
523
            }
3✔
524
        }
133✔
525

133✔
526
        this.camera.up.copy(cameraTarget.position).normalize();
133✔
527
        this.camera.lookAt(cameraTarget.position);
133✔
528

133✔
529
        if (!this.enableDamping) {
133✔
530
            sphericalDelta.theta = 0;
5✔
531
            sphericalDelta.phi = 0;
5✔
532
            moveAroundGlobe.set(0, 0, 0, 1);
5✔
533
        } else {
133✔
534
            sphericalDelta.theta *= (1 - dampingFactorDefault);
128✔
535
            sphericalDelta.phi *= (1 - dampingFactorDefault);
128✔
536
            moveAroundGlobe.slerp(dampingMove, this.dampingMoveFactor * 0.2);
128✔
537
        }
128✔
538

133✔
539
        orbitScale = 1;
133✔
540
        panVector.set(0, 0, 0);
133✔
541

133✔
542
        // update condition is:
133✔
543
        // min(camera displacement, camera rotation in radians)^2 > EPS
133✔
544
        // using small-angle approximation cos(x/2) = 1 - x^2 / 8
133✔
545
        if (lastPosition.distanceToSquared(this.camera.position) > EPS || 8 * (1 - lastQuaternion.dot(this.camera.quaternion)) > EPS) {
133✔
546
            this.view.notifyChange(this.camera);
11✔
547

11✔
548
            lastPosition.copy(this.camera.position);
11✔
549
            lastQuaternion.copy(this.camera.quaternion);
11✔
550
        }
11✔
551
        // Launch animationdamping if mouse stops these movements
133✔
552
        if (this.enableDamping && state === this.states.ORBIT && this.player.isStopped() && (sphericalDelta.theta > EPS || sphericalDelta.phi > EPS)) {
133!
553
            this.player.setCallback(() => { this.update(this.states.ORBIT); });
×
UNCOV
554
            this.player.playLater(durationDampingOrbital, 2);
×
UNCOV
555
        }
×
556

133✔
557
        this.view.dispatchEvent({
133✔
558
            type: VIEW_EVENTS.CAMERA_MOVED,
133✔
559
            coord: coordCameraTarget.setFromVector3(cameraTarget.position),
133✔
560
            range: spherical.radius,
133✔
561
            heading: -THREE.MathUtils.radToDeg(spherical.theta),
133✔
562
            tilt: 90 - THREE.MathUtils.radToDeg(spherical.phi),
133✔
563
        });
133✔
564
    }
133✔
565

1✔
566
    onStateChange(event) {
1✔
567
        // If the state changed to NONE, end the movement associated to the previous state.
22✔
568
        if (this.states.currentState === this.states.NONE) {
22✔
569
            this.handleEndMovement(event);
11✔
570
            return;
11✔
571
        }
11✔
572

11✔
573
        // Stop CameraUtils ongoing animations, which can for instance be triggered with `this.travel` or
11✔
574
        // `this.lookAtCoordinate` methods.
11✔
575
        CameraUtils.stop(this.view, this.camera);
11✔
576

11✔
577
        // Dispatch events which specify if changes occurred in camera transform options.
11✔
578
        this.onEndingMove();
11✔
579

11✔
580
        // Stop eventual damping movement.
11✔
581
        this.player.stop();
11✔
582

11✔
583
        // Update camera transform options.
11✔
584
        this.updateTarget();
11✔
585
        previous = CameraUtils.getTransformCameraLookingAtTarget(this.view, this.camera, pickedPosition);
11✔
586

11✔
587
        // Initialize rotation and panoramic movements.
11✔
588
        rotateStart.copy(event.viewCoords);
11✔
589

11✔
590
        // Initialize drag movement.
11✔
591
        if (this.view.getPickingPositionFromDepth(event.viewCoords, pickingPoint)) {
11✔
592
            pickSphere.radius = pickingPoint.length();
11✔
593
            lastNormalizedIntersection.copy(pickingPoint).normalize();
11✔
594
            this.updateHelper(pickingPoint, helpers.picking);
11✔
595
        }
11✔
596

11✔
597
        // Initialize dolly movement.
11✔
598
        dollyStart.copy(event.viewCoords);
11✔
599
        this.view.getPickingPositionFromDepth(event.viewCoords, pickedPosition);        // mouse position
11✔
600

11✔
601
        // Initialize pan movement.
11✔
602
        panStart.copy(event.viewCoords);
11✔
603
    }
11✔
604

1✔
605
    handleRotation(event) {
1✔
606
        // Stop player if needed. Player can be playing while moving mouse in the case of rotation. This is due to the
1✔
607
        // fact that a damping move can occur while rotating (without the need of releasing the mouse button)
1✔
608
        this.player.stop();
1✔
609
        this.handlePanoramic(event);
1✔
610
    }
1✔
611

1✔
612
    handleDrag(event) {
1✔
613
        const normalized = this.view.viewToNormalizedCoords(event.viewCoords);
1✔
614

1✔
615
        // An updateMatrixWorld on the camera prevents camera jittering when moving globe on a zoomed out view, with
1✔
616
        // devtools open in web browser.
1✔
617
        this.camera.updateMatrixWorld();
1✔
618

1✔
619
        raycaster.setFromCamera(normalized, this.camera);
1✔
620

1✔
621
        // If there's intersection then move globe else we stop the move
1✔
622
        if (raycaster.ray.intersectSphere(pickSphere, intersection)) {
1✔
623
            normalizedIntersection.copy(intersection).normalize();
1✔
624
            moveAroundGlobe.setFromUnitVectors(normalizedIntersection, lastNormalizedIntersection);
1✔
625
            lastTimeMouseMove = Date.now();
1✔
626
            this.update();
1✔
627
        } else {
1!
UNCOV
628
            this.states.onPointerUp();
×
UNCOV
629
        }
×
630
    }
1✔
631

1✔
632
    handleDolly(event) {
1✔
633
        dollyEnd.copy(event.viewCoords);
1✔
634
        dollyDelta.subVectors(dollyEnd, dollyStart);
1✔
635
        dollyStart.copy(dollyEnd);
1✔
636
        event.delta = dollyDelta.y;
1✔
637
        if (event.delta != 0) { this.handleZoom(event); }
1!
638
    }
1✔
639

1✔
640
    handlePan(event) {
1✔
641
        if (event.viewCoords) {
5✔
642
            panEnd.copy(event.viewCoords);
1✔
643
            panDelta.subVectors(panEnd, panStart);
1✔
644
            panStart.copy(panEnd);
1✔
645
        } else if (event.direction) {
5✔
646
            panDelta.copy(direction[event.direction]).multiplyScalar(this.keyPanSpeed);
4✔
647
        }
4✔
648

5✔
649
        this.mouseToPan(panDelta.x, panDelta.y);
5✔
650

5✔
651
        this.update(this.states.PAN);
5✔
652
    }
5✔
653

1✔
654
    handlePanoramic(event) {
1✔
655
        rotateEnd.copy(event.viewCoords);
2✔
656
        rotateDelta.subVectors(rotateEnd, rotateStart);
2✔
657

2✔
658
        const gfx = this.view.mainLoop.gfxEngine;
2✔
659

2✔
660
        sphericalDelta.theta -= 2 * Math.PI * rotateDelta.x / gfx.width * this.rotateSpeed;
2✔
661
        // rotating up and down along whole screen attempts to go 360, but limited to 180
2✔
662
        sphericalDelta.phi -= 2 * Math.PI * rotateDelta.y / gfx.height * this.rotateSpeed;
2✔
663

2✔
664
        rotateStart.copy(rotateEnd);
2✔
665
        this.update();
2✔
666
    }
2✔
667

1✔
668
    handleEndMovement(event = {}) {
1!
669
        this.dispatchEvent(this.endEvent);
11✔
670

11✔
671
        this.player.stop();
11✔
672

11✔
673
        // Launch damping movement for :
11✔
674
        //      * this.states.ORBIT
11✔
675
        //      * this.states.MOVE_GLOBE
11✔
676
        if (this.enableDamping) {
11✔
677
            if (event.previous === this.states.ORBIT && (sphericalDelta.theta > EPS || sphericalDelta.phi > EPS)) {
10!
678
                this.player.setCallback(() => { this.update(this.states.ORBIT); });
×
679
                this.player.play(durationDampingOrbital);
×
680
                this._onEndingMove = () => this.onEndingMove();
×
UNCOV
681
                this.player.addEventListener('animation-stopped', this._onEndingMove);
×
682
            } else if (event.previous === this.states.MOVE_GLOBE && (Date.now() - lastTimeMouseMove < 50)) {
10✔
683
                this.player.setCallback(() => { this.update(this.states.MOVE_GLOBE); });
4✔
684
                // animation since mouse up event occurs less than 50ms after the last mouse move
4✔
685
                this.player.play(durationDampingMove);
4✔
686
                this._onEndingMove = () => this.onEndingMove();
4✔
687
                this.player.addEventListener('animation-stopped', this._onEndingMove);
4✔
688
            } else {
10✔
689
                this.onEndingMove();
6✔
690
            }
6✔
691
        } else {
11✔
692
            this.onEndingMove();
1✔
693
        }
1✔
694
    }
11✔
695

1✔
696
    updateTarget() {
1✔
697
        // Check if the middle of the screen is on the globe (to prevent having a dark-screen bug if outside the globe)
12✔
698
        if (this.view.getPickingPositionFromDepth(null, pickedPosition)) {
12✔
699
            // Update camera's target position
12✔
700
            const distance = !isNaN(pickedPosition.x) ? this.camera.position.distanceTo(pickedPosition) : 100;
12!
701
            targetPosition.set(0, 0, -distance);
12✔
702
            this.camera.localToWorld(targetPosition);
12✔
703

12✔
704
            // set new camera target on globe
12✔
705
            positionObject(targetPosition, cameraTarget);
12✔
706
            cameraTarget.matrixWorldInverse.copy(cameraTarget.matrixWorld).invert();
12✔
707
            targetPosition.copy(this.camera.position);
12✔
708
            targetPosition.applyMatrix4(cameraTarget.matrixWorldInverse);
12✔
709
            spherical.setFromVector3(targetPosition);
12✔
710
        }
12✔
711
    }
12✔
712

1✔
713
    handlingEvent(current) {
1✔
714
        current = current || CameraUtils.getTransformCameraLookingAtTarget(this.view, this.camera);
49✔
715
        const diff = CameraUtils.getDiffParams(previous, current);
49✔
716
        if (diff) {
49✔
717
            if (diff.range) {
33✔
718
                this.dispatchEvent({
28✔
719
                    type: CONTROL_EVENTS.RANGE_CHANGED,
28✔
720
                    previous: diff.range.previous,
28✔
721
                    new: diff.range.new,
28✔
722
                });
28✔
723
            }
28✔
724
            if (diff.coord) {
33✔
725
                this.dispatchEvent({
23✔
726
                    type: CONTROL_EVENTS.CAMERA_TARGET_CHANGED,
23✔
727
                    previous: diff.coord.previous,
23✔
728
                    new: diff.coord.new,
23✔
729
                });
23✔
730
            }
23✔
731
            if (diff.tilt || diff.heading) {
33✔
732
                const event = {
20✔
733
                    type: CONTROL_EVENTS.ORIENTATION_CHANGED,
20✔
734
                };
20✔
735
                if (diff.tilt) {
20✔
736
                    event.previous = { tilt: diff.tilt.previous };
19✔
737
                    event.new = { tilt: diff.tilt.new };
19✔
738
                }
19✔
739

20✔
740
                if (diff.heading) {
20✔
741
                    event.previous = event.previous || {};
19✔
742
                    event.new = event.new || {};
19✔
743
                    event.new.heading = diff.heading.new;
19✔
744
                    event.previous.heading = diff.heading.previous;
19✔
745
                }
19✔
746

20✔
747
                this.dispatchEvent(event);
20✔
748
            }
20✔
749
        }
33✔
750
    }
49✔
751

1✔
752
    travel(event) {
1✔
753
        this.player.stop();
6✔
754
        const point = this.view.getPickingPositionFromDepth(event.viewCoords);
6✔
755
        const range = this.getRange(point);
6✔
756
        if (point && range > this.minDistance) {
6✔
757
            return this.lookAtCoordinate({
2✔
758
                coord: new Coordinates('EPSG:4978', point),
2✔
759
                range: range * (event.direction === 'out' ? 1 / 0.6 : 0.6),
2✔
760
                time: 1500,
2✔
761
            });
2✔
762
        }
2✔
763
    }
6✔
764

1✔
765
    handleZoom(event) {
1✔
766
        this.player.stop();
4✔
767
        CameraUtils.stop(this.view, this.camera);
4✔
768
        const zoomScale = event.delta > 0 ? this.zoomInScale : this.zoomOutScale;
4✔
769
        let point = event.type === 'dolly' ? pickedPosition : this.view.getPickingPositionFromDepth(event.viewCoords);        // get cursor position
4!
770
        let range = this.getRange();
4✔
771
        range *= zoomScale;
4✔
772

4✔
773
        if (point && (range > this.minDistance && range < this.maxDistance)) {  // check if the zoom is in the allowed interval
4✔
774
            const camPos = xyz.setFromVector3(cameraTarget.position).as('EPSG:4326', c).toVector3();
3✔
775
            point = xyz.setFromVector3(point).as('EPSG:4326', c).toVector3();
3✔
776

3✔
777
            if (camPos.x * point.x < 0) {     // Correct rotation at 180th meridian by using 0 <= longitude <=360 for interpolation purpose
3!
UNCOV
778
                if (camPos.x - point.x > 180) { point.x += 360; } else if (point.x - camPos.x > 180) { camPos.x += 360; }
×
UNCOV
779
            }
×
780
            point.lerp(  // point interpol between mouse cursor and cam pos
3✔
781
                camPos,
3✔
782
                zoomScale, // interpol factor
3✔
783
            );
3✔
784

3✔
785
            point = c.setFromVector3(point).as('EPSG:4978', xyz);
3✔
786

3✔
787
            return this.lookAtCoordinate({       // update view to the interpolate point
3✔
788
                coord: point,
3✔
789
                range,
3✔
790
            },
3✔
791
            false);
3✔
792
        }
3✔
793
    }
4✔
794

1✔
795
    onTouchStart(event) {
1✔
796
        // CameraUtils.stop(view);
1✔
797
        this.player.stop();
1✔
798
        // TODO : this.states.enabled check should be removed when moving touch events management to StateControl
1✔
799
        if (this.states.enabled === false) { return; }
1!
800

1✔
801
        this.state = this.states.touchToState(event.touches.length);
1✔
802

1✔
803
        this.updateTarget();
1✔
804

1✔
805
        if (this.state !== this.states.NONE) {
1✔
806
            switch (this.state) {
1✔
807
                case this.states.MOVE_GLOBE: {
1✔
808
                    const coords = this.view.eventToViewCoords(event);
1✔
809
                    if (this.view.getPickingPositionFromDepth(coords, pickingPoint)) {
1✔
810
                        pickSphere.radius = pickingPoint.length();
1✔
811
                        lastNormalizedIntersection.copy(pickingPoint).normalize();
1✔
812
                        this.updateHelper(pickingPoint, helpers.picking);
1✔
813
                    } else {
1!
814
                        this.state = this.states.NONE;
×
815
                    }
×
816
                    break;
1✔
817
                }
1✔
818
                case this.states.ORBIT:
1!
819
                case this.states.DOLLY: {
1!
820
                    const x = event.touches[0].pageX;
×
UNCOV
821
                    const y = event.touches[0].pageY;
×
UNCOV
822
                    const dx = x - event.touches[1].pageX;
×
UNCOV
823
                    const dy = y - event.touches[1].pageY;
×
UNCOV
824
                    const distance = Math.sqrt(dx * dx + dy * dy);
×
UNCOV
825
                    dollyStart.set(0, distance);
×
UNCOV
826
                    rotateStart.set(x, y);
×
UNCOV
827
                    break;
×
UNCOV
828
                }
×
829
                case this.states.PAN:
1!
830
                    panStart.set(event.touches[0].pageX, event.touches[0].pageY);
×
831
                    break;
×
832
                default:
1!
833
            }
1✔
834

1✔
835
            this.dispatchEvent(this.startEvent);
1✔
836
        }
1✔
837
    }
1✔
838

1✔
839
    onTouchMove(event) {
1✔
840
        if (this.player.isPlaying()) {
2!
UNCOV
841
            this.player.stop();
×
UNCOV
842
        }
×
843
        // TODO : this.states.enabled check should be removed when moving touch events management to StateControl
2✔
844
        if (this.states.enabled === false) { return; }
2!
845

2✔
846
        event.preventDefault();
2✔
847
        event.stopPropagation();
2✔
848

2✔
849
        switch (event.touches.length) {
2✔
850
            case this.states.MOVE_GLOBE.finger: {
2✔
851
                const coords = this.view.eventToViewCoords(event);
1✔
852
                const normalized = this.view.viewToNormalizedCoords(coords);
1✔
853
                // An updateMatrixWorld on the camera prevents camera jittering when moving globe on a zoomed out view, with
1✔
854
                // devtools open in web browser.
1✔
855
                this.camera.updateMatrixWorld();
1✔
856
                raycaster.setFromCamera(normalized, this.camera);
1✔
857
                // If there's intersection then move globe else we stop the move
1✔
858
                if (raycaster.ray.intersectSphere(pickSphere, intersection)) {
1✔
859
                    normalizedIntersection.copy(intersection).normalize();
1✔
860
                    moveAroundGlobe.setFromUnitVectors(normalizedIntersection, lastNormalizedIntersection);
1✔
861
                    lastTimeMouseMove = Date.now();
1✔
862
                } else {
1!
UNCOV
863
                    this.onTouchEnd();
×
UNCOV
864
                }
×
865
                break;
1✔
866
            }
1✔
867
            case this.states.ORBIT.finger:
2✔
868
            case this.states.DOLLY.finger: {
2✔
869
                const gfx = this.view.mainLoop.gfxEngine;
1✔
870
                rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
1✔
871
                rotateDelta.subVectors(rotateEnd, rotateStart);
1✔
872

1✔
873
                // rotating across whole screen goes 360 degrees around
1✔
874
                this.rotateLeft(2 * Math.PI * rotateDelta.x / gfx.width * this.rotateSpeed);
1✔
875
                // rotating up and down along whole screen attempts to go 360, but limited to 180
1✔
876
                this.rotateUp(2 * Math.PI * rotateDelta.y / gfx.height * this.rotateSpeed);
1✔
877

1✔
878
                rotateStart.copy(rotateEnd);
1✔
879
                const dx = event.touches[0].pageX - event.touches[1].pageX;
1✔
880
                const dy = event.touches[0].pageY - event.touches[1].pageY;
1✔
881
                const distance = Math.sqrt(dx * dx + dy * dy);
1✔
882

1✔
883
                dollyEnd.set(0, distance);
1✔
884
                dollyDelta.subVectors(dollyEnd, dollyStart);
1✔
885

1✔
886
                this.dolly(dollyDelta.y);
1✔
887

1✔
888
                dollyStart.copy(dollyEnd);
1✔
889

1✔
890
                break;
1✔
891
            }
1✔
892
            case this.states.PAN.finger:
2!
UNCOV
893
                panEnd.set(event.touches[0].pageX, event.touches[0].pageY);
×
UNCOV
894
                panDelta.subVectors(panEnd, panStart);
×
UNCOV
895

×
UNCOV
896
                this.mouseToPan(panDelta.x, panDelta.y);
×
897

×
898
                panStart.copy(panEnd);
×
899
                break;
×
900
            default:
2!
UNCOV
901
                this.state = this.states.NONE;
×
902
        }
2✔
903

2✔
904
        if (this.state !== this.states.NONE) {
2✔
905
            this.update(this.state);
2✔
906
        }
2✔
907
    }
2✔
908

1✔
909
    onTouchEnd() {
1✔
UNCOV
910
        this.handleEndMovement({ previous: this.state });
×
UNCOV
911
        this.state = this.states.NONE;
×
UNCOV
912
    }
×
913

1✔
914
    dispose() {
1✔
915
        this.view.domElement.removeEventListener('touchstart', this._onTouchStart, false);
1✔
916
        this.view.domElement.removeEventListener('touchend', this._onTouchEnd, false);
1✔
917
        this.view.domElement.removeEventListener('touchmove', this._onTouchMove, false);
1✔
918

1✔
919
        this.states.dispose();
1✔
920

1✔
921
        this.states.removeEventListener('state-changed', this._onStateChange, false);
1✔
922

1✔
923
        this.states.removeEventListener(this.states.ORBIT._event, this._onRotation, false);
1✔
924
        this.states.removeEventListener(this.states.MOVE_GLOBE._event, this._onDrag, false);
1✔
925
        this.states.removeEventListener(this.states.DOLLY._event, this._onDolly, false);
1✔
926
        this.states.removeEventListener(this.states.PAN._event, this._onPan, false);
1✔
927
        this.states.removeEventListener(this.states.PANORAMIC._event, this._onPanoramic, false);
1✔
928

1✔
929
        this.states.removeEventListener('zoom', this._onZoom, false);
1✔
930

1✔
931
        this.states.removeEventListener(this.states.TRAVEL_IN._event, this._onTravel, false);
1✔
932
        this.states.removeEventListener(this.states.TRAVEL_OUT._event, this._onTravel, false);
1✔
933

1✔
934
        this.dispatchEvent({ type: 'dispose' });
1✔
935
    }
1✔
936
    /**
1✔
937
     * Changes the tilt of the current camera, in degrees.
1✔
938
     * @param {number}  tilt
1✔
939
     * @param {boolean} isAnimated
1✔
940
     * @return {Promise<void>}
1✔
941
     */
1✔
942
    setTilt(tilt, isAnimated) {
1✔
943
        return this.lookAtCoordinate({ tilt }, isAnimated);
1✔
944
    }
1✔
945

1✔
946
    /**
1✔
947
     * Changes the heading of the current camera, in degrees.
1✔
948
     * @param {number} heading
1✔
949
     * @param {boolean} isAnimated
1✔
950
     * @return {Promise<void>}
1✔
951
     */
1✔
952
    setHeading(heading, isAnimated) {
1✔
953
        return this.lookAtCoordinate({ heading }, isAnimated);
1✔
954
    }
1✔
955

1✔
956
    /**
1✔
957
     * Sets the "range": the distance in meters between the camera and the current central point on the screen.
1✔
958
     * @param {number} range
1✔
959
     * @param {boolean} isAnimated
1✔
960
     * @return {Promise<void>}
1✔
961
     */
1✔
962
    setRange(range, isAnimated) {
1✔
963
        return this.lookAtCoordinate({ range }, isAnimated);
1✔
964
    }
1✔
965

1✔
966
    /**
1✔
967
     * Returns the {@linkcode Coordinates} of the globe point targeted by the camera in EPSG:4978 projection. See {@linkcode Coordinates} for conversion
1✔
968
     * @return {THREE.Vector3} position
1✔
969
     */
1✔
970
    getCameraTargetPosition() {
1✔
971
        return cameraTarget.position;
6✔
972
    }
6✔
973

1✔
974
    /**
1✔
975
     * Returns the "range": the distance in meters between the camera and the current central point on the screen.
1✔
976
     * @param {THREE.Vector3} [position] - The position to consider as picked on
1✔
977
     * the ground.
1✔
978
     * @return {number} number
1✔
979
     */
1✔
980
    getRange(position) {
1✔
981
        return CameraUtils.getTransformCameraLookingAtTarget(this.view, this.camera, position).range;
19✔
982
    }
19✔
983

1✔
984
    /**
1✔
985
     * Returns the tilt of the current camera in degrees.
1✔
986
     * @param {THREE.Vector3} [position] - The position to consider as picked on
1✔
987
     * the ground.
1✔
988
     * @return {number} The angle of the rotation in degrees.
1✔
989
     */
1✔
990
    getTilt(position) {
1✔
991
        return CameraUtils.getTransformCameraLookingAtTarget(this.view, this.camera, position).tilt;
2✔
992
    }
2✔
993

1✔
994
    /**
1✔
995
     * Returns the heading of the current camera in degrees.
1✔
996
     * @param {THREE.Vector3} [position] - The position to consider as picked on
1✔
997
     * the ground.
1✔
998
     * @return {number} The angle of the rotation in degrees.
1✔
999
     */
1✔
1000
    getHeading(position) {
1✔
1001
        return CameraUtils.getTransformCameraLookingAtTarget(this.view, this.camera, position).heading;
2✔
1002
    }
2✔
1003

1✔
1004
    /**
1✔
1005
     * Displaces the central point to a specific amount of pixels from its current position.
1✔
1006
     * The view flies to the desired coordinate, i.e.is not teleported instantly. Note : The results can be strange in some cases, if ever possible, when e.g.the camera looks horizontally or if the displaced center would not pick the ground once displaced.
1✔
1007
     * @param      {vector}  pVector  The vector
1✔
1008
     * @return {Promise}
1✔
1009
     */
1✔
1010
    pan(pVector) {
1✔
UNCOV
1011
        this.mouseToPan(pVector.x, pVector.y);
×
UNCOV
1012
        this.update(this.states.PAN);
×
UNCOV
1013
        return Promise.resolve();
×
UNCOV
1014
    }
×
1015

1✔
1016
    /**
1✔
1017
     * Returns the orientation angles of the current camera, in degrees.
1✔
1018
     * @return {Array<number>}
1✔
1019
     */
1✔
1020
    getCameraOrientation() {
1✔
1021
        this.view.getPickingPositionFromDepth(null, pickedPosition);
1✔
1022
        return [this.getTilt(pickedPosition), this.getHeading(pickedPosition)];
1✔
1023
    }
1✔
1024

1✔
1025
    /**
1✔
1026
     * Returns the camera location projected on the ground in lat,lon. See {@linkcode Coordinates} for conversion.
1✔
1027
     * @return {Coordinates} position
1✔
1028
     */
1✔
1029

1✔
1030
    getCameraCoordinate() {
1✔
1031
        return new Coordinates('EPSG:4978', this.camera.position).as('EPSG:4326');
1✔
1032
    }
1✔
1033

1✔
1034
    /**
1✔
1035
     * Returns the {@linkcode Coordinates} of the central point on screen in lat,lon. See {@linkcode Coordinates} for conversion.
1✔
1036
     * @return {Coordinates} coordinate
1✔
1037
     */
1✔
1038
    getLookAtCoordinate() {
1✔
1039
        return CameraUtils.getTransformCameraLookingAtTarget(this.view, this.camera).coord;
2✔
1040
    }
2✔
1041

1✔
1042
    /**
1✔
1043
     * Sets the animation enabled.
1✔
1044
     * @param      {boolean}  enable  enable
1✔
1045
     */
1✔
1046
    setAnimationEnabled(enable) {
1✔
1047
        enableAnimation = enable;
1✔
1048
    }
1✔
1049

1✔
1050
    /**
1✔
1051
     * Determines if animation enabled.
1✔
1052
     * @return     {boolean}  True if animation enabled, False otherwise.
1✔
1053
     */
1✔
1054
    isAnimationEnabled() {
1✔
1055
        return enableAnimation;
2✔
1056
    }
2✔
1057

1✔
1058
    /**
1✔
1059
     * Returns the actual zoom. The zoom will always be between the [getMinZoom(), getMaxZoom()].
1✔
1060
     * @return     {number}  The zoom .
1✔
1061
     */
1✔
1062
    getZoom() {
1✔
1063
        return this.view.tileLayer.computeTileZoomFromDistanceCamera(this.getRange(), this.view.camera);
1✔
1064
    }
1✔
1065

1✔
1066
    /**
1✔
1067
     * Sets the current zoom, which is an index in the logical scales predefined for the application.
1✔
1068
     * The higher the zoom, the closer to the ground.
1✔
1069
     * The zoom is always in the [getMinZoom(), getMaxZoom()] range.
1✔
1070
     * @param      {number}  zoom    The zoom
1✔
1071
     * @param      {boolean}  isAnimated  Indicates if animated
1✔
1072
     * @return     {Promise}
1✔
1073
     */
1✔
1074
    setZoom(zoom, isAnimated) {
1✔
1075
        return this.lookAtCoordinate({ zoom }, isAnimated);
1✔
1076
    }
1✔
1077

1✔
1078
    /**
1✔
1079
     * Return the current zoom scale at the central point of the view.
1✔
1080
     * This function compute the scale of a map
1✔
1081
     * @param      {number}  pitch   Screen pitch, in millimeters ; 0.28 by default
1✔
1082
     * @return     {number}  The zoom scale.
1✔
1083
     *
1✔
1084
     * @deprecated Use View#getScale instead.
1✔
1085
     */
1✔
1086
    getScale(pitch) /* istanbul ignore next */ {
1✔
1087
        console.warn('Deprecated, use View#getScale instead.');
×
1088
        return this.view.getScale(pitch);
×
1089
    }
×
1090

1✔
1091
    /**
1✔
1092
     * To convert the projection in meters on the globe of a number of pixels of screen
1✔
1093
     * @param      {number} pixels count pixels to project
1✔
1094
     * @param      {number} pixelPitch Screen pixel pitch, in millimeters (default = 0.28 mm / standard pixel size of 0.28 millimeters as defined by the OGC)
1✔
1095
     * @return     {number} projection in meters on globe
1✔
1096
     *
1✔
1097
     * @deprecated Use `View#getPixelsToMeters` instead.
1✔
1098
     */
1✔
1099
    pixelsToMeters(pixels, pixelPitch = 0.28) /* istanbul ignore next */ {
1✔
UNCOV
1100
        console.warn('Deprecated use View#getPixelsToMeters instead.');
×
UNCOV
1101
        const scaled = this.getScale(pixelPitch);
×
UNCOV
1102
        const size = pixels * pixelPitch;
×
1103
        return size / scaled / 1000;
×
1104
    }
×
1105

1✔
1106
    /**
1✔
1107
     * To convert the projection a number of horizontal pixels of screen to longitude degree WGS84 on the globe
1✔
1108
     * @param      {number} pixels count pixels to project
1✔
1109
     * @param      {number} pixelPitch Screen pixel pitch, in millimeters (default = 0.28 mm / standard pixel size of 0.28 millimeters as defined by the OGC)
1✔
1110
     * @return     {number} projection in degree on globe
1✔
1111
     *
1✔
1112
     * @deprecated Use `View#getPixelsToMeters` and `GlobeControls#metersToDegrees`
1✔
1113
     * instead.
1✔
1114
     */
1✔
1115
    pixelsToDegrees(pixels, pixelPitch = 0.28) /* istanbul ignore next */ {
1✔
UNCOV
1116
        console.warn('Deprecated, use View#getPixelsToMeters and GlobeControls#getMetersToDegrees instead.');
×
1117
        const chord = this.pixelsToMeters(pixels, pixelPitch);
×
1118
        return THREE.MathUtils.radToDeg(2 * Math.asin(chord / (2 * ellipsoidSizes.x)));
×
1119
    }
×
1120

1✔
1121
    /**
1✔
1122
     * Projection on screen in pixels of length in meter on globe
1✔
1123
     * @param      {number}  value Length in meter on globe
1✔
1124
     * @param      {number}  pixelPitch Screen pixel pitch, in millimeters (default = 0.28 mm / standard pixel size of 0.28 millimeters as defined by the OGC)
1✔
1125
     * @return     {number}  projection in pixels on screen
1✔
1126
     *
1✔
1127
     * @deprecated Use `View#getMetersToPixels` instead.
1✔
1128
     */
1✔
1129
    metersToPixels(value, pixelPitch = 0.28) /* istanbul ignore next */ {
1✔
UNCOV
1130
        console.warn('Deprecated, use View#getMetersToPixels instead.');
×
UNCOV
1131
        const scaled = this.getScale(pixelPitch);
×
UNCOV
1132
        pixelPitch /= 1000;
×
UNCOV
1133
        return value * scaled / pixelPitch;
×
UNCOV
1134
    }
×
1135

1✔
1136
    /**
1✔
1137
     * Changes the zoom of the central point of screen so that screen acts as a map with a specified scale.
1✔
1138
     *  The view flies to the desired zoom scale;
1✔
1139
     * @param      {number}  scale  The scale
1✔
1140
     * @param      {number}  pitch  The pitch
1✔
1141
     * @param      {boolean}  isAnimated  Indicates if animated
1✔
1142
     * @return     {Promise}
1✔
1143
     */
1✔
1144
    setScale(scale, pitch, isAnimated) {
1✔
1145
        return this.lookAtCoordinate({ scale, pitch }, isAnimated);
1✔
1146
    }
1✔
1147

1✔
1148
    /**
1✔
1149
     * Changes the center of the scene on screen to the specified in lat, lon. See {@linkcode Coordinates} for conversion.
1✔
1150
     * This function allows to change the central position, the zoom, the range, the scale and the camera orientation at the same time.
1✔
1151
     * The zoom has to be between the [getMinZoom(), getMaxZoom()].
1✔
1152
     * Zoom parameter is ignored if range is set
1✔
1153
     * The tilt's interval is between 4 and 89.5 degree
1✔
1154
     *
1✔
1155
     * @param {CameraUtils~CameraTransformOptions|Extent} [params] - camera transformation to apply
1✔
1156
     * @param {number} [params.zoom] - zoom
1✔
1157
     * @param {number} [params.scale] - scale
1✔
1158
     * @param {boolean} [isAnimated] - Indicates if animated
1✔
1159
     * @return {Promise} A promise that resolves when transformation is complete
1✔
1160
     */
1✔
1161
    lookAtCoordinate(params = {}, isAnimated = this.isAnimationEnabled()) {
1!
1162
        this.player.stop();
29✔
1163

29✔
1164
        if (!params.isExtent) {
29✔
1165
            if (params.zoom) {
28✔
1166
                params.range = this.view.tileLayer.computeDistanceCameraFromTileZoom(params.zoom, this.view.camera);
2✔
1167
            } else if (params.scale) {
28✔
1168
                params.range = this.view.getScaleFromDistance(params.pitch, params.scale);
1✔
1169
                if (params.range < this.minDistance || params.range > this.maxDistance) {
1!
1170
                    // eslint-disable-next-line no-console
×
UNCOV
1171
                    console.warn(`This scale ${params.scale} can not be reached`);
×
UNCOV
1172
                    params.range = THREE.MathUtils.clamp(params.range, this.minDistance, this.maxDistance);
×
UNCOV
1173
                }
×
1174
            }
1✔
1175

28✔
1176
            if (params.tilt !== undefined) {
28✔
1177
                const minTilt = 90 - THREE.MathUtils.radToDeg(this.maxPolarAngle);
16✔
1178
                const maxTilt = 90 - THREE.MathUtils.radToDeg(this.minPolarAngle);
16✔
1179
                if (params.tilt < minTilt || params.tilt > maxTilt) {
16!
UNCOV
1180
                    params.tilt = THREE.MathUtils.clamp(params.tilt, minTilt, maxTilt);
×
UNCOV
1181
                    // eslint-disable-next-line no-console
×
UNCOV
1182
                    console.warn('Tilt was clamped to ', params.tilt, ` the interval is between ${minTilt} and ${maxTilt} degree`);
×
UNCOV
1183
                }
×
1184
            }
16✔
1185
        }
28✔
1186

29✔
1187
        previous = CameraUtils.getTransformCameraLookingAtTarget(this.view, this.camera);
29✔
1188
        if (isAnimated) {
29✔
1189
            params.callback = r => cameraTarget.position.copy(r.targetWorldPosition);
1✔
1190
            this.dispatchEvent({ type: 'animation-started' });
1✔
1191
            return CameraUtils.animateCameraToLookAtTarget(this.view, this.camera, params)
1✔
1192
                .then((result) => {
1✔
1193
                    this.dispatchEvent({ type: 'animation-ended' });
1✔
1194
                    this.handlingEvent(result);
1✔
1195
                    return result;
1✔
1196
                });
1✔
1197
        } else {
29✔
1198
            return CameraUtils.transformCameraToLookAtTarget(this.view, this.camera, params).then((result) => {
28✔
1199
                cameraTarget.position.copy(result.targetWorldPosition);
28✔
1200
                this.handlingEvent(result);
28✔
1201
                return result;
28✔
1202
            });
28✔
1203
        }
28✔
1204
    }
29✔
1205

1✔
1206
    /**
1✔
1207
     * Pick a position on the globe at the given position in lat,lon. See {@linkcode Coordinates} for conversion.
1✔
1208
     * @param {Vector2} windowCoords - window coordinates
1✔
1209
     * @param {number=} y - The y-position inside the Globe element.
1✔
1210
     * @return {Coordinates} position
1✔
1211
     */
1✔
1212
    pickGeoPosition(windowCoords) {
1✔
1213
        const pickedPosition = this.view.getPickingPositionFromDepth(windowCoords);
1✔
1214

1✔
1215
        if (!pickedPosition) {
1!
UNCOV
1216
            return;
×
UNCOV
1217
        }
×
1218

1✔
1219
        return new Coordinates('EPSG:4978', pickedPosition).as('EPSG:4326');
1✔
1220
    }
1✔
1221
}
1✔
1222

1✔
1223
export default GlobeControls;
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