Merge pull request #168 from fredj/touch-events

Touch events support
This commit is contained in:
Frédéric Junod
2013-02-21 03:29:51 -08:00
7 changed files with 517 additions and 70 deletions

View File

@@ -12,6 +12,8 @@
@exportObjectLiteralProperty ol.MapOptions.renderers Array.<ol.RendererHint>|undefined
@exportObjectLiteralProperty ol.MapOptions.shiftDragZoom boolean|undefined
@exportObjectLiteralProperty ol.MapOptions.target Element|string
@exportObjectLiteralProperty ol.MapOptions.touchPan boolean|undefined
@exportObjectLiteralProperty ol.MapOptions.touchRotateZoom boolean|undefined
@exportObjectLiteralProperty ol.MapOptions.view ol.IView|undefined
@exportObjectLiteralProperty ol.MapOptions.zoomControl boolean|undefined
@exportObjectLiteralProperty ol.MapOptions.zoomDelta number|undefined

View File

@@ -0,0 +1,119 @@
goog.provide('ol.interaction.Touch');
goog.require('goog.functions');
goog.require('ol.MapBrowserEvent');
goog.require('ol.MapBrowserEvent.EventType');
goog.require('ol.Pixel');
goog.require('ol.interaction.Interaction');
/**
* @constructor
* @extends {ol.interaction.Interaction}
*/
ol.interaction.Touch = function() {
goog.base(this);
/**
* @type {boolean}
* @private
*/
this.handled_ = false;
/**
* @type {Object}
* @private
*/
this.trackedTouches_ = {};
/**
* @type {Array.<Object>}
*/
this.targetTouches = [];
};
goog.inherits(ol.interaction.Touch, ol.interaction.Interaction);
/**
* @param {Array.<Object>} touches TouchEvents.
* @return {ol.Pixel} Centroid pixel.
*/
ol.interaction.Touch.centroid = function(touches) {
var length = touches.length;
var clientX = 0;
var clientY = 0;
for (var i = 0; i < length; i++) {
clientX += touches[i].clientX;
clientY += touches[i].clientY;
}
return new ol.Pixel(clientX / length, clientY / length);
};
/**
* @param {ol.MapBrowserEvent} mapBrowserEvent Event.
* @private
*/
ol.interaction.Touch.prototype.updateTrackedTouches_ =
function(mapBrowserEvent) {
var event = mapBrowserEvent.browserEvent.getBrowserEvent();
if (goog.isDef(event.targetTouches)) {
// W3C touch events
this.targetTouches = event.targetTouches;
} else {
// IE pointer event
if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.TOUCHEND) {
delete this.trackedTouches_[event.pointerId];
} else {
this.trackedTouches_[event.pointerId] = event;
}
this.targetTouches = goog.object.getValues(this.trackedTouches_);
}
};
/**
* @param {ol.MapBrowserEvent} mapBrowserEvent Event.
* @protected
*/
ol.interaction.Touch.prototype.handleTouchMove = goog.nullFunction;
/**
* @param {ol.MapBrowserEvent} mapBrowserEvent Event.
* @protected
* @return {boolean} Capture dragging.
*/
ol.interaction.Touch.prototype.handleTouchEnd = goog.functions.FALSE;
/**
* @param {ol.MapBrowserEvent} mapBrowserEvent Event.
* @protected
* @return {boolean} Capture dragging.
*/
ol.interaction.Touch.prototype.handleTouchStart = goog.functions.FALSE;
/**
* @inheritDoc
*/
ol.interaction.Touch.prototype.handleMapBrowserEvent =
function(mapBrowserEvent) {
var browserEvent = mapBrowserEvent.browserEvent.getBrowserEvent();
this.updateTrackedTouches_(mapBrowserEvent);
if (this.handled_) {
if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.TOUCHMOVE) {
this.handleTouchMove(mapBrowserEvent);
} else if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.TOUCHEND) {
this.handled_ = this.handleTouchEnd(mapBrowserEvent);
}
}
if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.TOUCHSTART) {
this.handled_ = this.handleTouchStart(mapBrowserEvent);
}
};

View File

@@ -0,0 +1,116 @@
// FIXME works for View2D only
// FIXME opt_kinetic param
goog.provide('ol.interaction.TouchPan');
goog.require('goog.asserts');
goog.require('ol.Coordinate');
goog.require('ol.Kinetic');
goog.require('ol.Pixel');
goog.require('ol.PreRenderFunction');
goog.require('ol.View');
goog.require('ol.ViewHint');
goog.require('ol.interaction.Touch');
/**
* @constructor
* @extends {ol.interaction.Touch}
*/
ol.interaction.TouchPan = function() {
goog.base(this);
/**
* @private
* @type {ol.Kinetic}
*/
this.kinetic_ = new ol.Kinetic(-0.005, 0.05, 100);
/**
* @private
* @type {?ol.PreRenderFunction}
*/
this.kineticPreRenderFn_ = null;
/**
* @type {ol.Pixel}
*/
this.lastCentroid = null;
};
goog.inherits(ol.interaction.TouchPan, ol.interaction.Touch);
/**
* @inheritDoc
*/
ol.interaction.TouchPan.prototype.handleTouchMove = function(mapBrowserEvent) {
goog.asserts.assert(this.targetTouches.length >= 1);
var centroid = ol.interaction.Touch.centroid(this.targetTouches);
if (!goog.isNull(this.lastCentroid)) {
this.kinetic_.update(centroid.x, centroid.y);
var deltaX = this.lastCentroid.x - centroid.x;
var deltaY = centroid.y - this.lastCentroid.y;
var view = mapBrowserEvent.map.getView();
var center = new ol.Coordinate(deltaX, deltaY)
.scale(view.getResolution())
.rotate(view.getRotation())
.add(view.getCenter());
view.setCenter(center);
}
this.lastCentroid = centroid;
};
/**
* @inheritDoc
*/
ol.interaction.TouchPan.prototype.handleTouchEnd =
function(mapBrowserEvent) {
var map = mapBrowserEvent.map;
var view = map.getView();
if (this.targetTouches.length == 0) {
view.setHint(ol.ViewHint.PANNING, -1);
if (this.kinetic_.end()) {
var distance = this.kinetic_.getDistance();
var angle = this.kinetic_.getAngle();
var center = view.getCenter();
this.kineticPreRenderFn_ = this.kinetic_.pan(center);
map.addPreRenderFunction(this.kineticPreRenderFn_);
var centerpx = map.getPixelFromCoordinate(center);
var destpx = new ol.Pixel(
centerpx.x - distance * Math.cos(angle),
centerpx.y - distance * Math.sin(angle));
var dest = map.getCoordinateFromPixel(destpx);
view.setCenter(dest);
}
return false;
} else {
return true;
}
};
/**
* @inheritDoc
*/
ol.interaction.TouchPan.prototype.handleTouchStart =
function(mapBrowserEvent) {
if (this.targetTouches.length >= 1) {
var map = mapBrowserEvent.map;
var view = map.getView();
this.lastCentroid = null;
if (!goog.isNull(this.kineticPreRenderFn_) &&
map.removePreRenderFunction(this.kineticPreRenderFn_)) {
map.requestRenderFrame();
view.setCenter(mapBrowserEvent.frameState.view2DState.center);
this.kineticPreRenderFn_ = null;
}
this.kinetic_.begin();
view.setHint(ol.ViewHint.PANNING, 1);
return true;
} else {
return false;
}
};

View File

@@ -0,0 +1,149 @@
// FIXME works for View2D only
goog.provide('ol.interaction.TouchRotateAndZoom');
goog.require('goog.asserts');
goog.require('ol.View');
goog.require('ol.ViewHint');
goog.require('ol.interaction.Touch');
/**
* @constructor
* @extends {ol.interaction.Touch}
*/
ol.interaction.TouchRotateAndZoom = function() {
goog.base(this);
/**
* @private
* @type {number|undefined}
*/
this.lastAngle_;
/**
* @private
* @type {number|undefined}
*/
this.lastDistance_;
/**
* @private
* @type {boolean}
*/
this.rotating_ = false;
/**
* @private
* @type {number}
*/
this.rotationDelta_ = 0.0;
/**
* @private
* @type {number}
*/
this.rotationThreshold_ = 0.3;
};
goog.inherits(ol.interaction.TouchRotateAndZoom, ol.interaction.Touch);
/**
* @inheritDoc
*/
ol.interaction.TouchRotateAndZoom.prototype.handleTouchMove =
function(mapBrowserEvent) {
goog.asserts.assert(this.targetTouches.length >= 2);
var scaleDelta = 1.0;
var rotationDelta = 0.0;
var centroid = ol.interaction.Touch.centroid(this.targetTouches);
var touch0 = this.targetTouches[0];
var touch1 = this.targetTouches[1];
var dx = touch0.clientX - touch1.clientX;
var dy = touch0.clientY - touch1.clientY;
// angle between touches
var angle = Math.atan2(
touch1.clientY - touch0.clientY,
touch1.clientX - touch0.clientX);
// distance between touches
var distance = Math.sqrt(dx * dx + dy * dy);
if (goog.isDef(this.lastDistance_)) {
scaleDelta = this.lastDistance_ / distance;
}
this.lastDistance_ = distance;
if (goog.isDef(this.lastAngle_)) {
var delta = angle - this.lastAngle_;
this.rotationDelta_ += delta;
if (!this.rotating_ &&
Math.abs(this.rotationDelta_) > this.rotationThreshold_) {
this.rotating_ = true;
}
rotationDelta = delta;
}
this.lastAngle_ = angle;
var map = mapBrowserEvent.map;
var view = map.getView();
// rotate / scale anchor point.
// FIXME: should be the intersection point between the lines:
// touch0,touch1 and previousTouch0,previousTouch1
var viewportPosition = goog.style.getClientPosition(map.getViewport());
centroid.x -= viewportPosition.x;
centroid.y -= viewportPosition.y;
var anchor = map.getCoordinateFromPixel(centroid);
// scale, bypass the resolution constraint
view.zoom_(map, view.getResolution() * scaleDelta, anchor);
// rotate
if (this.rotating_) {
view.rotate(map, view.getRotation() + rotationDelta, anchor);
}
};
/**
* @inheritDoc
*/
ol.interaction.TouchRotateAndZoom.prototype.handleTouchEnd =
function(mapBrowserEvent) {
if (this.targetTouches.length < 2) {
var map = mapBrowserEvent.map;
var view = map.getView();
// take the resolution constraint into account
view.zoomToResolution(map, view.getResolution());
view.setHint(ol.ViewHint.PANNING, -1);
return false;
} else {
return true;
}
};
/**
* @inheritDoc
*/
ol.interaction.TouchRotateAndZoom.prototype.handleTouchStart =
function(mapBrowserEvent) {
if (this.targetTouches.length >= 2) {
var view = mapBrowserEvent.map.getView();
this.lastDistance_ = undefined;
this.lastAngle_ = undefined;
this.rotating_ = false;
this.rotationDelta_ = 0.0;
view.setHint(ol.ViewHint.PANNING, 1);
return true;
} else {
return false;
}
};

View File

@@ -54,6 +54,8 @@ goog.require('ol.interaction.Interaction');
goog.require('ol.interaction.KeyboardPan');
goog.require('ol.interaction.KeyboardZoom');
goog.require('ol.interaction.MouseWheelZoom');
goog.require('ol.interaction.TouchPan');
goog.require('ol.interaction.TouchRotateAndZoom');
goog.require('ol.interaction.condition');
goog.require('ol.layer.Layer');
goog.require('ol.renderer.Map');
@@ -959,6 +961,18 @@ ol.Map.createInteractions_ = function(mapOptions) {
interactions.push(new ol.interaction.DblClickZoom(zoomDelta));
}
var touchPan = goog.isDef(mapOptions.touchPan) ?
mapOptions.touchPan : true;
if (touchPan) {
interactions.push(new ol.interaction.TouchPan());
}
var touchRotateZoom = goog.isDef(mapOptions.touchRotateZoom) ?
mapOptions.touchRotateZoom : true;
if (touchRotateZoom) {
interactions.push(new ol.interaction.TouchRotateAndZoom());
}
var dragPan = goog.isDef(mapOptions.dragPan) ?
mapOptions.dragPan : true;
if (dragPan) {

View File

@@ -3,7 +3,6 @@ goog.provide('ol.MapBrowserEvent.EventType');
goog.provide('ol.MapBrowserEventHandler');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
@@ -49,6 +48,20 @@ ol.MapBrowserEvent = function(type, map, browserEvent, opt_frameState) {
goog.inherits(ol.MapBrowserEvent, ol.MapEvent);
/**
* IE specific events.
* See http://msdn.microsoft.com/en-us/library/ie/hh673557(v=vs.85).aspx
* FIXME: replace with goog.events.EventType enum once we use
* goog/events/eventtype.js above r2211
* @enum {string}
*/
ol.MapBrowserEvent.IEEventType = {
MSPOINTERDOWN: 'MSPointerDown',
MSPOINTERMOVE: 'MSPointerMove',
MSPOINTERUP: 'MSPointerUp'
};
/**
* @return {ol.Coordinate} Coordinate.
*/
@@ -114,10 +127,10 @@ ol.MapBrowserEventHandler = function(map) {
/**
* Timestamp for the first click of a double click. Will be set back to 0
* as soon as a double click is detected.
* @type {number}
* @type {?number}
* @private
*/
this.timestamp_ = 0;
this.timestamp_ = null;
/**
* @type {?number}
@@ -137,6 +150,12 @@ ol.MapBrowserEventHandler = function(map) {
*/
this.dragListenerKeys_ = null;
/**
* @type {Array.<number>}
* @private
*/
this.touchListenerKeys_ = null;
/**
* @type {goog.events.BrowserEvent}
* @private
@@ -144,38 +163,32 @@ ol.MapBrowserEventHandler = function(map) {
this.down_ = null;
var element = this.map_.getViewport();
if (!ol.BrowserFeature.HAS_TOUCH) {
this.clickListenerKey_ = goog.events.listen(element,
[goog.events.EventType.CLICK, goog.events.EventType.DBLCLICK],
this.click_, false, this);
}
this.clickListenerKey_ = goog.events.listen(element,
[goog.events.EventType.CLICK, goog.events.EventType.DBLCLICK],
this.click_, false, this);
this.downListenerKey_ = goog.events.listen(element,
ol.BrowserFeature.HAS_TOUCH ?
goog.events.EventType.TOUCHSTART :
goog.events.EventType.MOUSEDOWN,
this.handleDown_, false, this);
goog.events.EventType.MOUSEDOWN,
this.handleMouseDown_, false, this);
// touch events
this.touchListenerKeys_ = [
goog.events.listen(element, [
goog.events.EventType.TOUCHSTART,
ol.MapBrowserEvent.IEEventType.MSPOINTERDOWN
], this.handleTouchStart_, false, this),
goog.events.listen(element, [
goog.events.EventType.TOUCHMOVE,
ol.MapBrowserEvent.IEEventType.MSPOINTERMOVE
], this.handleTouchMove_, false, this),
goog.events.listen(element, [
goog.events.EventType.TOUCHEND,
ol.MapBrowserEvent.IEEventType.MSPOINTERUP
], this.handleTouchEnd_, false, this)
];
};
goog.inherits(ol.MapBrowserEventHandler, goog.events.EventTarget);
/**
* @param {goog.events.BrowserEvent} browserEvent Browser event.
* @private
*/
ol.MapBrowserEventHandler.prototype.touchEnableBrowserEvent_ =
function(browserEvent) {
if (ol.BrowserFeature.HAS_TOUCH) {
goog.asserts.assert(browserEvent instanceof goog.events.BrowserEvent);
var nativeEvent = browserEvent.getBrowserEvent();
if (nativeEvent.touches && nativeEvent.touches.length) {
var nativeTouch = nativeEvent.touches[0];
browserEvent.clientX = nativeTouch.clientX;
browserEvent.clientY = nativeTouch.clientY;
}
}
};
/**
* @param {goog.events.BrowserEvent} browserEvent Browser event.
* @private
@@ -183,15 +196,15 @@ ol.MapBrowserEventHandler.prototype.touchEnableBrowserEvent_ =
ol.MapBrowserEventHandler.prototype.click_ = function(browserEvent) {
if (!this.dragged_) {
var newEvent;
if (browserEvent.type !== goog.events.EventType.DBLCLICK) {
newEvent = new ol.MapBrowserEvent(
ol.MapBrowserEvent.EventType.CLICK, this.map_, browserEvent);
this.dispatchEvent(newEvent);
}
if (!this.timestamp_) {
var type = browserEvent.type;
if (this.timestamp_ == 0 || type == goog.events.EventType.DBLCLICK) {
newEvent = new ol.MapBrowserEvent(
ol.MapBrowserEvent.EventType.DBLCLICK, this.map_, browserEvent);
this.dispatchEvent(newEvent);
} else if (type == goog.events.EventType.CLICK) {
newEvent = new ol.MapBrowserEvent(
ol.MapBrowserEvent.EventType.CLICK, this.map_, browserEvent);
this.dispatchEvent(newEvent);
}
}
};
@@ -201,19 +214,8 @@ ol.MapBrowserEventHandler.prototype.click_ = function(browserEvent) {
* @param {goog.events.BrowserEvent} browserEvent Browser event.
* @private
*/
ol.MapBrowserEventHandler.prototype.handleUp_ = function(browserEvent) {
ol.MapBrowserEventHandler.prototype.handleMouseUp_ = function(browserEvent) {
if (this.previous_) {
if (!this.dragged_) {
var now = new Date().getTime();
if (!this.timestamp_ || now - this.timestamp_ > 250) {
this.timestamp_ = now;
} else {
this.timestamp_ = 0;
}
if (ol.BrowserFeature.HAS_TOUCH) {
this.click_(this.down_);
}
}
this.down_ = null;
goog.array.forEach(this.dragListenerKeys_, goog.events.unlistenByKey);
this.dragListenerKeys_ = null;
@@ -231,12 +233,11 @@ ol.MapBrowserEventHandler.prototype.handleUp_ = function(browserEvent) {
* @param {goog.events.BrowserEvent} browserEvent Browser event.
* @private
*/
ol.MapBrowserEventHandler.prototype.handleDown_ = function(browserEvent) {
ol.MapBrowserEventHandler.prototype.handleMouseDown_ = function(browserEvent) {
var newEvent = new ol.MapBrowserEvent(
ol.MapBrowserEvent.EventType.DOWN, this.map_, browserEvent);
this.dispatchEvent(newEvent);
if (!this.previous_) {
this.touchEnableBrowserEvent_(browserEvent);
this.down_ = browserEvent;
this.previous_ = {
clientX: browserEvent.clientX,
@@ -244,21 +245,13 @@ ol.MapBrowserEventHandler.prototype.handleDown_ = function(browserEvent) {
};
this.dragged_ = false;
this.dragListenerKeys_ = [
goog.events.listen(document,
ol.BrowserFeature.HAS_TOUCH ?
goog.events.EventType.TOUCHMOVE :
goog.events.EventType.MOUSEMOVE,
this.drag_, false, this),
goog.events.listen(document,
ol.BrowserFeature.HAS_TOUCH ?
goog.events.EventType.TOUCHEND :
goog.events.EventType.MOUSEUP,
this.handleUp_, false, this)
goog.events.listen(document, goog.events.EventType.MOUSEMOVE,
this.handleMouseMove_, false, this),
goog.events.listen(document, goog.events.EventType.MOUSEUP,
this.handleMouseUp_, false, this)
];
if (browserEvent.type === goog.events.EventType.MOUSEDOWN) {
// prevent browser image dragging on pointer devices
browserEvent.preventDefault();
}
// prevent browser image dragging with the dom renderer
browserEvent.preventDefault();
}
};
@@ -267,7 +260,7 @@ ol.MapBrowserEventHandler.prototype.handleDown_ = function(browserEvent) {
* @param {goog.events.BrowserEvent} browserEvent Browser event.
* @private
*/
ol.MapBrowserEventHandler.prototype.drag_ = function(browserEvent) {
ol.MapBrowserEventHandler.prototype.handleMouseMove_ = function(browserEvent) {
var newEvent;
if (!this.dragged_) {
this.dragged_ = true;
@@ -275,19 +268,64 @@ ol.MapBrowserEventHandler.prototype.drag_ = function(browserEvent) {
ol.MapBrowserEvent.EventType.DRAGSTART, this.map_, this.down_);
this.dispatchEvent(newEvent);
}
this.touchEnableBrowserEvent_(browserEvent);
this.previous_ = {
clientX: browserEvent.clientX,
clientY: browserEvent.clientY
};
// prevent viewport dragging on touch devices
browserEvent.preventDefault();
newEvent = new ol.MapBrowserEvent(
ol.MapBrowserEvent.EventType.DRAG, this.map_, browserEvent);
this.dispatchEvent(newEvent);
};
/**
* @param {goog.events.BrowserEvent} browserEvent Browser event.
* @private
*/
ol.MapBrowserEventHandler.prototype.handleTouchStart_ = function(browserEvent) {
// prevent context menu
browserEvent.preventDefault();
this.down_ = browserEvent;
this.dragged_ = false;
var newEvent = new ol.MapBrowserEvent(
ol.MapBrowserEvent.EventType.TOUCHSTART, this.map_, browserEvent);
this.dispatchEvent(newEvent);
};
/**
* @param {goog.events.BrowserEvent} browserEvent Browser event.
* @private
*/
ol.MapBrowserEventHandler.prototype.handleTouchMove_ = function(browserEvent) {
this.dragged_ = true;
var newEvent = new ol.MapBrowserEvent(
ol.MapBrowserEvent.EventType.TOUCHMOVE, this.map_, browserEvent);
this.dispatchEvent(newEvent);
};
/**
* @param {goog.events.BrowserEvent} browserEvent Browser event.
* @private
*/
ol.MapBrowserEventHandler.prototype.handleTouchEnd_ = function(browserEvent) {
var newEvent = new ol.MapBrowserEvent(
ol.MapBrowserEvent.EventType.TOUCHEND, this.map_, browserEvent);
this.dispatchEvent(newEvent);
if (!this.dragged_) {
var now = goog.now();
if (!this.timestamp_ || now - this.timestamp_ > 250) {
this.timestamp_ = now;
} else {
this.timestamp_ = 0;
}
this.click_(this.down_);
}
this.down_ = null;
};
/**
* FIXME empty description for jsdoc
*/
@@ -298,6 +336,10 @@ ol.MapBrowserEventHandler.prototype.disposeInternal = function() {
goog.array.forEach(this.dragListenerKeys_, goog.events.unlistenByKey);
this.dragListenerKeys_ = null;
}
if (!goog.isNull(this.touchListenerKeys_)) {
goog.array.forEach(this.touchListenerKeys_, goog.events.unlistenByKey);
this.touchListenerKeys_ = null;
}
goog.base(this, 'disposeInternal');
};
@@ -309,8 +351,11 @@ ol.MapBrowserEventHandler.prototype.disposeInternal = function() {
ol.MapBrowserEvent.EventType = {
CLICK: goog.events.EventType.CLICK,
DBLCLICK: goog.events.EventType.DBLCLICK,
DOWN: 'down',
DRAGSTART: 'dragstart',
DRAG: 'drag',
DRAGEND: 'dragend',
DOWN: 'down'
TOUCHSTART: goog.events.EventType.TOUCHSTART,
TOUCHMOVE: goog.events.EventType.TOUCHMOVE,
TOUCHEND: goog.events.EventType.TOUCHEND
};

View File

@@ -77,7 +77,9 @@ describe('ol.Map', function() {
dragPan: false,
keyboard: false,
mouseWheelZoom: false,
shiftDragZoom: false
shiftDragZoom: false,
touchPan: false,
touchRotateZoom: false
};
});