UNPKG

@cesium/engine

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

620 lines (555 loc) 19 kB
import Cartesian2 from "../Core/Cartesian2.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import DeveloperError from "../Core/DeveloperError.js"; import KeyboardEventModifier from "../Core/KeyboardEventModifier.js"; import CesiumMath from "../Core/Math.js"; import ScreenSpaceEventHandler from "../Core/ScreenSpaceEventHandler.js"; import ScreenSpaceEventType from "../Core/ScreenSpaceEventType.js"; import CameraEventType from "./CameraEventType.js"; function getKey(type, modifier) { let key = `${type}`; if (defined(modifier)) { key += `+${modifier}`; } return key; } function clonePinchMovement(pinchMovement, result) { Cartesian2.clone( pinchMovement.distance.startPosition, result.distance.startPosition, ); Cartesian2.clone( pinchMovement.distance.endPosition, result.distance.endPosition, ); Cartesian2.clone( pinchMovement.angleAndHeight.startPosition, result.angleAndHeight.startPosition, ); Cartesian2.clone( pinchMovement.angleAndHeight.endPosition, result.angleAndHeight.endPosition, ); } function listenToPinch(aggregator, modifier, canvas) { const key = getKey(CameraEventType.PINCH, modifier); const update = aggregator._update; const isDown = aggregator._isDown; const eventStartPosition = aggregator._eventStartPosition; const pressTime = aggregator._pressTime; const releaseTime = aggregator._releaseTime; update[key] = true; isDown[key] = false; eventStartPosition[key] = new Cartesian2(); let movement = aggregator._movement[key]; if (!defined(movement)) { movement = aggregator._movement[key] = {}; } movement.distance = { startPosition: new Cartesian2(), endPosition: new Cartesian2(), }; movement.angleAndHeight = { startPosition: new Cartesian2(), endPosition: new Cartesian2(), }; movement.prevAngle = 0.0; aggregator._eventHandler.setInputAction( function (event) { aggregator._buttonsDown++; isDown[key] = true; pressTime[key] = new Date(); // Compute center position and store as start point. Cartesian2.lerp( event.position1, event.position2, 0.5, eventStartPosition[key], ); }, ScreenSpaceEventType.PINCH_START, modifier, ); aggregator._eventHandler.setInputAction( function () { aggregator._buttonsDown = Math.max(aggregator._buttonsDown - 1, 0); isDown[key] = false; releaseTime[key] = new Date(); }, ScreenSpaceEventType.PINCH_END, modifier, ); aggregator._eventHandler.setInputAction( function (mouseMovement) { if (isDown[key]) { // Aggregate several input events into a single animation frame. if (!update[key]) { Cartesian2.clone( mouseMovement.distance.endPosition, movement.distance.endPosition, ); Cartesian2.clone( mouseMovement.angleAndHeight.endPosition, movement.angleAndHeight.endPosition, ); } else { clonePinchMovement(mouseMovement, movement); update[key] = false; movement.prevAngle = movement.angleAndHeight.startPosition.x; } // Make sure our aggregation of angles does not "flip" over 360 degrees. let angle = movement.angleAndHeight.endPosition.x; const prevAngle = movement.prevAngle; const TwoPI = Math.PI * 2; while (angle >= prevAngle + Math.PI) { angle -= TwoPI; } while (angle < prevAngle - Math.PI) { angle += TwoPI; } movement.angleAndHeight.endPosition.x = (-angle * canvas.clientWidth) / 12; movement.angleAndHeight.startPosition.x = (-prevAngle * canvas.clientWidth) / 12; } }, ScreenSpaceEventType.PINCH_MOVE, modifier, ); } function listenToWheel(aggregator, modifier) { const key = getKey(CameraEventType.WHEEL, modifier); const pressTime = aggregator._pressTime; const releaseTime = aggregator._releaseTime; const update = aggregator._update; update[key] = true; let movement = aggregator._movement[key]; if (!defined(movement)) { movement = aggregator._movement[key] = {}; } let lastMovement = aggregator._lastMovement[key]; if (!defined(lastMovement)) { lastMovement = aggregator._lastMovement[key] = { startPosition: new Cartesian2(), endPosition: new Cartesian2(), valid: false, }; } movement.startPosition = new Cartesian2(); Cartesian2.clone(Cartesian2.ZERO, movement.startPosition); movement.endPosition = new Cartesian2(); aggregator._eventHandler.setInputAction( function (delta) { const arcLength = 7.5 * CesiumMath.toRadians(delta); pressTime[key] = releaseTime[key] = new Date(); movement.endPosition.x = 0.0; movement.endPosition.y = arcLength; Cartesian2.clone(movement.endPosition, lastMovement.endPosition); lastMovement.valid = true; update[key] = false; }, ScreenSpaceEventType.WHEEL, modifier, ); } function listenMouseButtonDownUp(aggregator, modifier, type) { const key = getKey(type, modifier); const isDown = aggregator._isDown; const eventStartPosition = aggregator._eventStartPosition; const pressTime = aggregator._pressTime; isDown[key] = false; eventStartPosition[key] = new Cartesian2(); let lastMovement = aggregator._lastMovement[key]; if (!defined(lastMovement)) { lastMovement = aggregator._lastMovement[key] = { startPosition: new Cartesian2(), endPosition: new Cartesian2(), valid: false, }; } let down; let up; if (type === CameraEventType.LEFT_DRAG) { down = ScreenSpaceEventType.LEFT_DOWN; up = ScreenSpaceEventType.LEFT_UP; } else if (type === CameraEventType.RIGHT_DRAG) { down = ScreenSpaceEventType.RIGHT_DOWN; up = ScreenSpaceEventType.RIGHT_UP; } else if (type === CameraEventType.MIDDLE_DRAG) { down = ScreenSpaceEventType.MIDDLE_DOWN; up = ScreenSpaceEventType.MIDDLE_UP; } aggregator._eventHandler.setInputAction( function (event) { aggregator._buttonsDown++; lastMovement.valid = false; isDown[key] = true; pressTime[key] = new Date(); Cartesian2.clone(event.position, eventStartPosition[key]); }, down, modifier, ); aggregator._eventHandler.setInputAction( function () { cancelMouseDownAction(getKey(type, undefined), aggregator); for (const modifier of Object.values(KeyboardEventModifier)) { const cancelKey = getKey(type, modifier); cancelMouseDownAction(cancelKey, aggregator); } }, up, modifier, ); } function cancelMouseDownAction(cancelKey, aggregator) { const releaseTime = aggregator._releaseTime; const isDown = aggregator._isDown; if (isDown[cancelKey]) { aggregator._buttonsDown = Math.max(aggregator._buttonsDown - 1, 0); } isDown[cancelKey] = false; releaseTime[cancelKey] = new Date(); } function cloneMouseMovement(mouseMovement, result) { Cartesian2.clone(mouseMovement.startPosition, result.startPosition); Cartesian2.clone(mouseMovement.endPosition, result.endPosition); } function refreshMouseDownStatus(type, modifier, aggregator) { // first: Judge if the mouse is pressed const isDown = aggregator._isDown; let anyButtonIsDown = false; const currentKey = getKey(type, modifier); for (const [downKey, downValue] of Object.entries(isDown)) { if (downKey.startsWith(type) && downValue && downKey !== currentKey) { anyButtonIsDown = true; cancelMouseDownAction(downKey, aggregator); } } if (!anyButtonIsDown) { return; } // second: If it is pressed, it will be transferred to the current modifier. const pressTime = aggregator._pressTime; let lastMovement = aggregator._lastMovement[currentKey]; if (!defined(lastMovement)) { lastMovement = aggregator._lastMovement[currentKey] = { startPosition: new Cartesian2(), endPosition: new Cartesian2(), valid: false, }; } aggregator._buttonsDown++; lastMovement.valid = false; isDown[currentKey] = true; pressTime[currentKey] = new Date(); } function listenMouseMove(aggregator, modifier) { const update = aggregator._update; const movement = aggregator._movement; const lastMovement = aggregator._lastMovement; const isDown = aggregator._isDown; for (const typeName in CameraEventType) { if (CameraEventType.hasOwnProperty(typeName)) { const type = CameraEventType[typeName]; if (defined(type)) { const key = getKey(type, modifier); update[key] = true; if (!defined(aggregator._lastMovement[key])) { aggregator._lastMovement[key] = { startPosition: new Cartesian2(), endPosition: new Cartesian2(), valid: false, }; } if (!defined(aggregator._movement[key])) { aggregator._movement[key] = { startPosition: new Cartesian2(), endPosition: new Cartesian2(), }; } } } } aggregator._eventHandler.setInputAction( function (mouseMovement) { for (const typeName in CameraEventType) { if (CameraEventType.hasOwnProperty(typeName)) { const type = CameraEventType[typeName]; if (defined(type)) { const key = getKey(type, modifier); refreshMouseDownStatus(type, modifier, aggregator); if (isDown[key]) { if (!update[key]) { Cartesian2.clone( mouseMovement.endPosition, movement[key].endPosition, ); } else { cloneMouseMovement(movement[key], lastMovement[key]); lastMovement[key].valid = true; cloneMouseMovement(mouseMovement, movement[key]); update[key] = false; } } } } } Cartesian2.clone( mouseMovement.endPosition, aggregator._currentMousePosition, ); }, ScreenSpaceEventType.MOUSE_MOVE, modifier, ); } /** * Aggregates input events. For example, suppose the following inputs are received between frames: * left mouse button down, mouse move, mouse move, left mouse button up. These events will be aggregated into * one event with a start and end position of the mouse. * * @alias CameraEventAggregator * @constructor * * @param {HTMLCanvasElement} [canvas=document] The element to handle events for. * * @see ScreenSpaceEventHandler */ function CameraEventAggregator(canvas) { //>>includeStart('debug', pragmas.debug); if (!defined(canvas)) { throw new DeveloperError("canvas is required."); } //>>includeEnd('debug'); this._eventHandler = new ScreenSpaceEventHandler(canvas); this._update = {}; this._movement = {}; this._lastMovement = {}; this._isDown = {}; this._eventStartPosition = {}; this._pressTime = {}; this._releaseTime = {}; this._buttonsDown = 0; this._currentMousePosition = new Cartesian2(); listenToWheel(this, undefined); listenToPinch(this, undefined, canvas); listenMouseButtonDownUp(this, undefined, CameraEventType.LEFT_DRAG); listenMouseButtonDownUp(this, undefined, CameraEventType.RIGHT_DRAG); listenMouseButtonDownUp(this, undefined, CameraEventType.MIDDLE_DRAG); listenMouseMove(this, undefined); for (const modifierName in KeyboardEventModifier) { if (KeyboardEventModifier.hasOwnProperty(modifierName)) { const modifier = KeyboardEventModifier[modifierName]; if (defined(modifier)) { listenToWheel(this, modifier); listenToPinch(this, modifier, canvas); listenMouseButtonDownUp(this, modifier, CameraEventType.LEFT_DRAG); listenMouseButtonDownUp(this, modifier, CameraEventType.RIGHT_DRAG); listenMouseButtonDownUp(this, modifier, CameraEventType.MIDDLE_DRAG); listenMouseMove(this, modifier); } } } } Object.defineProperties(CameraEventAggregator.prototype, { /** * Gets the current mouse position. * @memberof CameraEventAggregator.prototype * @type {Cartesian2} */ currentMousePosition: { get: function () { return this._currentMousePosition; }, }, /** * Gets whether any mouse button is down, a touch has started, or the wheel has been moved. * @memberof CameraEventAggregator.prototype * @type {boolean} */ anyButtonDown: { get: function () { const wheelMoved = !this._update[getKey(CameraEventType.WHEEL)] || !this._update[ getKey(CameraEventType.WHEEL, KeyboardEventModifier.SHIFT) ] || !this._update[ getKey(CameraEventType.WHEEL, KeyboardEventModifier.CTRL) ] || !this._update[getKey(CameraEventType.WHEEL, KeyboardEventModifier.ALT)]; return this._buttonsDown > 0 || wheelMoved; }, }, }); /** * Gets if a mouse button down or touch has started and has been moved. * * @param {CameraEventType} type The camera event type. * @param {KeyboardEventModifier} [modifier] The keyboard modifier. * @returns {boolean} Returns <code>true</code> if a mouse button down or touch has started and has been moved; otherwise, <code>false</code> */ CameraEventAggregator.prototype.isMoving = function (type, modifier) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError("type is required."); } //>>includeEnd('debug'); const key = getKey(type, modifier); return !this._update[key]; }; /** * Gets the aggregated start and end position of the current event. * * @param {CameraEventType} type The camera event type. * @param {KeyboardEventModifier} [modifier] The keyboard modifier. * @returns {object} An object with two {@link Cartesian2} properties: <code>startPosition</code> and <code>endPosition</code>. */ CameraEventAggregator.prototype.getMovement = function (type, modifier) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError("type is required."); } //>>includeEnd('debug'); const key = getKey(type, modifier); const movement = this._movement[key]; return movement; }; /** * Gets the start and end position of the last move event (not the aggregated event). * * @param {CameraEventType} type The camera event type. * @param {KeyboardEventModifier} [modifier] The keyboard modifier. * @returns {object|undefined} An object with two {@link Cartesian2} properties: <code>startPosition</code> and <code>endPosition</code> or <code>undefined</code>. */ CameraEventAggregator.prototype.getLastMovement = function (type, modifier) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError("type is required."); } //>>includeEnd('debug'); const key = getKey(type, modifier); const lastMovement = this._lastMovement[key]; if (lastMovement.valid) { return lastMovement; } return undefined; }; /** * Gets whether the mouse button is down or a touch has started. * * @param {CameraEventType} type The camera event type. * @param {KeyboardEventModifier} [modifier] The keyboard modifier. * @returns {boolean} Whether the mouse button is down or a touch has started. */ CameraEventAggregator.prototype.isButtonDown = function (type, modifier) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError("type is required."); } //>>includeEnd('debug'); const key = getKey(type, modifier); return this._isDown[key]; }; /** * Gets the mouse position that started the aggregation. * * @param {CameraEventType} type The camera event type. * @param {KeyboardEventModifier} [modifier] The keyboard modifier. * @returns {Cartesian2} The mouse position. */ CameraEventAggregator.prototype.getStartMousePosition = function ( type, modifier, ) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError("type is required."); } //>>includeEnd('debug'); if (type === CameraEventType.WHEEL) { return this._currentMousePosition; } const key = getKey(type, modifier); return this._eventStartPosition[key]; }; /** * Gets the time the button was pressed or the touch was started. * * @param {CameraEventType} type The camera event type. * @param {KeyboardEventModifier} [modifier] The keyboard modifier. * @returns {Date} The time the button was pressed or the touch was started. */ CameraEventAggregator.prototype.getButtonPressTime = function (type, modifier) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError("type is required."); } //>>includeEnd('debug'); const key = getKey(type, modifier); return this._pressTime[key]; }; /** * Gets the time the button was released or the touch was ended. * * @param {CameraEventType} type The camera event type. * @param {KeyboardEventModifier} [modifier] The keyboard modifier. * @returns {Date} The time the button was released or the touch was ended. */ CameraEventAggregator.prototype.getButtonReleaseTime = function ( type, modifier, ) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError("type is required."); } //>>includeEnd('debug'); const key = getKey(type, modifier); return this._releaseTime[key]; }; /** * Signals that all of the events have been handled and the aggregator should be reset to handle new events. */ CameraEventAggregator.prototype.reset = function () { for (const name in this._update) { if (this._update.hasOwnProperty(name)) { this._update[name] = true; } } }; /** * Returns true if this object was destroyed; otherwise, false. * <br /><br /> * If this object was destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. * * @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>. * * @see CameraEventAggregator#destroy */ CameraEventAggregator.prototype.isDestroyed = function () { return false; }; /** * Removes mouse listeners held by this object. * <br /><br /> * Once an object is destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore, * assign the return value (<code>undefined</code>) to the object as done in the example. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * * @example * handler = handler && handler.destroy(); * * @see CameraEventAggregator#isDestroyed */ CameraEventAggregator.prototype.destroy = function () { this._eventHandler = this._eventHandler && this._eventHandler.destroy(); return destroyObject(this); }; export default CameraEventAggregator;