src/controller/MoveController.js

goog.module('gep.controller.MoveController');

const {getBrowserPoints} = goog.require('nbsrc.utils.parser');

const {listen,unlisten,Event,EventTarget,EventType,KeyEvent} = goog.require('goog.events');
const KeyCodes = goog.require('goog.events.KeyCodes');
const {getWindow} = goog.require('goog.dom');
const {Coordinate} = goog.require('goog.math');
const Vec3 = goog.require('goog.math.Vec3');
const classlist = goog.require('goog.dom.classlist');

/**
 * The controller is used for pure monitoring of Interactions events. It only stores interaction states and values for the motion calculation. The application of the motion to the corresponding object or the calculation and processing does not take place in the controller. This is done in the main render process of the WebGLRenderer in {@Link Exhibition#updateScene_}.
 * @extends {EventTarget}
 */
class MoveController extends EventTarget
{
    /**
     * @param {number|Map<string,number>} acceleration List for defined acceleration values. It can be a single value or a list according to self-defined keys.
     * @param {number|Map<string,number>} friction List for defined friction values. It can be a single value or a list according to self-defined keys.
     */
    constructor(acceleration, friction)
    {
        super();

        /**
         * Element on which the event listeners are applied.
         * @type {Element}
         * @protected
         */
        this.target_ = null;

        /**
         * Indicates whether the keyboard (WASD and arrow keys) is also used.
         * @type {boolean}
         * @protected
         */
        this.keysAreEnabled_ = false;

        /**
         * Indicates whether zooming (scroll wheel and 2 finger pinch) is enabled.
         * @type {boolean}
         * @protected
         */
        this.zoomingIsEnabled_ = false;

        /**
         * Specifies whether mouse events should be captured.
         * @type {boolean}
         * @protected
         */
        this.mouseIsEnabled_ = false;

        /**
         * Specifies whether it should be possible that a gamepad can also be used.
         * @type {boolean}
         * @protected
         */
        this.gamepadIsEnabled_ = false;

        /**
         * List of {@Link MoveType} activation states
         * @type {Map<string,boolean>}
         * @protected
         */
        this.states_ = new Map([
            [MoveType.LEFT, false],
            [MoveType.RIGHT, false],
            [MoveType.UP, false],
            [MoveType.DOWN, false],
            [MoveType.ZOOM_IN, false],
            [MoveType.ZOOM_OUT, false],
            [MoveType.TOUCH_LEFT, false],
            [MoveType.TOUCH_RIGHT, false],
            [MoveType.TOUCH_UP, false],
            [MoveType.TOUCH_DOWN, false],
            [MoveType.TOUCH_ZOOM_IN, false],
            [MoveType.TOUCH_ZOOM_OUT, false],
            [MoveType.PAD_LEFT, false],
            [MoveType.PAD_RIGHT, false],
            [MoveType.PAD_UP, false],
            [MoveType.PAD_DOWN, false],
        ]);

        /**
         * Reference to the points where the interaction on the target started.
         * @type {Array<Coordinate>}
         * @protected
         */
        this.interactionStartPoints_ = null;

        /**
         * Reference to the points during the interaction of the user.
         * @type {Array<Coordinate>}
         * @protected
         */
        this.interactionMovePoints_ = null;

        /**
         * Reference to distance of the current points to the last saved points during the interaction.
         * @type {Array<Coordinate>}
         * @protected
         */
        this.interactionMoveDistances_ = [];

        /**
         * Holds the value of the distance changes during interaction.
         * @type {number}
         * @private
         */
        this.interactionZoomDistance_ = 0;

        /**
         * Reference to the points where the interaction on the target ended.
         * @type {Array<Coordinate>}
         * @protected
         */
        this.interactionEndPoints_ = null;

        /**
         * Interaction distance between two points which is used for the check of 2 finger pinch zooming
         * @type {number}
         * @private
         */
        this.interactionDistance_ = 0;

        /**
         * Saves whether it was mouse or touch interaction
         * @type {string}
         * @protected
         */
        this.interactionType_ = '';

        /**
         * List for defined acceleration values. These can be used for a smooth motion calculation.
         * It can be a single value or a list according to self-defined keys.
         * @type {number|Map<string,number>}
         * @protected
         */
        this.acceleration_ = acceleration;

        /**
         * List for defined friction values. These can be used for a smooth motion calculation.
         * It can be a single value or a list according to self-defined keys.
         * @type {number|Map<string,number>}
         * @protected
         */
        this.friction_ = friction;

        /**
         * Motion velocity value.
         * @type {Vec3|Object}
         */
        this.velocity = new Vec3(0,0,0);

        /**
         * Timeout instance for delayed zoom state changes.
         * @type {number}
         * @protected
         */
        this.zoomTimeout_ = 0;

        /**
         * Value that indicates whether the window is in focus by the user.
         * @type {boolean}
         * @protected
         */
        this.isFocused_ = true;

        /**
         * Tolerance values to treat a touch move as a motion.
         * @type {Coordinate|Object}
         */
        this.touchMoveTolerance = new Coordinate(10, 5);

        /**
         * Value of the distance of the current interaction point to the start interaction point.
         * @type {Coordinate}
         * @private
         */
        this.touchStartMoveDistance_ = new Coordinate(0, 0);

        /**
         * Gamepad instance
         * @type {Gamepad}
         * @private
         */
        this.gamepad_ = null;

        /**
         * List of the gamepad axes
         * @type {Map<string,number>}
         * @private
         */
        this.gamepadAxes_ = new Map([
            [MoveType.PAD_LEFT_HORIZONTAL, 0],
            [MoveType.PAD_LEFT_VERTICAL, 0],
            [MoveType.PAD_RIGHT_HORIZONTAL, 0],
            [MoveType.PAD_RIGHT_VERTICAL, 0],
        ]);

        window.addEventListener('gamepadconnected', (event) => {
            if (event['gamepad'] && this.gamepadIsEnabled_)
                this.checkGamepad(event['gamepad']);
        });

        /**
         * List of the current active (pressed) keys
         * @type {Array<number>}
         * @private
         */
        this.activKeys_ = [];
    }

    /**
     * Method activates the listener and thus the monitoring of user interaction with the application.
     * @param {!Element} target Element on which the event listeners are applied.
     * @param {boolean=} disableKeys Indicates whether the keyboard (WASD and arrow keys) is also used.
     * @param {boolean=} disableZooming Indicates whether zooming (scroll wheel and 2 finger pinch) is enabled.
     * @param {boolean=} disableMouse Specifies whether mouse events should be captured.
     * @param {boolean=} enableGamepad Specifies whether it should be monitored that a gamepad can also be used.
     */
    activate(target, disableKeys = false, disableZooming = false, disableMouse = false, enableGamepad = false)
    {
        this.target_ = target;
        this.keysAreEnabled_ = !disableKeys;
        this.zoomingIsEnabled_ = !disableZooming;
        this.mouseIsEnabled_ = !disableMouse;
        this.gamepadIsEnabled_ = enableGamepad;

        if('ontouchstart' in getWindow())
        {
            listen(this.target_, EventType.TOUCHSTART, this.handleUserDown_, false, this);
            listen(this.target_, EventType.TOUCHMOVE, this.handleUserMove_, false, this);
            listen(this.target_, EventType.TOUCHEND, this.handleUserUp_, false, this);
        }
        else if(getWindow()['PointerEvent'] != null)
        {
            listen(this.target_, EventType.POINTERDOWN, this.handleUserDown_, false, this);
            listen(this.target_, EventType.POINTERMOVE, this.handleUserMove_, false, this);
            listen(this.target_, EventType.POINTERUP, this.handleUserUp_, false, this);
        }
        else if(getWindow()['MSPointerEvent'] != null)
        {
            listen(this.target_, EventType.MSPOINTERDOWN, this.handleUserDown_, false, this);
            listen(this.target_, EventType.MSPOINTERMOVE, this.handleUserMove_, false, this);
            listen(this.target_, EventType.MSPOINTERUP, this.handleUserUp_, false, this);
        }

        if(this.mouseIsEnabled_)
        {
            listen(this.target_, EventType.MOUSEDOWN, this.handleUserDown_, false, this);
            listen(this.target_, EventType.MOUSEMOVE, this.handleUserMove_, false, this);
            listen(this.target_, EventType.MOUSEUP, this.handleUserUp_, false, this);
            listen(this.target_, EventType.MOUSELEAVE, this.handleUserUp_, false, this);
        }
        listen(window, EventType.BLUR, this.handleUserFocusOut_, false, this);
        listen(window, EventType.FOCUS, this.handleUserFocusIn_, false, this);

        if(this.keysAreEnabled_)
        {
            listen(document.documentElement, EventType.KEYDOWN, this.handleKeyDown_, false, this);
            listen(document.documentElement, EventType.KEYUP, this.handleKeyUp_, false, this);
        }

        if(this.mouseIsEnabled_ || this.zoomingIsEnabled_)
        {
            listen(document.documentElement, EventType.WHEEL, this.handleUserScroll_, false, this);
        }
    }

    /**
     * Method deactivates the listener. It will stop the monitoring of user interaction with the application.
     */
    deactivate()
    {
        if('ontouchstart' in getWindow())
        {
            unlisten(this.target_, EventType.TOUCHSTART, this.handleUserDown_, false, this);
            unlisten(this.target_, EventType.TOUCHMOVE, this.handleUserMove_, false, this);
            unlisten(this.target_, EventType.TOUCHEND, this.handleUserUp_, false, this);
        }
        else if(getWindow()['PointerEvent'] != null)
        {
            unlisten(this.target_, EventType.POINTERDOWN, this.handleUserDown_, false, this);
            unlisten(this.target_, EventType.POINTERMOVE, this.handleUserMove_, false, this);
            unlisten(this.target_, EventType.POINTERUP, this.handleUserUp_, false, this);
        }
        else if(getWindow()['MSPointerEvent'] != null)
        {
            unlisten(this.target_, EventType.MSPOINTERDOWN, this.handleUserDown_, false, this);
            unlisten(this.target_, EventType.MSPOINTERMOVE, this.handleUserMove_, false, this);
            unlisten(this.target_, EventType.MSPOINTERUP, this.handleUserUp_, false, this);
        }

        if(this.mouseIsEnabled_)
        {
            unlisten(this.target_, EventType.MOUSEDOWN, this.handleUserDown_, false, this);
            unlisten(this.target_, EventType.MOUSEMOVE, this.handleUserMove_, false, this);
            unlisten(this.target_, EventType.MOUSEUP, this.handleUserUp_, false, this);
            unlisten(this.target_, EventType.MOUSELEAVE, this.handleUserUp_, false, this);
        }
        unlisten(window, EventType.BLUR, this.handleUserFocusOut_, false, this);
        unlisten(window, EventType.FOCUS, this.handleUserFocusIn_, false, this);

        if(this.keysAreEnabled_)
        {
            unlisten(document.documentElement, EventType.KEYDOWN, this.handleKeyDown_, false, this);
            unlisten(document.documentElement, EventType.KEYUP, this.handleKeyUp_, false, this);
        }

        if(this.mouseIsEnabled_ || this.zoomingIsEnabled_)
        {
            unlisten(document.documentElement, EventType.WHEEL, this.handleUserScroll_, false, this);
        }
    }

    /**
     * Method monitors the window focus in event.
     * @protected
     */
    handleUserFocusIn_()
    {
        this.isFocused_ = true;
    }

    /**
     * Method monitors the window focus out event.
     * @protected
     */
    handleUserFocusOut_()
    {
        this.handleUserUp_(null);
        this.isFocused_ = false;
    }

    /**
     * Method monitors the document keyboard key down event.
     * @param {KeyEvent} event
     * @protected
     */
    handleKeyDown_(event)
    {
        if(!this.isFocused_)
            return;

        if(this.activKeys_.indexOf(event.keyCode) == -1)
            this.activKeys_.push(event.keyCode);

        switch(event.keyCode)
        {
            case KeyCodes.LEFT:
            case KeyCodes.A:
                if(this.states_.get(MoveType.LEFT) !== true)
                {
                    this.states_.set(MoveType.LEFT, true);
                    this.states_.set(MoveType.RIGHT, false);
                    this.dispatchEvent(new Event(EventType.CHANGE));
                }
                break;
            case KeyCodes.RIGHT:
            case KeyCodes.D:
                if(this.states_.get(MoveType.RIGHT) !== true)
                {
                    this.states_.set(MoveType.LEFT, false);
                    this.states_.set(MoveType.RIGHT, true);
                    this.dispatchEvent(new Event(EventType.CHANGE));
                }
                break;
            case KeyCodes.UP:
            case KeyCodes.W:
                if(this.states_.get(MoveType.UP) !== true)
                {
                    this.states_.set(MoveType.UP, true);
                    this.states_.set(MoveType.DOWN, false);
                    this.dispatchEvent(new Event(EventType.CHANGE));
                }
                break;
            case KeyCodes.DOWN:
            case KeyCodes.S:
                if(this.states_.get(MoveType.DOWN) !== true)
                {
                    this.states_.set(MoveType.UP, false);
                    this.states_.set(MoveType.DOWN, true);
                    this.dispatchEvent(new Event(EventType.CHANGE));
                }
                break;
        }
    }

    /**
     * Method monitors the document keyboard key up event.
     * @param {KeyEvent} event
     * @protected
     */
    handleKeyUp_(event)
    {
        let index = this.activKeys_.indexOf(event.keyCode);
        if(index != -1)
            this.activKeys_.splice(index, 1);

        switch(event.keyCode)
        {
            case KeyCodes.LEFT:
            case KeyCodes.A:
                this.states_.set(MoveType.LEFT, false);
                this.dispatchEvent(new Event(EventType.CHANGE));
                break;
            case KeyCodes.RIGHT:
            case KeyCodes.D:
                this.states_.set(MoveType.RIGHT, false);
                this.dispatchEvent(new Event(EventType.CHANGE));
                break;
            case KeyCodes.UP:
            case KeyCodes.W:
                this.states_.set(MoveType.UP, false);
                this.dispatchEvent(new Event(EventType.CHANGE));
                break;
            case KeyCodes.DOWN:
            case KeyCodes.S:
                this.states_.set(MoveType.DOWN, false);
                this.dispatchEvent(new Event(EventType.CHANGE));
                break;
        }
    }

    /**
     * Method monitors the window scroll wheel event.
     * @param {Event} event
     * @private
     */
    handleUserScroll_(event)
    {
        let deltaY = event.getBrowserEvent().deltaY;
        if(deltaY < 0)
        {
            if(this.states_.get(MoveType.ZOOM_IN) !== true)
            {
                this.states_.set(MoveType.ZOOM_IN, true);
                this.states_.set(MoveType.ZOOM_OUT, false);
                this.dispatchEvent(new Event(EventType.CHANGE));
            }
        }
        else
        {
            if(this.states_.get(MoveType.ZOOM_OUT) !== true)
            {
                this.states_.set(MoveType.ZOOM_IN, false);
                this.states_.set(MoveType.ZOOM_OUT, true);
                this.dispatchEvent(new Event(EventType.CHANGE));
            }
        }

        clearTimeout(this.zoomTimeout_);
        this.zoomTimeout_ = setTimeout(() => {
            this.states_.set(MoveType.ZOOM_IN, false);
            this.states_.set(MoveType.ZOOM_OUT, false);
            this.dispatchEvent(new Event(EventType.CHANGE));
        }, 300);
    }

    /**
     * Method monitors the target (pointer|mouse|touch) down event.
     * @param {Event} event
     * @protected
     */
    handleUserDown_(event)
    {
        if(classlist.contains(document.documentElement, 'is-mouse-mode') && [EventType.POINTERDOWN, EventType.MSPOINTERDOWN].indexOf(event.type) !== -1)
            return;

        this.isFocused_ = true;
        let points = getBrowserPoints(event);
        if(!this.interactionStartPoints_ || points.length != this.interactionStartPoints_.length)
        {
            this.states_.set(MoveType.TOUCH_LEFT, false);
            this.states_.set(MoveType.TOUCH_RIGHT, false);
            this.states_.set(MoveType.TOUCH_UP, false);
            this.states_.set(MoveType.TOUCH_DOWN, false);
            this.states_.set(MoveType.TOUCH_ZOOM_IN, false);
            this.states_.set(MoveType.TOUCH_ZOOM_OUT, false);

            this.interactionType_ = event.type == EventType.MOUSEDOWN ? 'MOUSE' : 'TOUCH';
            this.interactionStartPoints_ = points;
            this.interactionDistance_ = points.length > 1 ? Math.sqrt(Math.pow(points[0].x-points[1].x, 2) + Math.pow(points[0].y-points[1].y, 2)) : 0;
            this.interactionMoveDistances_ = [];
            this.interactionZoomDistance_ = 0;
            this.touchStartMoveDistance_ = new Coordinate(0,0);
        }
    }

    /**
     * Method monitors the target (pointer|mouse|touch) move event.
     * @param {Event} event
     * @protected
     */
    handleUserMove_(event)
    {
        if(this.interactionStartPoints_ && this.interactionStartPoints_.length > 0 &&
           ((event.type == EventType.MOUSEMOVE && this.interactionType_ == 'MOUSE') ||
            (event.type != EventType.MOUSEMOVE && this.interactionType_ != 'MOUSE')))
        {
            event.preventDefault();

            let movePoints = getBrowserPoints(event);
            let lastPoints = this.interactionMovePoints_ ? this.interactionMovePoints_ : this.interactionStartPoints_;
            if(movePoints.length > 1 && lastPoints.length > 1)
            {
                let lastDistance = Math.sqrt(Math.pow(lastPoints[0].x-lastPoints[1].x, 2) + Math.pow(lastPoints[0].y-lastPoints[1].y, 2));
                let currentDistance = Math.sqrt(Math.pow(movePoints[0].x-movePoints[1].x, 2) + Math.pow(movePoints[0].y-movePoints[1].y, 2));
                this.interactionZoomDistance_ = currentDistance - lastDistance;
            }
            movePoints.forEach((movePoint, index) => {
                this.interactionMoveDistances_[index] = new Coordinate(
                    this.interactionMovePoints_ && this.interactionMovePoints_[index] ? movePoint.x - this.interactionMovePoints_[index].x : 0,
                    this.interactionMovePoints_ && this.interactionMovePoints_[index] ? movePoint.y - this.interactionMovePoints_[index].y : 0,
                );
            });
            this.interactionMovePoints_ = movePoints;

            //MOVING
            if(this.interactionStartPoints_.length == 1)
            {
                let directionX = this.interactionMovePoints_[0].x > this.interactionStartPoints_[0].x ? 1 : this.interactionMovePoints_[0].x < this.interactionStartPoints_[0].x ? -1 : 0;
                let directionY = this.interactionMovePoints_[0].y > this.interactionStartPoints_[0].y ? -1 : this.interactionMovePoints_[0].y < this.interactionStartPoints_[0].y ? 1 : 0;
                let distanceX = Math.abs(this.interactionMovePoints_[0].x - this.interactionStartPoints_[0].x);
                let distanceY = Math.abs(this.interactionMovePoints_[0].y - this.interactionStartPoints_[0].y);

                if(distanceX >= this.touchMoveTolerance.x)
                {
                    this.touchStartMoveDistance_.x = distanceX;
                    if(directionX < 0)
                    {
                        this.states_.set(MoveType.TOUCH_LEFT, true);
                        this.states_.set(MoveType.TOUCH_RIGHT, false);
                        this.dispatchEvent(new Event(EventType.CHANGE));
                    }
                    else
                    {
                        this.states_.set(MoveType.TOUCH_LEFT, false);
                        this.states_.set(MoveType.TOUCH_RIGHT, true);
                        this.dispatchEvent(new Event(EventType.CHANGE));
                    }
                }
                else
                {
                    this.touchStartMoveDistance_.x = 0;
                    this.states_.set(MoveType.TOUCH_LEFT, false);
                    this.states_.set(MoveType.TOUCH_RIGHT, false);
                    this.dispatchEvent(new Event(EventType.CHANGE));
                }

                if(distanceY >= this.touchMoveTolerance.y)
                {
                    this.touchStartMoveDistance_.y = distanceY;
                    if(directionY > 0)
                    {
                        this.states_.set(MoveType.TOUCH_UP, true);
                        this.states_.set(MoveType.TOUCH_DOWN, false);
                        this.dispatchEvent(new Event(EventType.CHANGE));
                    }
                    else
                    {
                        this.states_.set(MoveType.TOUCH_UP, false);
                        this.states_.set(MoveType.TOUCH_DOWN, true);
                        this.dispatchEvent(new Event(EventType.CHANGE));
                    }
                }
                else
                {
                    this.touchStartMoveDistance_.y = 0;
                    this.states_.set(MoveType.TOUCH_UP, false);
                    this.states_.set(MoveType.TOUCH_DOWN, false);
                    this.dispatchEvent(new Event(EventType.CHANGE));
                }
            }
            //ZOOMING
            else
            {
                this.states_.set(MoveType.TOUCH_LEFT, false);
                this.states_.set(MoveType.TOUCH_RIGHT, false);
                this.states_.set(MoveType.TOUCH_UP, false);
                this.states_.set(MoveType.TOUCH_DOWN, false);

                let toleranceZoom = 10;
                let distance = Math.sqrt(Math.pow(this.interactionMovePoints_[0].x-this.interactionMovePoints_[1].x, 2) + Math.pow(this.interactionMovePoints_[0].y-this.interactionMovePoints_[1].y, 2));
                if(distance > this.interactionDistance_ + toleranceZoom)
                {
                    this.states_.set(MoveType.TOUCH_ZOOM_IN, true);
                    this.states_.set(MoveType.TOUCH_ZOOM_OUT, false);
                }
                else if(distance < this.interactionDistance_ - toleranceZoom)
                {
                    this.states_.set(MoveType.TOUCH_ZOOM_IN, false);
                    this.states_.set(MoveType.TOUCH_ZOOM_OUT, true);
                }
                else
                {
                    this.states_.set(MoveType.TOUCH_ZOOM_IN, false);
                    this.states_.set(MoveType.TOUCH_ZOOM_OUT, false);
                }
            }
        }
    }

    /**
     * Method monitors the target (pointer|mouse|touch) up event.
     * @param {Event} event
     * @protected
     */
    handleUserUp_(event)
    {
        if(this.interactionStartPoints_)
        {
            this.states_.set(MoveType.TOUCH_LEFT, false);
            this.states_.set(MoveType.TOUCH_RIGHT, false);
            this.states_.set(MoveType.TOUCH_UP, false);
            this.states_.set(MoveType.TOUCH_DOWN, false);
            this.states_.set(MoveType.TOUCH_ZOOM_IN, false);
            this.states_.set(MoveType.TOUCH_ZOOM_OUT, false);

            this.interactionStartPoints_ = null;
            this.interactionMovePoints_ = null;
            this.interactionEndPoints_ = event ? getBrowserPoints(event) : null;
            this.interactionDistance_ = 0;
            this.touchStartMoveDistance_ = new Coordinate(0,0);
            this.dispatchEvent(new Event(EventType.CHANGE));
        }
    }

    /**
     * Sets/saves an acceleration value by an optional list key.
     * @param {number} value
     * @param {string=} axisOrZoom
     */
    setAcceleration(value, axisOrZoom = '')
    {
        if (typeof this.acceleration_ == 'number')
            this.acceleration_ = value;
        else
            this.acceleration_.set(axisOrZoom, value);
    }

    /**
     * Gets an acceleration value by an optional list key.
     * @param {string=} axisOrZoom
     * @return {number}
     */
    getAcceleration(axisOrZoom = '')
    {
        if (typeof this.acceleration_ == 'number')
            return this.acceleration_;
        else
            return this.acceleration_.get(axisOrZoom);
    }

    /**
     * Sets/saves a friction value by an optional list key.
     * @param {number} value
     * @param {string=} axisOrZoom
     */
    setFriction(value, axisOrZoom = '')
    {
        if (typeof this.friction_ == 'number')
            this.friction_ = value;
        else
            this.friction_.set(axisOrZoom, value);
    }

    /**
     * Gets a friction value by an optional list key.
     * @param {string} axisOrZoom
     * @return {number}
     */
    getFriction(axisOrZoom = '')
    {
        if (typeof this.friction_ == 'number')
            return this.friction_;
        else
            return this.friction_.get(axisOrZoom);
    }

    /**
     * Will Stop only the current interaction monitoring
     */
    stopInteraction()
    {
        let hadChangedState = false;
        this.states_.forEach((value, state) => {
            if(value === true)
            {
                hadChangedState = true;
                this.states_.set(state, false);
            }
        });

        clearTimeout(this.zoomTimeout_);
        this.interactionStartPoints_ = null;
        this.interactionMovePoints_ = null;
        this.interactionEndPoints_ = null;
        this.interactionDistance_ = 0;
        if(hadChangedState)
            this.dispatchEvent(new Event(EventType.CHANGE));
    }

    /**
     * Method monitors the gamepad button press event.
     * @param {Object|number} button
     * @return {boolean}
     * @private
     */
    hasGamepadButtonPressed_(button)
    {
        if (typeof(button) == "object") {
            return button['pressed'];
        }
        return button == 1;
    }

    /**
     * Method checks if a gamepad is available and should be used.
     * @param {Gamepad=} gamepad
     * @param {string|null} idCondition
     * @return {boolean}
     */
    checkGamepad(gamepad, idCondition = null)
    {
        this.gamepad_ = gamepad ? gamepad : this.getGamepad(idCondition);

        if(this.gamepadIsEnabled_ && this.gamepad_)
        {
            this.gamepadAxes_.set(MoveType.PAD_LEFT_HORIZONTAL, this.gamepad_.axes[0]);
            this.gamepadAxes_.set(MoveType.PAD_LEFT_VERTICAL, this.gamepad_.axes[1]);
            this.gamepadAxes_.set(MoveType.PAD_RIGHT_HORIZONTAL, this.gamepad_.axes[2]);
            this.gamepadAxes_.set(MoveType.PAD_RIGHT_VERTICAL, this.gamepad_.axes[3]);

            let up = this.hasGamepadButtonPressed_(this.gamepad_.buttons[12]) || Math.round(this.gamepad_.axes[1] * 10) < -1 || Math.round(this.gamepad_.axes[3] * 10) < -1;
            let down = this.hasGamepadButtonPressed_(this.gamepad_.buttons[13]) || Math.round(this.gamepad_.axes[1] * 10) > 1 || Math.round(this.gamepad_.axes[3] * 10) > 1;
            let left = this.hasGamepadButtonPressed_(this.gamepad_.buttons[14]) || Math.round(this.gamepad_.axes[0] * 10) < -1 || Math.round(this.gamepad_.axes[2] * 10) < -1;
            let right = this.hasGamepadButtonPressed_(this.gamepad_.buttons[15]) || Math.round(this.gamepad_.axes[0] * 10) > 1 || Math.round(this.gamepad_.axes[2] * 10) > 1;

            this.states_.set(MoveType.PAD_UP, up);
            this.states_.set(MoveType.PAD_DOWN, down);
            this.states_.set(MoveType.PAD_LEFT, left);
            this.states_.set(MoveType.PAD_RIGHT, right);
        }

        return this.gamepad_ != null;
    }

    /**
     * Get the gamepag instance
     * @param {string|null} idCondition
     * @return {Gamepad}
     */
    getGamepad(idCondition)
    {
        try{
            if(idCondition != null)
            {
                console.log('gamepads', window.navigator.getGamepads());
                for(let i=0; i<window.navigator.getGamepads().length; i++)
                {
                    const gamepad = window.navigator.getGamepads()[i];
                    const condition = idCondition.substr(0,1) == '!' ? gamepad.id.indexOf(idCondition.substr(1)) == -1 : gamepad.id.indexOf(idCondition) != -1;
                    if(gamepad && gamepad.id && condition)
                    {
                        console.log('used gamepad', gamepad.id);
                        return gamepad;
                    }
                }
            }
            else
                return window.navigator.getGamepads()[0];
        }catch(error){console.warn(error);}

        return null;
    }

    /**
     * Getter
     * @return {Array<number>}
     */
    get activeKeys()
    {
        return this.activKeys_;
    }

    /**
     * Getter
     * @return {boolean}
     */
    get isMoving()
    {
        return this.isMovingLeft != null || this.isMovingRight != null || this.isMovingUp != null || this.isMovingDown != null;
    }

    /**
     * Getter
     * @return {boolean}
     */
    get isMouseMoved()
    {
        return this.isMovingLeft == MoveType.MOUSE_LEFT || this.isMovingRight == MoveType.MOUSE_RIGHT || this.isMovingUp == MoveType.MOUSE_UP || this.isMovingDown == MoveType.MOUSE_DOWN;
    }

    /**
     * Getter
     * @return {string|null}
     */
    get isMovingLeft()
    {
        if(this.states_.get(MoveType.LEFT) === true)
            return MoveType.LEFT;
        else if(this.states_.get(MoveType.TOUCH_LEFT) === true)
            if(this.interactionType_ == 'MOUSE')
                return MoveType.MOUSE_LEFT;
            else
                return MoveType.TOUCH_LEFT;
        else if(this.gamepadIsEnabled_ && this.states_.get(MoveType.PAD_LEFT) === true)
            return MoveType.PAD_LEFT;
        return null;
    }

    /**
     * Getter
     * @return {string|null}
     */
    get isMovingRight()
    {
        if(this.states_.get(MoveType.RIGHT) === true)
            return MoveType.RIGHT;
        else if(this.states_.get(MoveType.TOUCH_RIGHT) === true)
            if(this.interactionType_ == 'MOUSE')
                return MoveType.MOUSE_RIGHT;
            else
                return MoveType.TOUCH_RIGHT;
        else if(this.gamepadIsEnabled_ && this.states_.get(MoveType.PAD_RIGHT) === true)
            return MoveType.PAD_RIGHT;
        return null;
    }

    /**
     * Getter
     * @return {string|null}
     */
    get isMovingUp()
    {
        if(this.states_.get(MoveType.UP) === true)
            return MoveType.UP;
        else if(this.states_.get(MoveType.TOUCH_UP) === true)
            if(this.interactionType_ == 'MOUSE')
                return MoveType.MOUSE_UP;
            else
                return MoveType.TOUCH_UP;
        else if(this.gamepadIsEnabled_ && this.states_.get(MoveType.PAD_UP) === true)
            return MoveType.PAD_UP;
        return null;
    }

    /**
     * Getter
     * @return {string|null}
     */
    get isMovingDown()
    {
        if(this.states_.get(MoveType.DOWN) === true)
            return MoveType.DOWN;
        else if(this.states_.get(MoveType.TOUCH_DOWN) === true)
            if(this.interactionType_ == 'MOUSE')
                return MoveType.MOUSE_DOWN;
            else
                return MoveType.TOUCH_DOWN;
        else if(this.gamepadIsEnabled_ && this.states_.get(MoveType.PAD_DOWN) === true)
            return MoveType.PAD_DOWN;
        return null;
    }

    /**
     * Getter
     * @return {string|null}
     */
    get isZooming()
    {
        return this.isZoomingIn || this.isZoomingOut;
    }

    /**
     * Getter
     * @return {string|null}
     */
    get isZoomingIn()
    {
        if(this.states_.get(MoveType.ZOOM_IN) === true)
            return MoveType.ZOOM_IN;
        else if(this.states_.get(MoveType.TOUCH_ZOOM_IN) === true)
            return MoveType.TOUCH_ZOOM_IN;
        return null;
    }

    /**
     * Getter
     * @return {string|null}
     */
    get isZoomingOut()
    {
        if(this.states_.get(MoveType.ZOOM_OUT) === true)
            return MoveType.ZOOM_OUT;
        else if(this.states_.get(MoveType.TOUCH_ZOOM_OUT) === true)
            return MoveType.TOUCH_ZOOM_OUT;
        return null;
    }

    /**
     * Getter
     * @return {Map<string,boolean>}
     */
    get states()
    {
        return this.states_;
    }

    /**
     * Getter
     * @return {Array<Coordinate>}
     */
    get interactionStartPoints()
    {
        return this.interactionStartPoints_;
    }

    /**
     * Getter
     * @return {Array<Coordinate>}
     */
    get interactionMovePoints()
    {
        return this.interactionMovePoints_;
    }

    /**
     * Getter
     * @return {Array<Coordinate>}
     */
    get interactionMoveDistances()
    {
        return this.interactionMoveDistances_;
    }

    /**
     * Getter
     * @return {number}
     */
    get interactionZoomDistance()
    {
        return this.interactionZoomDistance_;
    }

    /**
     * Getter
     * @return {Array<Coordinate>}
     */
    get interactionEndPoints()
    {
        return this.interactionEndPoints_;
    }

    /**
     * Getter
     * @return {string}
     */
    get interactionType()
    {
        return this.interactionType_;
    }

    /**
     * Getter
     * @return {Map<string, number>}
     */
    get gamepadAxes()
    {
        return this.gamepadAxes_;
    }

    /**
     * Getter
     * @return {boolean}
     */
    get keysAreEnabled()
    {
        return this.keysAreEnabled_;
    }

    /**
     * Getter
     * @return {boolean}
     */
    get zoomingIsEnabled()
    {
        return this.zoomingIsEnabled_;
    }

    /**
     * Getter
     * @return {boolean}
     */
    get mouseIsEnabled()
    {
        return this.mouseIsEnabled_;
    }

    /**
     * Getter
     * @return {boolean}
     */
    get gamepadIsEnabled()
    {
        return this.gamepadIsEnabled_;
    }

    /**
     * Getter
     * @return {Coordinate}
     */
    get touchStartMoveDistance()
    {
        return this.touchStartMoveDistance_;
    }
}

/**
 * @enum {string}
 */
const MoveType = {
    /** left */                 LEFT: 'left',
    /** right */                RIGHT: 'right',
    /** up */                   UP: 'up',
    /** down */                 DOWN: 'down',
    /** mouse-left */           MOUSE_LEFT: 'mouse-left',
    /** mouse-right */          MOUSE_RIGHT: 'mouse-right',
    /** mouse-up */             MOUSE_UP: 'mouse-up',
    /** mouse-down */           MOUSE_DOWN: 'mouse-down',
    /** touch-left */           TOUCH_LEFT: 'touch-left',
    /** touch-right */          TOUCH_RIGHT: 'touch-right',
    /** touch-up */             TOUCH_UP: 'touch-up',
    /** touch-down */           TOUCH_DOWN: 'touch-down',
    /** zoom-in */              ZOOM_IN: 'zoom-in',
    /** zoom-out */             ZOOM_OUT: 'zoom-out',
    /** touch-zoom-in */        TOUCH_ZOOM_IN: 'touch-zoom-in',
    /** touch-zoom-out */       TOUCH_ZOOM_OUT: 'touch-zoom-out',
    /** pad-left */             PAD_LEFT: 'pad-left',
    /** pad-right */            PAD_RIGHT: 'pad-right',
    /** pad-up */               PAD_UP: 'pad-up',
    /** pad-down */             PAD_DOWN: 'pad-down',
    /** padLeftHorizontal */    PAD_LEFT_HORIZONTAL: 'padLeftHorizontal',
    /** padLeftVertical */      PAD_LEFT_VERTICAL: 'padLeftVertical',
    /** padRightHorizontal */   PAD_RIGHT_HORIZONTAL: 'padRightHorizontal',
    /** padRightVertical */     PAD_RIGHT_VERTICAL: 'padRightVertical',
};

exports = {MoveController,MoveType};