UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

432 lines (414 loc) 15.4 kB
import * as THREE from 'three'; const CONTROL_KEYS = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, SPACE: 32, SHIFT: 16, CTRL: 17, META: 91, S: 83 }; // TODO : a class should be made for `State`, and the properties marked with `_` prefix should be made private const DEFAULT_STATES = { ORBIT: { enable: true, mouseButton: THREE.MOUSE.LEFT, double: false, keyboard: CONTROL_KEYS.CTRL, finger: 2, _event: 'rotate' }, MOVE_GLOBE: { enable: true, mouseButton: THREE.MOUSE.LEFT, double: false, finger: 1, _event: 'drag' }, DOLLY: { enable: true, mouseButton: THREE.MOUSE.MIDDLE, double: false, finger: 2, _event: 'dolly' }, PAN: { enable: true, mouseButton: THREE.MOUSE.RIGHT, double: false, finger: 3, _event: 'pan' }, PANORAMIC: { enable: true, mouseButton: THREE.MOUSE.LEFT, double: false, keyboard: CONTROL_KEYS.SHIFT, _event: 'panoramic' }, TRAVEL_IN: { enable: true, mouseButton: THREE.MOUSE.LEFT, double: true, _event: 'travel_in', _trigger: true, _direction: 'in' }, TRAVEL_OUT: { enable: false, double: false, _event: 'travel_out', _trigger: true, _direction: 'out' }, ZOOM: { enable: true, _event: 'zoom', _trigger: true }, PAN_UP: { enable: true, keyboard: CONTROL_KEYS.UP, double: false, _event: 'pan', _trigger: true, _direction: 'up' }, PAN_BOTTOM: { enable: true, keyboard: CONTROL_KEYS.BOTTOM, double: false, _event: 'pan', _trigger: true, _direction: 'bottom' }, PAN_LEFT: { enable: true, keyboard: CONTROL_KEYS.LEFT, double: false, _event: 'pan', _trigger: true, _direction: 'left' }, PAN_RIGHT: { enable: true, keyboard: CONTROL_KEYS.RIGHT, double: false, _event: 'pan', _trigger: true, _direction: 'right' } }; const viewCoords = new THREE.Vector2(); /** * @typedef {Object} StateControl~State * @property {boolean} enable=true Indicate whether the state is enabled or not. * @property {Number} [mouseButton] The mouse button bound to this state. * @property {Number} [keyboard] The keyCode of the keyboard input bound to this state. * @property {Number} [finger] The number of fingers on the pad bound to this state. * @property {boolean} [double] True if the mouse button bound to this state must be pressed twice. For * example, if `double` is set to true with a `mouseButton` set to left click, * the State will be bound to a double click mouse button. */ /** * It represents the control's states. * Each {@link State} is a control mode of the camera and how to interact with * the interface to activate this mode. * @class StateControl * * @property {State} NONE {@link State} when camera is idle. * @property {State} ORBIT {@link State} describing camera orbiting movement : the camera moves around its * target at a constant distance from it. * @property {State} DOLLY {@link State} describing camera dolly movement : the camera moves forward or * backward from its target. * @property {State} PAN {@link State} describing camera pan movement : the camera moves parallel to the * current view plane. * @property {State} MOVE_GLOBE {@link State} describing camera drag movement : the camera is moved around the view * to give the feeling that the view is dragged under a static camera. * @property {State} PANORAMIC {@link State} describing camera panoramic movement : the camera is rotated around * its own position. * @property {State} TRAVEL_IN {@link State} describing camera travel in movement : the camera is zoomed in toward * a given position. The target position depends on the key/mouse binding of this * state. If bound to a mouse button, the target position is the mouse position. * Otherwise, it is the center of the screen. * @property {State} TRAVEL_OUT {@link State} describing camera travel out movement : the camera is zoomed out from * a given position. The target position depends on the key/mouse binding of this * state. If bound to a mouse button, the target position is the mouse position. * Otherwise, it is the center of the screen. It is disabled by default. * @property {State} ZOOM {@link State} describing camera zoom in and out movement. * @property {boolean} enable Defines whether all input will be communicated to the associated `Controls` or not. * Default is true. * @property {boolean} enableKeys Defines whether keyboard input will be communicated to the associated `Controls` or * not. Default is true. */ class StateControl extends THREE.EventDispatcher { constructor(view) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; super(); this._view = view; this._domElement = view.domElement; let enabled = true; Object.defineProperty(this, 'enabled', { get: () => enabled, set: value => { if (!value) { this.onKeyUp(); this.onPointerUp(); } enabled = value; } }); // Set to true to disable use of the keys let enableKeys = true; Object.defineProperty(this, 'enableKeys', { get: () => enableKeys, set: value => { if (!value) { this.onKeyUp(); } enableKeys = value; } }); this.NONE = {}; let currentState = this.NONE; Object.defineProperty(this, 'currentState', { get: () => currentState, set: newState => { if (currentState !== newState) { const previous = currentState; currentState = newState; this.dispatchEvent({ type: 'state-changed', viewCoords, previous }); } } }); // TODO : the 4 next properties should be made private when ES6 allows it this._clickTimeStamp = 0; this._lastMousePressed = { viewCoords: new THREE.Vector2() }; this._currentMousePressed = undefined; this._currentKeyPressed = undefined; this._onPointerDown = this.onPointerDown.bind(this); this._onPointerMove = this.onPointerMove.bind(this); this._onPointerUp = this.onPointerUp.bind(this); this._onMouseWheel = this.onMouseWheel.bind(this); this._onKeyDown = this.onKeyDown.bind(this); this._onKeyUp = this.onKeyUp.bind(this); this._onBlur = this.onBlur.bind(this); this._onContextMenu = this.onContextMenu.bind(this); this._domElement.addEventListener('pointerdown', this._onPointerDown, false); this._domElement.addEventListener('wheel', this._onMouseWheel, false); this._domElement.addEventListener('keydown', this._onKeyDown, false); this._domElement.addEventListener('keyup', this._onKeyUp, false); // Reset key/mouse when window loose focus this._domElement.addEventListener('blur', this._onBlur); // disable context menu when right-clicking this._domElement.addEventListener('contextmenu', this._onContextMenu, false); this.setFromOptions(options); } /** * get the state corresponding to the mouse button and the keyboard key. If the input relates to a trigger - a * single event which triggers movement, without the move of the mouse for instance -, dispatch a relevant event. * @param {Number} mouseButton The mouse button * @param {Number} keyboard The keyboard * @param {Boolean} [double] Value of the searched state `double` property * @return {State} the state corresponding */ inputToState(mouseButton, keyboard) { let double = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; for (const key of Object.keys(DEFAULT_STATES)) { const state = this[key]; if (state.enable && state.mouseButton === mouseButton && state.keyboard === keyboard && state.double === double) { // If the input relates to a state, returns it if (!state._trigger) { return state; } // If the input relates to a trigger (TRAVEL_IN, TRAVEL_OUT), dispatch a relevant event. this.dispatchEvent({ type: state._event, // Dont pass viewCoords if the input is only a keyboard input. viewCoords: mouseButton !== undefined && viewCoords, direction: state._direction }); } } return this.NONE; } /** * get the state corresponding to the number of finger on the pad * * @param {Number} finger The number of finger * @return {state} the state corresponding */ touchToState(finger) { for (const key of Object.keys(DEFAULT_STATES)) { const state = this[key]; if (state.enable && finger === state.finger) { return state; } } return this.NONE; } /** * Set the current StateControl {@link State} properties to given values. * @param {Object} options Object containing the `State` values to set current `StateControl` properties to. * The `enable` property do not necessarily need to be specified. In that case, the * previous value of this property will be kept for the new {@link State}. * * @example * // Switch bindings for PAN and MOVE_GLOBE actions, and disabling PANORAMIC movement : * view.controls.states.setFromOptions({ * PAN: { * mouseButton: itowns.THREE.MOUSE.LEFT, * }, * MOVE_GLOBE: { * mouseButton: itowns.THREE.MOUSE.RIGHT, * }, * PANORAMIC: { * enable: false, * }, * }; */ setFromOptions(options) { for (const state in DEFAULT_STATES) { if ({}.hasOwnProperty.call(DEFAULT_STATES, state)) { let newState = {}; newState = options[state] || this[state] || Object.assign(newState, DEFAULT_STATES[state]); // Copy the previous value of `enable` property if not defined in options if (options[state] && options[state].enable === undefined) { newState.enable = this[state].enable; } // If no value is provided for the `double` property, // defaults it to `false` instead of leaving it undefined newState.double = !!newState.double; // Copy the `_event` and `_trigger` properties newState._event = DEFAULT_STATES[state]._event; newState._trigger = DEFAULT_STATES[state]._trigger; newState._direction = DEFAULT_STATES[state]._direction; this[state] = newState; } } } // ---------- POINTER EVENTS : ---------- onPointerDown(event) { if (!this.enabled) { return; } viewCoords.copy(this._view.eventToViewCoords(event)); switch (event.pointerType) { case 'mouse': { this._currentMousePressed = event.button; if (this._currentKeyPressed === undefined) { if (event.ctrlKey) { this._currentKeyPressed = CONTROL_KEYS.CTRL; } else if (event.shiftKey) { this._currentKeyPressed = CONTROL_KEYS.SHIFT; } else if (event.metaKey) { this._currentKeyPressed = CONTROL_KEYS.META; } } this.currentState = this.inputToState(this._currentMousePressed, this._currentKeyPressed, // Detect if the mouse button was pressed less than 500 ms before, and if the cursor has not moved two much // since previous click. If so, set dblclick to true. event.timeStamp - this._clickTimeStamp < 500 && this._lastMousePressed.button === this._currentMousePressed && this._lastMousePressed.viewCoords.distanceTo(viewCoords) < 5); this._clickTimeStamp = event.timeStamp; this._lastMousePressed.button = this._currentMousePressed; this._lastMousePressed.viewCoords.copy(viewCoords); break; } // TODO : add touch event management default: } this._domElement.addEventListener('pointermove', this._onPointerMove, false); this._domElement.addEventListener('pointerup', this._onPointerUp, false); this._domElement.addEventListener('mouseleave', this._onPointerUp, false); } onPointerMove(event) { event.preventDefault(); if (!this.enabled) { return; } viewCoords.copy(this._view.eventToViewCoords(event)); switch (event.pointerType) { case 'mouse': this.dispatchEvent({ type: this.currentState._event, viewCoords }); break; // TODO : add touch event management default: } } onPointerUp() { if (!this.enabled) { return; } this._currentMousePressed = undefined; this._domElement.removeEventListener('pointermove', this._onPointerMove, false); this._domElement.removeEventListener('pointerup', this._onPointerUp, false); this._domElement.removeEventListener('mouseleave', this._onPointerUp, false); this.currentState = this.NONE; } // ---------- WHEEL EVENT : ---------- onMouseWheel(event) { event.preventDefault(); if (this.enabled && this.ZOOM.enable) { viewCoords.copy(this._view.eventToViewCoords(event)); this.currentState = this.ZOOM; this.dispatchEvent({ type: this.ZOOM._event, delta: event.deltaY, viewCoords }); } } // ---------- KEYBOARD EVENTS : ---------- onKeyDown(event) { if (!this.enabled || !this.enableKeys) { return; } this._currentKeyPressed = event.keyCode; this.inputToState(this._currentMousePressed, this._currentKeyPressed); } onKeyUp() { if (!this.enabled || !this.enableKeys) { return; } this._currentKeyPressed = undefined; if (this._currentMousePressed === undefined) { this.currentState = this.NONE; } } onBlur() { this.onKeyUp(); this.onPointerUp(); } onContextMenu(event) { event.preventDefault(); } /** * Remove all event listeners created within this instance of `StateControl` */ dispose() { this._clickTimeStamp = 0; this._lastMousePressed = undefined; this._currentKeyPressed = undefined; this._domElement.removeEventListener('pointerdown', this._onPointerDown, false); this._domElement.removeEventListener('pointermove', this._onPointerMove, false); this._domElement.removeEventListener('pointerup', this._onPointerUp, false); this._domElement.removeEventListener('wheel', this._onMouseWheel, false); this._domElement.removeEventListener('keydown', this._onKeyDown, false); this._domElement.removeEventListener('keyup', this._onKeyUp, false); this._domElement.removeEventListener('blur', this._onBlur); this._domElement.removeEventListener('contextmenu', this._onContextMenu, false); } } export default StateControl;