/*global define*/ define(['Core/DeveloperError', 'Core/defined', 'Core/destroyObject', 'Core/Cartesian2', 'Core/ScreenSpaceEventType', 'Core/KeyboardEventModifier', 'Core/defaultValue'], function( DeveloperError, defined, destroyObject, Cartesian2, ScreenSpaceEventType, KeyboardEventModifier, defaultValue) { "use strict"; /** * Handles user input events. Custom functions can be added to be executed on * when the user enters input. * * @alias ScreenSpaceEventHandler * * @param {DOC_TBA} element The element to add events to. Defaults to document. * @constructor */ var ScreenSpaceEventHandler = function(element) { this._mouseEvents = {}; this._leftMouseButtonDown = false; this._middleMouseButtonDown = false; this._rightMouseButtonDown = false; this._isPinching = false; this._seenAnyTouchEvents = false; this._lastMousePosition = new Cartesian2(); this._lastTouch2 = new Cartesian2(); this._totalPixels = 0; this._touchID1 = 0; this._touchID2 = 0; // TODO: Revisit when doing mobile development. May need to be configurable // or determined based on the platform? this._clickPixelTolerance = 5; this._element = defaultValue(element, document); register(this); }; var scratchPosition = new Cartesian2(); function getPosition(screenSpaceEventHandler, event, result) { if (screenSpaceEventHandler._element === document) { result.x = event.clientX; result.y = event.clientY; return result; } var rect = screenSpaceEventHandler._element.getBoundingClientRect(); result.x = event.clientX - rect.left; result.y = event.clientY - rect.top; return result; } function getMouseEventsKey(type, modifier) { var key = type.name; if (defined(modifier)) { key += '+' + modifier.name; } return key; } /** * Set a function to be executed on an input event. * * @memberof ScreenSpaceEventHandler * * @param {Function} action Function to be executed when the input event occurs. * @param {Enumeration} type The ScreenSpaceEventType of input event. * @param {Enumeration} [modifier] A KeyboardEventModifier key that is held when a <code>type</code> * event occurs. * * @exception {DeveloperError} action is required. * @exception {DeveloperError} type is required. * * @see ScreenSpaceEventHandler#getInputAction * @see ScreenSpaceEventHandler#removeInputAction */ ScreenSpaceEventHandler.prototype.setInputAction = function(action, type, modifier) { //>>includeStart('debug', pragmas.debug); if (!defined(action)) { throw new DeveloperError('action is required.'); } if (!defined(type)) { throw new DeveloperError('type is required.'); } //>>includeEnd('debug'); var key = getMouseEventsKey(type, modifier); this._mouseEvents[key] = action; }; /** * Returns the function to be executed on an input event. * * @memberof ScreenSpaceEventHandler * * @param {Enumeration} type The ScreenSpaceEventType of input event. * @param {Enumeration} [modifier] A KeyboardEventModifier key that is held when a <code>type</code> * event occurs. * * @exception {DeveloperError} type is required. * * @see ScreenSpaceEventHandler#setInputAction * @see ScreenSpaceEventHandler#removeInputAction */ ScreenSpaceEventHandler.prototype.getInputAction = function(type, modifier) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError('type is required.'); } //>>includeEnd('debug'); var key = getMouseEventsKey(type, modifier); return this._mouseEvents[key]; }; /** * Removes the function to be executed on an input event. * * @memberof ScreenSpaceEventHandler * * @param {Enumeration} type The ScreenSpaceEventType of input event. * @param {Enumeration} [modifier] A KeyboardEventModifier key that is held when a <code>type</code> * event occurs. * * @exception {DeveloperError} type is required. * * @see ScreenSpaceEventHandler#getInputAction * @see ScreenSpaceEventHandler#setInputAction */ ScreenSpaceEventHandler.prototype.removeInputAction = function(type, modifier) { //>>includeStart('debug', pragmas.debug); if (!defined(type)) { throw new DeveloperError('type is required.'); } //>>includeEnd('debug'); var key = getMouseEventsKey(type, modifier); delete this._mouseEvents[key]; }; function getModifier(event) { if (event.shiftKey) { return KeyboardEventModifier.SHIFT; } else if (event.ctrlKey) { return KeyboardEventModifier.CTRL; } else if (event.altKey) { return KeyboardEventModifier.ALT; } return undefined; } var scratchMouseDownEvent = { position : new Cartesian2() }; function handleMouseDown(screenSpaceEventHandler, event) { var pos = getPosition(screenSpaceEventHandler, event, scratchMouseDownEvent.position); screenSpaceEventHandler._lastMousePosition.x = pos.x; screenSpaceEventHandler._lastMousePosition.y = pos.y; screenSpaceEventHandler._totalPixels = 0; if (screenSpaceEventHandler._seenAnyTouchEvents) { return; } var modifier = getModifier(event); var action; // IE_TODO: On some versions of IE, the left-button is 1, and the right-button is 4. // See: http://www.unixpapa.com/js/mouse.html // This is not the case in Chrome Frame, so we are OK for now, but are there // constants somewhere? if (event.button === 0) { screenSpaceEventHandler._leftMouseButtonDown = true; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_DOWN, modifier); } else if (event.button === 1) { screenSpaceEventHandler._middleMouseButtonDown = true; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.MIDDLE_DOWN, modifier); } else if (event.button === 2) { screenSpaceEventHandler._rightMouseButtonDown = true; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.RIGHT_DOWN, modifier); } if (defined(action)) { action(scratchMouseDownEvent); } event.preventDefault(); } var scratchMouseUpEvent = { position : new Cartesian2() }; function handleMouseUp(screenSpaceEventHandler, event) { var modifier = getModifier(event); var action, clickAction; if (screenSpaceEventHandler._seenAnyTouchEvents) { return; } // IE_TODO: On some versions of IE, the left-button is 1, and the right-button is 4. // See: http://www.unixpapa.com/js/mouse.html // This is not the case in Chrome Frame, so we are OK for now, but are there // constants somewhere? if (event.button === 0) { screenSpaceEventHandler._leftMouseButtonDown = false; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_UP, modifier); clickAction = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_CLICK, modifier); } else if (event.button === 1) { screenSpaceEventHandler._middleMouseButtonDown = false; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.MIDDLE_UP, modifier); clickAction = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.MIDDLE_CLICK, modifier); } else if (event.button === 2) { screenSpaceEventHandler._rightMouseButtonDown = false; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.RIGHT_UP, modifier); clickAction = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.RIGHT_CLICK, modifier); } var pos = getPosition(screenSpaceEventHandler, event, scratchMouseUpEvent.position); var xDiff = screenSpaceEventHandler._lastMousePosition.x - pos.x; var yDiff = screenSpaceEventHandler._lastMousePosition.y - pos.y; screenSpaceEventHandler._totalPixels += Math.sqrt(xDiff * xDiff + yDiff * yDiff); if (defined(action)) { action(scratchMouseUpEvent); } if (defined(clickAction) && screenSpaceEventHandler._totalPixels < screenSpaceEventHandler._clickPixelTolerance) { clickAction(scratchMouseUpEvent); } } var scratchMouseMoveEvent = { startPosition : new Cartesian2(), endPosition : new Cartesian2() }; function handleMouseMove(screenSpaceEventHandler, event) { var pos = getPosition(screenSpaceEventHandler, event, scratchMouseMoveEvent.endPosition); if (screenSpaceEventHandler._seenAnyTouchEvents) { return; } var xDiff = screenSpaceEventHandler._lastMousePosition.x - pos.x; var yDiff = screenSpaceEventHandler._lastMousePosition.y - pos.y; screenSpaceEventHandler._totalPixels += Math.sqrt(xDiff * xDiff + yDiff * yDiff); Cartesian2.clone(screenSpaceEventHandler._lastMousePosition, scratchMouseMoveEvent.startPosition); var modifier = getModifier(event); var action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.MOUSE_MOVE, modifier); if (defined(action)) { action(scratchMouseMoveEvent); } Cartesian2.clone(scratchMouseMoveEvent.endPosition, screenSpaceEventHandler._lastMousePosition); if (screenSpaceEventHandler._leftMouseButtonDown || screenSpaceEventHandler._middleMouseButtonDown || screenSpaceEventHandler._rightMouseButtonDown) { event.preventDefault(); } } var touchStartEvent = { position : new Cartesian2() }; var touch2StartEvent = { position1 : new Cartesian2(), position2 : new Cartesian2() }; function handleTouchStart(screenSpaceEventHandler, event) { var numberOfTouches = event.touches.length; screenSpaceEventHandler._seenAnyTouchEvents = true; var modifier = getModifier(event); var action; var pos = getPosition(screenSpaceEventHandler, event.touches[0], touchStartEvent.position); if (numberOfTouches === 1) { Cartesian2.clone(pos, screenSpaceEventHandler._lastMousePosition); screenSpaceEventHandler._totalPixels = 0; screenSpaceEventHandler._leftMouseButtonDown = true; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_DOWN, modifier); if (defined(action)) { action(touchStartEvent); } event.preventDefault(); } else if (screenSpaceEventHandler._leftMouseButtonDown) { // Release "mouse" without clicking, because we are adding more touches. screenSpaceEventHandler._leftMouseButtonDown = false; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_UP, modifier); if (defined(action)) { action(touchStartEvent); } } if (numberOfTouches === 2) { screenSpaceEventHandler._isPinching = true; var pos2 = getPosition(screenSpaceEventHandler, event.touches[1], touch2StartEvent.position2); screenSpaceEventHandler._touchID1 = event.touches[0].identifier; screenSpaceEventHandler._touchID2 = event.touches[1].identifier; Cartesian2.clone(pos, screenSpaceEventHandler._lastMousePosition); Cartesian2.clone(pos, touch2StartEvent.position1); Cartesian2.clone(pos2, screenSpaceEventHandler._lastTouch2); action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.PINCH_START, modifier); if (defined(action)) { action(touch2StartEvent); } } else if (screenSpaceEventHandler._isPinching) { screenSpaceEventHandler._isPinching = false; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.PINCH_END, modifier); if (defined(action)) { action(); } } } var touchEndEvent = { position : new Cartesian2() }; function handleTouchEnd(screenSpaceEventHandler, event) { var numberOfTouches = event.touches.length; var numberOfChangedTouches = event.changedTouches.length; var modifier = getModifier(event); var action; var clickAction; if (screenSpaceEventHandler._leftMouseButtonDown) { screenSpaceEventHandler._leftMouseButtonDown = false; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_UP, modifier); clickAction = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_CLICK, modifier); if (numberOfChangedTouches > 0) { var pos = getPosition(screenSpaceEventHandler, event.changedTouches[0], touchEndEvent.position); var xDiff = screenSpaceEventHandler._lastMousePosition.x - pos.x; var yDiff = screenSpaceEventHandler._lastMousePosition.y - pos.y; screenSpaceEventHandler._totalPixels += Math.sqrt(xDiff * xDiff + yDiff * yDiff); if (defined(action)) { action(touchEndEvent); } if (defined(clickAction) && screenSpaceEventHandler._totalPixels < screenSpaceEventHandler._clickPixelTolerance) { clickAction(touchEndEvent); } } } if (screenSpaceEventHandler._isPinching) { screenSpaceEventHandler._isPinching = false; action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.PINCH_END, modifier); if (action) { action(); } } if (numberOfTouches === 1 || numberOfTouches === 2) { handleTouchStart(screenSpaceEventHandler, event); } } var touchMovementEvent = { startPosition : new Cartesian2(), endPosition : new Cartesian2() }; var touchPinchMovementEvent = { distance : { startPosition : new Cartesian2(), endPosition : new Cartesian2() }, angleAndHeight : { startPosition : new Cartesian2(), endPosition : new Cartesian2() } }; function handleTouchMove(screenSpaceEventHandler, event) { var modifier = getModifier(event); var pos; var pos2; var action; if (screenSpaceEventHandler._leftMouseButtonDown && (event.touches.length === 1)) { pos = getPosition(screenSpaceEventHandler, event.touches[0], touchMovementEvent.endPosition); var xDiff = screenSpaceEventHandler._lastMouseX - pos.x; var yDiff = screenSpaceEventHandler._lastMouseY - pos.y; screenSpaceEventHandler._totalPixels += Math.sqrt(xDiff * xDiff + yDiff * yDiff); Cartesian2.clone(screenSpaceEventHandler._lastMousePosition, touchMovementEvent.startPosition); action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.MOUSE_MOVE, modifier); if (defined(action)) { action(touchMovementEvent); } Cartesian2.clone(touchMovementEvent.endPosition, screenSpaceEventHandler._lastMousePosition); if (screenSpaceEventHandler._leftMouseButtonDown || screenSpaceEventHandler._middleMouseButtonDown || screenSpaceEventHandler._rightMouseButtonDown) { event.preventDefault(); } } if (screenSpaceEventHandler._isPinching && (event.touches.length === 2)) { // Check the touch identifier to make sure the order is correct. if (event.touches[0].identifier === screenSpaceEventHandler._touchID2) { pos = getPosition(screenSpaceEventHandler, event.touches[1], touchMovementEvent.startPosition); pos2 = getPosition(screenSpaceEventHandler, event.touches[0], touchMovementEvent.endPosition); } else { pos = getPosition(screenSpaceEventHandler, event.touches[0],touchMovementEvent.startPosition); pos2 = getPosition(screenSpaceEventHandler, event.touches[1], touchMovementEvent.endPosition); } action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.PINCH_MOVE, modifier); if (defined(action)) { var dX = pos2.x - pos.x; var dY = pos2.y - pos.y; var dist = Math.sqrt(dX * dX + dY * dY) * 0.25; var prevDX = screenSpaceEventHandler._lastTouch2.x - screenSpaceEventHandler._lastMousePosition.x; var prevDY = screenSpaceEventHandler._lastTouch2.y - screenSpaceEventHandler._lastMousePosition.y; var prevDist = Math.sqrt(prevDX * prevDX + prevDY * prevDY) * 0.25; var cY = (pos2.y + pos.y) * 0.125; var prevCY = (screenSpaceEventHandler._lastTouch2.y + screenSpaceEventHandler._lastMousePosition.y) * 0.125; var angle = Math.atan2(dY, dX); var prevAngle = Math.atan2(prevDY, prevDX); Cartesian2.fromElements(0.0, prevDist, touchPinchMovementEvent.distance.startPosition); Cartesian2.fromElements(0.0, dist, touchPinchMovementEvent.distance.endPosition); Cartesian2.fromElements(prevAngle, prevCY, touchPinchMovementEvent.angleAndHeight.startPosition); Cartesian2.fromElements(angle, cY, touchPinchMovementEvent.angleAndHeight.endPosition); action(touchPinchMovementEvent); } Cartesian2.clone(pos, screenSpaceEventHandler._lastMousePosition); Cartesian2.clone(pos2, screenSpaceEventHandler._lastTouch2); } } function handleMouseWheel(screenSpaceEventHandler, event) { // Some browsers use event.detail to count the number of clicks. The sign // of the integer is the direction the wheel is scrolled. In that case, convert // to the angle it was rotated in degrees. var delta = event.detail ? event.detail * -120 : event.wheelDelta; var modifier = getModifier(event); var type = ScreenSpaceEventType.WHEEL; var action = screenSpaceEventHandler.getInputAction(type, modifier); if (defined(action)) { event.preventDefault(); action(delta); } } var mouseDbleClickEvent = { position : new Cartesian2() }; function handleMouseDblClick(screenSpaceEventHandler, event) { var modifier = getModifier(event); var action; var pos = getPosition(screenSpaceEventHandler, event, mouseDbleClickEvent.position); // IE_TODO: On some versions of IE, the left-button is 1, and the right-button is 4. // See: http://www.unixpapa.com/js/mouse.html // This is not the case in Chrome Frame, so we are OK for now, but are there // constants somewhere? if (event.button === 0) { action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, modifier); } else if (event.button === 1) { action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.MIDDLE_DOUBLE_CLICK, modifier); } else if (event.button === 2) { action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.RIGHT_DOUBLE_CLICK, modifier); } if (defined(action)) { action(mouseDbleClickEvent); } } function register(screenSpaceEventHandler) { var that = screenSpaceEventHandler, useDoc = true; screenSpaceEventHandler._callbacks = []; if (defined(screenSpaceEventHandler._element.disableRootEvents)) { useDoc = false; } screenSpaceEventHandler._callbacks.push({ name : 'mousedown', onDoc : false, action : function(e) { handleMouseDown(that, e); } }); screenSpaceEventHandler._callbacks.push({ name : 'mouseup', onDoc : useDoc, action : function(e) { handleMouseUp(that, e); } }); screenSpaceEventHandler._callbacks.push({ name : 'mousemove', onDoc : useDoc, action : function(e) { handleMouseMove(that, e); } }); screenSpaceEventHandler._callbacks.push({ name : 'dblclick', onDoc : false, action : function(e) { handleMouseDblClick(that, e); } }); screenSpaceEventHandler._callbacks.push({ name : 'touchstart', onDoc : false, action : function(e) { handleTouchStart(that, e); } }); screenSpaceEventHandler._callbacks.push({ name : 'touchend', onDoc : useDoc, action : function(e) { handleTouchEnd(that, e); } }); screenSpaceEventHandler._callbacks.push({ name : 'touchmove', onDoc : useDoc, action : function(e) { handleTouchMove(that, e); } }); // Firefox calls the mouse wheel event 'DOMMouseScroll', all others use 'mousewheel' screenSpaceEventHandler._callbacks.push({ name : 'mousewheel', onDoc : false, action : function(e) { handleMouseWheel(that, e); } }); screenSpaceEventHandler._callbacks.push({ name : 'DOMMouseScroll', onDoc : false, action : function(e) { handleMouseWheel(that, e); } }); for ( var i = 0; i < screenSpaceEventHandler._callbacks.length; i++) { var cback = screenSpaceEventHandler._callbacks[i]; if (cback.onDoc) { document.addEventListener(cback.name, cback.action, false); } else { screenSpaceEventHandler._element.addEventListener(cback.name, cback.action, false); } } } ScreenSpaceEventHandler.prototype._unregister = function() { for ( var i = 0; i < this._callbacks.length; i++) { var cback = this._callbacks[i]; if (cback.onDoc) { document.removeEventListener(cback.name, cback.action, false); } else { this._element.removeEventListener(cback.name, cback.action, false); } } }; /** * 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. * * @memberof ScreenSpaceEventHandler * * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>. * * @see ScreenSpaceEventHandler#destroy */ ScreenSpaceEventHandler.prototype.isDestroyed = function() { return false; }; /** * Removes 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. * * @memberof ScreenSpaceEventHandler * * @returns {undefined} * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * @see ScreenSpaceEventHandler#isDestroyed * * @example * handler = handler && handler.destroy(); */ ScreenSpaceEventHandler.prototype.destroy = function() { this._unregister(); return destroyObject(this); }; return ScreenSpaceEventHandler; });