@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
JavaScript
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;