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

iTowns / itowns / 10635241580

30 Aug 2024 03:26PM UTC coverage: 86.966% (-2.8%) from 89.766%
10635241580

push

github

jailln
feat(3dtiles): add new OGC3DTilesLayer using 3d-tiles-renderer-js

Deprecate C3DTilesLayer (replaced by OGC3DTilesLayer).
Add new iGLTFLoader that loads gltf 1.0 and 2.0 files.

2791 of 3694 branches covered (75.55%)

Branch coverage included in aggregate %.

480 of 644 new or added lines in 8 files covered. (74.53%)

2144 existing lines in 111 files now uncovered.

24319 of 27479 relevant lines covered (88.5%)

1024.72 hits per line

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

96.71
/src/Controls/StateControl.js
1
import * as THREE from 'three';
1✔
2

1✔
3
const CONTROL_KEYS = {
1✔
4
    LEFT: 37,
1✔
5
    UP: 38,
1✔
6
    RIGHT: 39,
1✔
7
    BOTTOM: 40,
1✔
8
    SPACE: 32,
1✔
9
    SHIFT: 16,
1✔
10
    CTRL: 17,
1✔
11
    META: 91,
1✔
12
    S: 83,
1✔
13
};
1✔
14

1✔
15

1✔
16
// TODO : a class should be made for `State`, and the properties marked with `_` prefix should be made private
1✔
17
const DEFAULT_STATES = {
1✔
18
    ORBIT: {
1✔
19
        enable: true,
1✔
20
        mouseButton: THREE.MOUSE.LEFT,
1✔
21
        double: false,
1✔
22
        keyboard: CONTROL_KEYS.CTRL,
1✔
23
        finger: 2,
1✔
24
        _event: 'rotate',
1✔
25
    },
1✔
26
    MOVE_GLOBE: {
1✔
27
        enable: true,
1✔
28
        mouseButton: THREE.MOUSE.LEFT,
1✔
29
        double: false,
1✔
30
        finger: 1,
1✔
31
        _event: 'drag',
1✔
32
    },
1✔
33
    DOLLY: {
1✔
34
        enable: true,
1✔
35
        mouseButton: THREE.MOUSE.MIDDLE,
1✔
36
        double: false,
1✔
37
        finger: 2,
1✔
38
        _event: 'dolly',
1✔
39
    },
1✔
40
    PAN: {
1✔
41
        enable: true,
1✔
42
        mouseButton: THREE.MOUSE.RIGHT,
1✔
43
        double: false,
1✔
44
        finger: 3,
1✔
45
        _event: 'pan',
1✔
46
    },
1✔
47
    PANORAMIC: {
1✔
48
        enable: true,
1✔
49
        mouseButton: THREE.MOUSE.LEFT,
1✔
50
        double: false,
1✔
51
        keyboard: CONTROL_KEYS.SHIFT,
1✔
52
        _event: 'panoramic',
1✔
53
    },
1✔
54
    TRAVEL_IN: {
1✔
55
        enable: true,
1✔
56
        mouseButton: THREE.MOUSE.LEFT,
1✔
57
        double: true,
1✔
58
        _event: 'travel_in',
1✔
59
        _trigger: true,
1✔
60
        _direction: 'in',
1✔
61
    },
1✔
62
    TRAVEL_OUT: {
1✔
63
        enable: false,
1✔
64
        double: false,
1✔
65
        _event: 'travel_out',
1✔
66
        _trigger: true,
1✔
67
        _direction: 'out',
1✔
68
    },
1✔
69
    ZOOM: {
1✔
70
        enable: true,
1✔
71
        _event: 'zoom',
1✔
72
        _trigger: true,
1✔
73
    },
1✔
74
    PAN_UP: {
1✔
75
        enable: true,
1✔
76
        keyboard: CONTROL_KEYS.UP,
1✔
77
        double: false,
1✔
78
        _event: 'pan',
1✔
79
        _trigger: true,
1✔
80
        _direction: 'up',
1✔
81
    },
1✔
82
    PAN_BOTTOM: {
1✔
83
        enable: true,
1✔
84
        keyboard: CONTROL_KEYS.BOTTOM,
1✔
85
        double: false,
1✔
86
        _event: 'pan',
1✔
87
        _trigger: true,
1✔
88
        _direction: 'bottom',
1✔
89
    },
1✔
90
    PAN_LEFT: {
1✔
91
        enable: true,
1✔
92
        keyboard: CONTROL_KEYS.LEFT,
1✔
93
        double: false,
1✔
94
        _event: 'pan',
1✔
95
        _trigger: true,
1✔
96
        _direction: 'left',
1✔
97
    },
1✔
98
    PAN_RIGHT: {
1✔
99
        enable: true,
1✔
100
        keyboard: CONTROL_KEYS.RIGHT,
1✔
101
        double: false,
1✔
102
        _event: 'pan',
1✔
103
        _trigger: true,
1✔
104
        _direction: 'right',
1✔
105
    },
1✔
106
};
1✔
107

1✔
108

1✔
109
const viewCoords = new THREE.Vector2();
1✔
110

1✔
111

1✔
112
/**
1✔
113
 * @typedef {Object} StateControl~State
1✔
114
 * @property {boolean} enable=true Indicate whether the state is enabled or not.
1✔
115
 * @property {Number} [mouseButton] The mouse button bound to this state.
1✔
116
 * @property {Number} [keyboard] The keyCode of the keyboard input bound to this state.
1✔
117
 * @property {Number} [finger] The number of fingers on the pad bound to this state.
1✔
118
 * @property {boolean} [double] True if the mouse button bound to this state must be pressed twice. For
1✔
119
                                * example, if `double` is set to true with a `mouseButton` set to left click,
1✔
120
                                * the State will be bound to a double click mouse button.
1✔
121
 */
1✔
122

1✔
123
/**
1✔
124
 * It represents the control's states.
1✔
125
 * Each {@link State} is a control mode of the camera and how to interact with
1✔
126
 * the interface to activate this mode.
1✔
127
 * @class StateControl
1✔
128
 *
1✔
129
 * @property {State}    NONE        {@link State} when camera is idle.
1✔
130
 * @property {State}    ORBIT       {@link State} describing camera orbiting movement : the camera moves around its
1✔
131
                                    * target at a constant distance from it.
1✔
132
 * @property {State}    DOLLY       {@link State} describing camera dolly movement : the camera moves forward or
1✔
133
                                    * backward from its target.
1✔
134
 * @property {State}    PAN         {@link State} describing camera pan movement : the camera moves parallel to the
1✔
135
                                    * current view plane.
1✔
136
 * @property {State}    MOVE_GLOBE  {@link State} describing camera drag movement : the camera is moved around the view
1✔
137
                                    * to give the feeling that the view is dragged under a static camera.
1✔
138
 * @property {State}    PANORAMIC   {@link State} describing camera panoramic movement : the camera is rotated around
1✔
139
                                    * its own position.
1✔
140
 * @property {State}    TRAVEL_IN   {@link State} describing camera travel in movement : the camera is zoomed in toward
1✔
141
                                    * a given position. The target position depends on the key/mouse binding of this
1✔
142
                                    * state. If bound to a mouse button, the target position is the mouse position.
1✔
143
                                    * Otherwise, it is the center of the screen.
1✔
144
 * @property {State}    TRAVEL_OUT  {@link State} describing camera travel out movement : the camera is zoomed out from
1✔
145
                                    * a given position. The target position depends on the key/mouse binding of this
1✔
146
                                    * state. If bound to a mouse button, the target position is the mouse position.
1✔
147
                                    * Otherwise, it is the center of the screen. It is disabled by default.
1✔
148
 * @property {State}    ZOOM        {@link State} describing camera zoom in and out movement.
1✔
149
 * @property {boolean}  enable      Defines whether all input will be communicated to the associated `Controls` or not.
1✔
150
                                    * Default is true.
1✔
151
 * @property {boolean}  enableKeys  Defines whether keyboard input will be communicated to the associated `Controls` or
1✔
152
                                    * not. Default is true.
1✔
153
 */
1✔
154
class StateControl extends THREE.EventDispatcher {
1✔
155
    constructor(view, options = {}) {
1!
156
        super();
16✔
157

16✔
158
        this._view = view;
16✔
159
        this._domElement = view.domElement;
16✔
160

16✔
161
        let enabled = true;
16✔
162
        Object.defineProperty(this, 'enabled', {
16✔
163
            get: () => enabled,
16✔
164
            set: (value) => {
16✔
165
                if (!value) {
2✔
166
                    this.onKeyUp();
1✔
167
                    this.onPointerUp();
1✔
168
                }
1✔
169
                enabled = value;
2✔
170
            },
2✔
171
        });
16✔
172

16✔
173
        // Set to true to disable use of the keys
16✔
174
        let enableKeys = true;
16✔
175
        Object.defineProperty(this, 'enableKeys', {
16✔
176
            get: () => enableKeys,
16✔
177
            set: (value) => {
16✔
UNCOV
178
                if (!value) {
×
UNCOV
179
                    this.onKeyUp();
×
UNCOV
180
                }
×
UNCOV
181
                enableKeys = value;
×
UNCOV
182
            },
×
183
        });
16✔
184

16✔
185
        this.NONE = {};
16✔
186

16✔
187
        let currentState = this.NONE;
16✔
188
        Object.defineProperty(this, 'currentState', {
16✔
189
            get: () => currentState,
16✔
190
            set: (newState) => {
16✔
191
                if (currentState !== newState) {
42✔
192
                    const previous = currentState;
22✔
193
                    currentState = newState;
22✔
194
                    this.dispatchEvent({ type: 'state-changed', viewCoords, previous });
22✔
195
                }
22✔
196
            },
42✔
197
        });
16✔
198

16✔
199
        // TODO : the 4 next properties should be made private when ES6 allows it
16✔
200
        this._clickTimeStamp = 0;
16✔
201
        this._lastMousePressed = { viewCoords: new THREE.Vector2() };
16✔
202
        this._currentMousePressed = undefined;
16✔
203
        this._currentKeyPressed = undefined;
16✔
204

16✔
205
        this._onPointerDown = this.onPointerDown.bind(this);
16✔
206
        this._onPointerMove = this.onPointerMove.bind(this);
16✔
207
        this._onPointerUp = this.onPointerUp.bind(this);
16✔
208
        this._onMouseWheel = this.onMouseWheel.bind(this);
16✔
209

16✔
210
        this._onKeyDown = this.onKeyDown.bind(this);
16✔
211
        this._onKeyUp = this.onKeyUp.bind(this);
16✔
212

16✔
213
        this._onBlur = this.onBlur.bind(this);
16✔
214
        this._onContextMenu = this.onContextMenu.bind(this);
16✔
215

16✔
216
        this._domElement.addEventListener('pointerdown', this._onPointerDown, false);
16✔
217
        this._domElement.addEventListener('wheel', this._onMouseWheel, false);
16✔
218
        this._domElement.addEventListener('keydown', this._onKeyDown, false);
16✔
219
        this._domElement.addEventListener('keyup', this._onKeyUp, false);
16✔
220

16✔
221
        // Reset key/mouse when window loose focus
16✔
222
        this._domElement.addEventListener('blur', this._onBlur);
16✔
223
        // disable context menu when right-clicking
16✔
224
        this._domElement.addEventListener('contextmenu', this._onContextMenu, false);
16✔
225

16✔
226
        this.setFromOptions(options);
16✔
227
    }
16✔
228

1✔
229
    /**
1✔
230
     * get the state corresponding to the mouse button and the keyboard key. If the input relates to a trigger - a
1✔
231
     * single event which triggers movement, without the move of the mouse for instance -, dispatch a relevant event.
1✔
232
     * @param      {Number}  mouseButton  The mouse button
1✔
233
     * @param      {Number}  keyboard     The keyboard
1✔
234
     * @param      {Boolean} [double]     Value of the searched state `double` property
1✔
235
     * @return     {State}  the state corresponding
1✔
236
     */
1✔
237
    inputToState(mouseButton, keyboard, double = false) {
1✔
238
        for (const key of Object.keys(DEFAULT_STATES)) {
23✔
239
            const state = this[key];
184✔
240
            if (state.enable
184✔
241
                && state.mouseButton === mouseButton
182✔
242
                && state.keyboard === keyboard
71✔
243
                && state.double === double
22✔
244
            ) {
184✔
245
                // If the input relates to a state, returns it
18✔
246
                if (!state._trigger) { return state; }
18✔
247
                // If the input relates to a trigger (TRAVEL_IN, TRAVEL_OUT), dispatch a relevant event.
8✔
248
                this.dispatchEvent({
8✔
249
                    type: state._event,
8✔
250
                    // Dont pass viewCoords if the input is only a keyboard input.
8✔
251
                    viewCoords: mouseButton !== undefined && viewCoords,
18✔
252
                    direction: state._direction,
18✔
253
                });
18✔
254
            }
18✔
255
        }
184✔
256
        return this.NONE;
13✔
257
    }
13✔
258

1✔
259
    /**
1✔
260
     * get the state corresponding to the number of finger on the pad
1✔
261
     *
1✔
262
     * @param      {Number}  finger  The number of finger
1✔
263
     * @return     {state}  the state corresponding
1✔
264
     */
1✔
265
    touchToState(finger) {
1✔
266
        for (const key of Object.keys(DEFAULT_STATES)) {
3✔
267
            const state = this[key];
18✔
268
            if (state.enable && finger == state.finger) {
18✔
269
                return state;
2✔
270
            }
2✔
271
        }
18✔
272
        return this.NONE;
1✔
273
    }
1✔
274

1✔
275
    /**
1✔
276
     * Set the current StateControl {@link State} properties to given values.
1✔
277
     * @param {Object}  options     Object containing the `State` values to set current `StateControl` properties to.
1✔
278
                                    * The `enable` property do not necessarily need to be specified. In that case, the
1✔
279
                                    * previous value of this property will be kept for the new {@link State}.
1✔
280
     *
1✔
281
     * @example
1✔
282
     * // Switch bindings for PAN and MOVE_GLOBE actions, and disabling PANORAMIC movement :
1✔
283
     * view.controls.states.setFromOptions({
1✔
284
     *     PAN: {
1✔
285
     *         mouseButton: itowns.THREE.MOUSE.LEFT,
1✔
286
     *     },
1✔
287
     *     MOVE_GLOBE: {
1✔
288
     *         mouseButton: itowns.THREE.MOUSE.RIGHT,
1✔
289
     *     },
1✔
290
     *     PANORAMIC: {
1✔
291
     *         enable: false,
1✔
292
     *     },
1✔
293
     * };
1✔
294
     */
1✔
295
    setFromOptions(options) {
1✔
296
        for (const state in DEFAULT_STATES) {
20✔
297
            if ({}.hasOwnProperty.call(DEFAULT_STATES, state)) {
240✔
298
                let newState = {};
240✔
299
                newState = options[state] || this[state] || Object.assign(newState, DEFAULT_STATES[state]);
240✔
300

240✔
301
                // Copy the previous value of `enable` property if not defined in options
240✔
302
                if (options[state] && options[state].enable === undefined) {
240✔
303
                    newState.enable = this[state].enable;
5✔
304
                }
5✔
305
                // If no value is provided for the `double` property,
240✔
306
                // defaults it to `false` instead of leaving it undefined
240✔
307
                newState.double = !!newState.double;
240✔
308

240✔
309
                // Copy the `_event` and `_trigger` properties
240✔
310
                newState._event = DEFAULT_STATES[state]._event;
240✔
311
                newState._trigger = DEFAULT_STATES[state]._trigger;
240✔
312
                newState._direction = DEFAULT_STATES[state]._direction;
240✔
313

240✔
314
                this[state] = newState;
240✔
315
            }
240✔
316
        }
240✔
317
    }
20✔
318

1✔
319

1✔
320
    // ---------- POINTER EVENTS : ----------
1✔
321

1✔
322
    onPointerDown(event) {
1✔
323
        if (!this.enabled) { return; }
16✔
324

13✔
325
        viewCoords.copy(this._view.eventToViewCoords(event));
13✔
326

13✔
327
        switch (event.pointerType) {
13✔
328
            case 'mouse': {
13✔
329
                this._currentMousePressed = event.button;
13✔
330

13✔
331
                if (this._currentKeyPressed === undefined) {
13✔
332
                    if (event.ctrlKey) {
11!
UNCOV
333
                        this._currentKeyPressed = CONTROL_KEYS.CTRL;
×
334
                    } else if (event.shiftKey) {
11!
UNCOV
335
                        this._currentKeyPressed = CONTROL_KEYS.SHIFT;
×
336
                    } else if (event.metaKey) {
11!
UNCOV
337
                        this._currentKeyPressed = CONTROL_KEYS.META;
×
UNCOV
338
                    }
×
339
                }
11✔
340
                this.currentState = this.inputToState(
13✔
341
                    this._currentMousePressed,
13✔
342
                    this._currentKeyPressed,
13✔
343
                    // Detect if the mouse button was pressed less than 500 ms before, and if the cursor has not moved two much
13✔
344
                    // since previous click. If so, set dblclick to true.
13✔
345
                    event.timeStamp - this._clickTimeStamp < 500
13✔
346
                    && this._lastMousePressed.button === this._currentMousePressed
4✔
347
                    && this._lastMousePressed.viewCoords.distanceTo(viewCoords) < 5,
4✔
348
                );
13✔
349

13✔
350
                this._clickTimeStamp = event.timeStamp;
13✔
351
                this._lastMousePressed.button = this._currentMousePressed;
13✔
352
                this._lastMousePressed.viewCoords.copy(viewCoords);
13✔
353

13✔
354
                break;
13✔
355
            }
13✔
356
            // TODO : add touch event management
16✔
357
            default:
16!
358
        }
16✔
359

13✔
360
        this._domElement.addEventListener('pointermove', this._onPointerMove, false);
13✔
361
        this._domElement.addEventListener('pointerup', this._onPointerUp, false);
13✔
362
        this._domElement.addEventListener('mouseleave', this._onPointerUp, false);
13✔
363
    }
13✔
364

1✔
365
    onPointerMove(event) {
1✔
366
        event.preventDefault();
5✔
367
        if (!this.enabled) { return; }
5!
368

5✔
369
        viewCoords.copy(this._view.eventToViewCoords(event));
5✔
370

5✔
371
        switch (event.pointerType) {
5✔
372
            case 'mouse':
5✔
373
                this.dispatchEvent({ type: this.currentState._event, viewCoords });
5✔
374
                break;
5✔
375
            // TODO : add touch event management
5✔
376
            default:
5!
377
        }
5✔
378
    }
5✔
379

1✔
380
    onPointerUp() {
1✔
381
        if (!this.enabled) { return; }
18✔
382
        this._currentMousePressed = undefined;
15✔
383

15✔
384
        this._domElement.removeEventListener('pointermove', this._onPointerMove, false);
15✔
385
        this._domElement.removeEventListener('pointerup', this._onPointerUp, false);
15✔
386
        this._domElement.removeEventListener('mouseleave', this._onPointerUp, false);
15✔
387

15✔
388
        this.currentState = this.NONE;
15✔
389
    }
15✔
390

1✔
391

1✔
392
    // ---------- WHEEL EVENT : ----------
1✔
393

1✔
394
    onMouseWheel(event) {
1✔
395
        event.preventDefault();
3✔
396

3✔
397
        if (this.enabled && this.ZOOM.enable) {
3✔
398
            this.dispatchEvent({ type: this.ZOOM._event, delta: event.deltaY });
1✔
399
        }
1✔
400
    }
3✔
401

1✔
402

1✔
403
    // ---------- KEYBOARD EVENTS : ----------
1✔
404

1✔
405
    onKeyDown(event) {
1✔
406
        if (!this.enabled || !this.enableKeys) { return; }
14✔
407
        this._currentKeyPressed = event.keyCode;
8✔
408

8✔
409
        this.inputToState(this._currentMousePressed, this._currentKeyPressed);
8✔
410
    }
8✔
411

1✔
412
    onKeyUp() {
1✔
413
        if (!this.enabled || !this.enableKeys) { return; }
16✔
414
        this._currentKeyPressed = undefined;
10✔
415
        if (this._currentMousePressed === undefined) {
10✔
416
            this.currentState = this.NONE;
10✔
417
        }
10✔
418
    }
16✔
419

1✔
420

1✔
421
    onBlur() {
1✔
422
        this.onKeyUp();
1✔
423
        this.onPointerUp();
1✔
424
    }
1✔
425

1✔
426
    onContextMenu(event) {
1✔
427
        event.preventDefault();
1✔
428
    }
1✔
429

1✔
430
    /**
1✔
431
     * Remove all event listeners created within this instance of `StateControl`
1✔
432
     */
1✔
433
    dispose() {
1✔
434
        this._clickTimeStamp = 0;
2✔
435
        this._lastMousePressed = undefined;
2✔
436
        this._currentKeyPressed = undefined;
2✔
437

2✔
438
        this._domElement.removeEventListener('pointerdown', this._onPointerDown, false);
2✔
439
        this._domElement.removeEventListener('pointermove', this._onPointerMove, false);
2✔
440
        this._domElement.removeEventListener('pointerup', this._onPointerUp, false);
2✔
441
        this._domElement.removeEventListener('wheel', this._onMouseWheel, false);
2✔
442

2✔
443
        this._domElement.removeEventListener('keydown', this._onKeyDown, false);
2✔
444
        this._domElement.removeEventListener('keyup', this._onKeyUp, false);
2✔
445

2✔
446
        this._domElement.removeEventListener('blur', this._onBlur);
2✔
447
        this._domElement.removeEventListener('contextmenu', this._onContextMenu, false);
2✔
448
    }
2✔
449
}
1✔
450

1✔
451
export default StateControl;
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