Source: Scene/CameraEventAggregator.js

/*global define*/
define([
        '../Core/Cartesian2',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/destroyObject',
        '../Core/DeveloperError',
        '../Core/KeyboardEventModifier',
        '../Core/Math',
        '../Core/ScreenSpaceEventHandler',
        '../Core/ScreenSpaceEventType',
        './CameraEventType'
    ], function(
        Cartesian2,
        defined,
        defineProperties,
        destroyObject,
        DeveloperError,
        KeyboardEventModifier,
        CesiumMath,
        ScreenSpaceEventHandler,
        ScreenSpaceEventType,
        CameraEventType) {
    'use strict';

    function getKey(type, modifier) {
        var 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) {
        var key = getKey(CameraEventType.PINCH, modifier);

        var update = aggregator._update;
        var isDown = aggregator._isDown;
        var eventStartPosition = aggregator._eventStartPosition;
        var pressTime = aggregator._pressTime;
        var releaseTime = aggregator._releaseTime;

        update[key] = true;
        isDown[key] = false;
        eventStartPosition[key] = new Cartesian2();

        var 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.
                var angle = movement.angleAndHeight.endPosition.x;
                var prevAngle = movement.prevAngle;
                var 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) {
        var key = getKey(CameraEventType.WHEEL, modifier);

        var update = aggregator._update;
        update[key] = true;

        var movement = aggregator._movement[key];
        if (!defined(movement)) {
            movement = aggregator._movement[key] = {};
        }

        movement.startPosition = new Cartesian2();
        movement.endPosition = new Cartesian2();

        aggregator._eventHandler.setInputAction(function(delta) {
            // TODO: magic numbers
            var arcLength = 15.0 * CesiumMath.toRadians(delta);
            if (!update[key]) {
                movement.endPosition.y = movement.endPosition.y + arcLength;
            } else {
                Cartesian2.clone(Cartesian2.ZERO, movement.startPosition);
                movement.endPosition.x = 0.0;
                movement.endPosition.y = arcLength;
                update[key] = false;
            }
        }, ScreenSpaceEventType.WHEEL, modifier);
    }

    function listenMouseButtonDownUp(aggregator, modifier, type) {
        var key = getKey(type, modifier);

        var isDown = aggregator._isDown;
        var eventStartPosition = aggregator._eventStartPosition;
        var pressTime = aggregator._pressTime;
        var releaseTime = aggregator._releaseTime;

        isDown[key] = false;
        eventStartPosition[key] = new Cartesian2();

        var lastMovement = aggregator._lastMovement[key];
        if (!defined(lastMovement)) {
            lastMovement = aggregator._lastMovement[key] = {
                startPosition : new Cartesian2(),
                endPosition : new Cartesian2(),
                valid : false
            };
        }

        var down;
        var 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() {
            aggregator._buttonsDown = Math.max(aggregator._buttonsDown - 1, 0);
            isDown[key] = false;
            releaseTime[key] = new Date();
        }, up, modifier);
    }

    function cloneMouseMovement(mouseMovement, result) {
        Cartesian2.clone(mouseMovement.startPosition, result.startPosition);
        Cartesian2.clone(mouseMovement.endPosition, result.endPosition);
    }

    function listenMouseMove(aggregator, modifier) {
        var update = aggregator._update;
        var movement = aggregator._movement;
        var lastMovement = aggregator._lastMovement;
        var isDown = aggregator._isDown;

        for ( var typeName in CameraEventType) {
            if (CameraEventType.hasOwnProperty(typeName)) {
                var type = CameraEventType[typeName];
                if (defined(type)) {
                    var 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 ( var typeName in CameraEventType) {
                if (CameraEventType.hasOwnProperty(typeName)) {
                    var type = CameraEventType[typeName];
                    if (defined(type)) {
                        var key = getKey(type, modifier);
                        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 {Canvas} [element=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, true);

        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 ( var modifierName in KeyboardEventModifier) {
            if (KeyboardEventModifier.hasOwnProperty(modifierName)) {
                var 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);
                }
            }
        }
    }

    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() {
                var 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');

        var 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');

        var key = getKey(type, modifier);
        var 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');

        var key = getKey(type, modifier);
        var 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');

        var 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;
        }

        var 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');

        var 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');

        var 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 ( var 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.
     *
     * @returns {undefined}
     *
     * @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);
    };

    return CameraEventAggregator;
});