Reworking click and pinch handling to get better behavior on multi-touch devices. r=ahocevar +testing from others (closes #3133)

git-svn-id: http://svn.openlayers.org/trunk/openlayers@11695 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
Tim Schaub
2011-03-11 01:53:26 +00:00
parent 3ce239208f
commit 5b7d530461
6 changed files with 488 additions and 184 deletions

View File

@@ -124,6 +124,7 @@ OpenLayers.Control.PinchZoom = OpenLayers.Class(OpenLayers.Control, {
*/
pinchStart: function(evt, pinchData) {
this.pinchOrigin = evt.xy;
this.currentCenter = evt.xy;
},
/**
@@ -170,7 +171,9 @@ OpenLayers.Control.PinchZoom = OpenLayers.Class(OpenLayers.Control, {
* of the pinch gesture. This give us the final scale of the pinch.
*/
pinchDone: function(evt, start, last) {
this.applyTransform("");
var zoom = this.map.getZoomForResolution(this.map.getResolution() / last.scale, true);
if (zoom !== this.map.getZoom() || !this.currentCenter.equals(this.pinchOrigin)) {
var resolution = this.map.getResolutionForZoom(zoom);
var location = this.map.getLonLatFromPixel(this.pinchOrigin);
@@ -181,10 +184,7 @@ OpenLayers.Control.PinchZoom = OpenLayers.Class(OpenLayers.Control, {
location.lat -= resolution * ((size.h / 2) - zoomPixel.y);
this.map.setCenter(location, zoom);
var style = this.map.layerContainerDiv.style;
style['-webkit-transform'] = "";
style['-moz-transform'] = "";
}
},
CLASS_NAME: "OpenLayers.Control.PinchZoom"

View File

@@ -44,6 +44,12 @@ OpenLayers.Control.TouchNavigation = OpenLayers.Class(OpenLayers.Control, {
*/
pinchZoomOptions: null,
/**
* APIProperty: clickHandlerOptions
* {Object} Options passed to the Click handler.
*/
clickHandlerOptions: null,
/**
* APIProperty: documentDrag
* {Boolean} Allow panning of the map by dragging outside map viewport.
@@ -121,13 +127,14 @@ OpenLayers.Control.TouchNavigation = OpenLayers.Class(OpenLayers.Control, {
*/
draw: function() {
var clickCallbacks = {
'click': this.defaultClick,
'dblclick': this.defaultDblClick
};
var clickOptions = {
'double': true,
'stopDouble': true
click: this.defaultClick,
dblclick: this.defaultDblClick
};
var clickOptions = OpenLayers.Util.extend({
"double": true,
stopDouble: true,
pixelTolerance: 2
}, this.clickHandlerOptions);
this.handlers.click = new OpenLayers.Handler.Click(
this, clickCallbacks, clickOptions
);

View File

@@ -52,12 +52,20 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/
pixelTolerance: 0,
/**
* APIProperty: dblclickTolerance
* {Number} Maximum distance in pixels between clicks for a sequence of
* events to be considered a double click. Default is 13. If the
* distance between two clicks is greater than this value, a double-
* click will not be fired.
*/
dblclickTolerance: 13,
/**
* APIProperty: stopSingle
* {Boolean} Stop other listeners from being notified of clicks. Default
* is false. If true, any click listeners registered before this one
* will not be notified of *any* click event (associated with double
* or single clicks).
* is false. If true, any listeners registered before this one for
* click or rightclick events will not be notified.
*/
stopSingle: false,
@@ -83,6 +91,13 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/
timerId: null,
/**
* Property: touch
* {Boolean} When a touchstart event is fired, touch will be true and all
* mouse related listeners will do nothing.
*/
touch: false,
/**
* Property: down
* {Object} Object that store relevant information about the last
@@ -95,12 +110,19 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
/**
* Property: last
* {Object} Object that store relevant information about the last
* touchmove. Its 'xy' OpenLayers.Pixel property gives
* mousemove or touchmove. Its 'xy' OpenLayers.Pixel property gives
* the average location of the mouse/touch event. Its 'touches'
* property records clientX/clientY of each touches.
*/
last: null,
/**
* Property: first
* {Object} When waiting for double clicks, this object will store
* information about the first click in a two click sequence.
*/
first: null,
/**
* Property: rightclickTimerId
* {Number} The id of the right mouse timeout waiting to clear the
@@ -126,25 +148,8 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/
initialize: function(control, callbacks, options) {
OpenLayers.Handler.prototype.initialize.apply(this, arguments);
// optionally register for mousedown
if(this.pixelTolerance != null) {
this.mousedown = function(evt) {
this.down = this.getEventInfo(evt);
return true;
};
}
},
/**
* Method: mousedown
* Handle mousedown. Only registered as a listener if pixelTolerance is
* a non-zero value at construction.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
mousedown: null,
/**
* Method: touchstart
* Handle touchstart.
@@ -153,8 +158,67 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Continue propagating this event.
*/
touchstart: function(evt) {
if (!this.touch) {
this.unregisterMouseListeners();
this.touch = true;
}
this.down = this.getEventInfo(evt);
this.last = null;
this.last = this.getEventInfo(evt);
return true;
},
/**
* Method: touchmove
* Store position of last move, because touchend event can have
* an empty "touches" property.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
touchmove: function(evt) {
this.last = this.getEventInfo(evt);
return true;
},
/**
* Method: touchend
* Correctly set event xy property, and add lastTouches to have
* touches property from last touchstart or touchmove
*/
touchend: function(evt) {
// touchstart may not have been allowed to propagate
if (this.down) {
evt.xy = this.last.xy;
evt.lastTouches = this.last.touches;
this.handleSingle(evt);
}
return true;
},
/**
* Method: unregisterMouseListeners
* In a touch environment, we don't want to handle mouse events.
*/
unregisterMouseListeners: function() {
this.map.events.un({
mousedown: this.mousedown,
mouseup: this.mouseup,
click: this.click,
dblclick: this.dblclick,
scope: this
});
},
/**
* Method: mousedown
* Handle mousedown.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
mousedown: function(evt) {
this.down = this.getEventInfo(evt);
this.last = this.getEventInfo(evt);
return true;
},
@@ -171,8 +235,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
// Collect right mouse clicks from the mouseup
// IE - ignores the second right click in mousedown so using
// mouseup instead
if(this.checkModifiers(evt) &&
this.control.handleRightClicks &&
if (this.checkModifiers(evt) && this.control.handleRightClicks &&
OpenLayers.Event.isRightClick(evt)) {
propagate = this.rightclick(evt);
}
@@ -227,6 +290,22 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
if (evt) {
this.callback('rightclick', [evt]);
}
},
/**
* Method: click
* Handle click events from the browser. This is registered as a listener
* for click events and should not be called from other events in this
* handler.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
click: function(evt) {
if (!this.last) {
this.last = this.getEventInfo(evt);
}
this.handleSingle(evt);
return !this.stopSingle;
},
@@ -234,84 +313,68 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
* Method: dblclick
* Handle dblclick. For a dblclick, we get two clicks in some browsers
* (FF) and one in others (IE). So we need to always register for
* dblclick to properly handle single clicks.
* dblclick to properly handle single clicks. This method is registered
* as a listener for the dblclick browser event. It should *not* be
* called by other methods in this handler.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
dblclick: function(evt) {
// for touch devices trigger dblclick only for
// "one finger" touch
var last = this.down || this.last;
if (this.passesTolerance(evt) &&
(!last || !last.touches || last.touches.length == 1)) {
if(this["double"]) {
this.callback('dblclick', [evt]);
}
this.clearTimer();
}
this.handleDouble(evt);
return !this.stopDouble;
},
/**
* Method: touchmove
* Store position of last move, because touchend event can have
* an empty "touches" property.
* Method: handleDouble
* Handle double-click sequence.
*/
touchmove: function(evt) {
this.last = this.getEventInfo(evt);
handleDouble: function(evt) {
if (this["double"] && this.passesDblclickTolerance(evt)) {
this.callback("dblclick", [evt]);
}
},
/**
* Method: touchend
* Correctly set event xy property, and add lastTouches to have
* touches property from last touchstart or touchmove
* Method: handleSingle
* Handle single click sequence.
*/
touchend: function(evt) {
var last = this.last || this.down;
if (!evt || !last) {
return false;
}
evt.xy = last.xy;
evt.lastTouches = last.touches;
return evt.xy ? this.click(evt) : false;
},
/**
* Method: click
* Handle click.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
click: function(evt) {
// Sencha Touch emulates click events, see ticket 3079 for more info
if (this.down && this.down.touches && evt.type === "click") {
return !this.stopSingle;
}
handleSingle: function(evt) {
if (this.passesTolerance(evt)) {
if (this.timerId != null) {
// already received a click
var last = this.down || this.last;
if (last && last.touches && last.touches.length > 0) {
// touch device - we may trigger dblclick
this.dblclick(evt);
} else {
if (this.last.touches && this.last.touches.length === 1) {
// touch device, no dblclick event - this may be a double
this.handleDouble(evt);
}
// if we're not in a touch environment we clear the click timer
// if we've got a second touch, we'll get two touchend events
if (!this.last.touches || this.last.touches.length !== 2) {
this.clearTimer();
}
} else {
// remember the first click info so we can compare to the second
this.first = this.getEventInfo(evt);
// set the timer, send evt only if single is true
//use a clone of the event object because it will no longer
//be a valid event object in IE in the timer callback
var clickEvent = this.single ?
OpenLayers.Util.extend({}, evt) : null;
this.queuePotentialClick(clickEvent);
}
}
},
/**
* Method: queuePotentialClick
* This method is separated out largely to make testing easier (so we
* don't have to override window.setTimeout)
*/
queuePotentialClick: function(evt) {
this.timerId = window.setTimeout(
OpenLayers.Function.bind(this.delayedCall, this, clickEvent),
OpenLayers.Function.bind(this.delayedCall, this, evt),
this.delay
);
}
}
return !this.stopSingle;
},
/**
@@ -328,14 +391,53 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
passesTolerance: function(evt) {
var passes = true;
if (this.pixelTolerance != null && this.down && this.down.xy) {
var dpx = Math.sqrt(
Math.pow(this.down.xy.x - evt.xy.x, 2) +
Math.pow(this.down.xy.y - evt.xy.y, 2)
);
if(dpx > this.pixelTolerance) {
passes = this.pixelTolerance >= this.down.xy.distanceTo(evt.xy);
// for touch environments, we also enforce that all touches
// start and end within the given tolerance to be considered a click
if (passes && this.touch &&
this.down.touches.length === this.last.touches.length) {
// the touchend event doesn't come with touches, so we check
// down and last
for (var i=0, ii=this.down.touches.length; i<ii; ++i) {
if (this.getTouchDistance(
this.down.touches[i],
this.last.touches[i]
) > this.pixelTolerance) {
passes = false;
break;
}
}
}
}
return passes;
},
/**
* Method: getTouchDistance
*
* Returns:
* {Boolean} The pixel displacement between two touches.
*/
getTouchDistance: function(from, to) {
return Math.sqrt(
Math.pow(from.clientX - to.clientX, 2) +
Math.pow(from.clientY - to.clientY, 2)
);
},
/**
* Method: passesDblclickTolerance
* Determine whether the event is within the optional double-cick pixel
* tolerance.
*
* Returns:
* {Boolean} The click is within the double-click pixel tolerance.
*/
passesDblclickTolerance: function(evt) {
var passes = true;
if (this.down && this.first) {
passes = this.down.xy.distanceTo(this.first.xy) <= this.dblclickTolerance;
}
return passes;
},
@@ -362,7 +464,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
delayedCall: function(evt) {
this.timerId = null;
if (evt) {
this.callback('click', [evt]);
this.callback("click", [evt]);
}
},
@@ -407,7 +509,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) {
this.clearTimer();
this.down = null;
this.last = null;
this.first = null;
deactivated = true;
}
return deactivated;

View File

@@ -37,9 +37,9 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
/**
* Property: stopDown
* {Boolean} Stop propagation of touchstart events from getting to
* listeners on the same element. Default is true.
* listeners on the same element. Default is false.
*/
stopDown: true,
stopDown: false,
/**
* Property: pinching
@@ -105,6 +105,7 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
this.start = null;
this.last = null;
}
// prevent document dragging
OpenLayers.Event.stop(evt);
return propagate;
},
@@ -120,15 +121,15 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Let the event propagate.
*/
touchmove: function(evt) {
var propagate = true;
if (this.started && OpenLayers.Event.isMultiTouch(evt)) {
this.pinching = true;
var current = this.getPinchData(evt);
this.callback("move", [evt, current]);
this.last = current;
propagate = false;
// prevent document dragging
OpenLayers.Event.stop(evt);
}
return propagate;
return true;
},
/**
@@ -142,16 +143,14 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Let the event propagate.
*/
touchend: function(evt) {
var propagate = true;
if (this.started) {
this.started = false;
this.pinching = false;
this.callback("done", [evt, this.start, this.last]);
this.start = null;
this.last = null;
propagate = false;
}
return propagate;
return true;
},
/**

View File

@@ -102,6 +102,29 @@
}
function test_clickHandlerOptions(t) {
t.plan(3);
var nav = new OpenLayers.Control.TouchNavigation();
t.eq(nav.clickHandlerOptions, null, "clickHandlerOptions null by default");
var map = new OpenLayers.Map({
div: document.body,
controls: [
new OpenLayers.Control.TouchNavigation({
clickHandlerOptions: {foo: "bar"}
})
]
});
nav = map.controls[0];
t.eq(nav.handlers.click.foo, "bar", "foo property is set on the click handler");
t.eq(nav.handlers.click.pixelTolerance, 2, "pixelTolerance is 2 by default");
map.destroy();
}
function test_zoomOut(t) {
t.plan(1);
@@ -114,7 +137,7 @@
var control = new OpenLayers.Control.TouchNavigation();
map.addControl(control);
var handler = control.handlers.click;
handler.touchstart({xy: {x: 1, y: 1}, touches: ["foo", "bar"]});
handler.touchstart({xy: new OpenLayers.Pixel(1 ,1), touches: ["foo", "bar"]});
handler.touchend({});
t.delay_call(1, function() {
t.eq(map.getZoom(), 4, "Did we zoom out?");

View File

@@ -102,11 +102,15 @@
var callbackMap;
function callbackSetup(log, options) {
callbackMap = new OpenLayers.Map('map', {controls: []});
var control = {
map: callbackMap
};
callbackMap = new OpenLayers.Map({
div: "map",
controls: [], // no controls here because these tests use a custom setTimeout and we only want setTimeout calls from a single handler
layers: [new OpenLayers.Layer(null, {isBaseLayer: true})],
center: new OpenLayers.LonLat(0, 0),
zoom: 1
});
var control = new OpenLayers.Control();
callbackMap.addControl(control);
var callbacks = {
"click": function(evt) {
@@ -119,6 +123,7 @@
var handler = new OpenLayers.Handler.Click(control, callbacks, options);
handler.activate();
var timers = {};
window._setTimeout = window.setTimeout;
window.setTimeout = function(func, delay) {
@@ -270,13 +275,140 @@
handler.map.events.triggerEvent("mouseup", up);
handler.map.events.triggerEvent("click", up);
t.eq(log.length, 0, "nothing logged - event outside tolerance");
callbackTeardown();
}
function test_callbacks_within_dblclickTolerance(t) {
t.plan(6);
var log = [];
var handler = callbackSetup(log, {single: false, "double": true, dblclickTolerance: 8});
var first = {
xy: px(0, 0)
};
var second = {
xy: px(0, 5)
};
handler.map.events.triggerEvent("mousedown", first);
handler.map.events.triggerEvent("mouseup", first);
handler.map.events.triggerEvent("click", first);
t.eq(log.length, 1, "one item logged");
t.eq(log[0] && log[0].method, "setTimeout", "setTimeout called");
handler.map.events.triggerEvent("mousedown", second);
handler.map.events.triggerEvent("mouseup", second);
handler.map.events.triggerEvent("click", second);
t.eq(log.length, 2, "two events logged");
t.eq(log[1] && log[1].method, "clearTimeout", "clearTimeout called");
handler.map.events.triggerEvent("dblclick", second);
t.eq(log.length, 3, "three items logged");
t.eq(log[2] && log[2].callback, "dblclick", "dblclick callback called");
callbackTeardown();
}
function test_callbacks_outside_dblclickTolerance(t) {
t.plan(5);
var log = [];
// default dblclickTolerance is 13
var handler = callbackSetup(log, {single: false, "double": true});
var first = {
xy: px(0, 0)
};
var second = {
xy: px(13.5, 0)
};
handler.map.events.triggerEvent("mousedown", first);
handler.map.events.triggerEvent("mouseup", first);
handler.map.events.triggerEvent("click", first);
t.eq(log.length, 1, "one item logged");
t.eq(log[0] && log[0].method, "setTimeout", "setTimeout called");
handler.map.events.triggerEvent("mousedown", second);
handler.map.events.triggerEvent("mouseup", second);
handler.map.events.triggerEvent("click", second);
t.eq(log.length, 2, "two items logged");
t.eq(log[1] && log[1].method, "clearTimeout", "clearTimeout called");
handler.map.events.triggerEvent("dblclick", second);
t.eq(log.length, 2, "still two items logged - dblclick callback is not called");
callbackTeardown();
}
function test_callbacks_multitouch_single(t) {
t.plan(2);
var log = [];
var callbacks = {
click: function(evt) {
log.push({callback: "click", type: evt.type});
},
dblclick: function(evt) {
log.push({callback: "dblclick", type: evt.type});
}
};
var map = new OpenLayers.Map("map");
var layer = new OpenLayers.Layer(null, {isBaseLayer: true});
map.addLayer(layer);
map.setCenter(new OpenLayers.LonLat(0, 0), 1);
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.Click(
control, callbacks,
{"double": true, single: true, pixelTolerance: 2}
);
// we override here so we don't have to wait for the timeout
handler.queuePotentialClick = function(evt) {
log.push({potential: true, evt: evt});
OpenLayers.Handler.Click.prototype.queuePotentialClick.call(this, evt);
}
handler.activate();
function handle(o) {
var touches = [];
if (("x0" in o) && ("y0" in o)) {
touches.push({
clientX: o.x0, clientY: o.y0
});
}
if (("x1" in o) && ("y1" in o)) {
touches.push({
clientX: o.x1, clientY: o.y1
});
}
handler.map.events.handleBrowserEvent({
type: o.type, touches: touches
});
}
// a typical multitouch sequence goes like this:
// touchstart, touchstart, touchend, touchend
handle({type: "touchstart", x0: 10, y0: 10});
handle({type: "touchstart", x0: 10, y0: 10, x1: 30, y1: 15});
handle({type: "touchend"});
handle({type: "touchend"});
t.eq(log.length, 1, "one item logged");
t.eq(log[0] && log[0].potential, true, "click in queue - no dblclick called");
map.destroy();
}
function test_Handler_Click_deactivate(t) {
t.plan(4);
var control = {
@@ -395,83 +527,124 @@
});
}
function test_touch_ignoresimulatedclick(t) {
t.plan(2);
// set up
function test_touch_within_dblclickTolerance(t) {
t.plan(4);
var log;
var map = new OpenLayers.Map('map');
var control = {map: map};
var callbacks = {
'dblclick': function(e) {
log.dblclick = {x: e.xy.x, y: e.xy.y,
lastTouches: e.lastTouches};
click: function(evt) {
log.push({callback: "click", type: evt.type});
},
dblclick: function(evt) {
log.push({callback: "dblclick", type: evt.type});
}
};
var map = new OpenLayers.Map("map");
var layer = new OpenLayers.Layer(null, {isBaseLayer: true});
map.addLayer(layer);
map.setCenter(new OpenLayers.LonLat(0, 0), 1);
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.Click(
control, callbacks,
{'double': true, pixelTolerance: null});
{"double": true, single: true, pixelTolerance: 2}
);
handler.activate();
function handle(type, x, y) {
map.events.handleBrowserEvent({
type: type,
touches: [
{clientX: x, clientY: y}
]
});
}
// test
log = [];
// sequence of two clicks on a touch device
// click 1
handle("touchstart", 10, 10);
handle("touchend", 11, 10);
handle("mousemove", 11, 10);
handle("mousedown", 10, 10);
handle("mouseup", 11, 10);
handle("click", 11, 10);
// click 2
handle("touchstart", 12, 10);
handle("touchend", 12, 10);
handle("mousedown", 12, 10);
handle("mouseup", 12, 10);
handle("click", 12, 10);
log = {};
handler.touchstart({xy: px(1, 1), touches: ["foo"]});
handler.touchend({});
handler.touchstart({xy: px(1, 1), touches: ["foo"]});
handler.touchend({type: "click"});
t.eq(!!handler.down.touches, true, "Handler down touches property should be truthy");
t.ok(log.dblclick == undefined, "dblclick callback not called with simulated click");
t.eq(log.length, 1, "one callback called");
t.eq(log[0] && log[0].callback, "dblclick", "click callback called");
t.eq(log[0] && log[0].type, "touchend", "click callback called with touchend event");
t.ok(!handler.timerId, "handler doesn't have a timerId waiting for click")
// tear down
map.destroy();
}
function test_touch_dblclick(t) {
t.plan(5);
// set up
function test_touch_outside_dblclickTolerance(t) {
t.plan(2);
var log;
var map = new OpenLayers.Map('map');
var control = {map: map};
var callbacks = {
'click': function(e) {
log.click = {x: e.xy.x, y: e.xy.y,
lastTouches: e.lastTouches};
click: function(evt) {
log.push({callback: "click", type: evt.type});
},
'dblclick': function(e) {
log.dblclick = {x: e.xy.x, y: e.xy.y,
lastTouches: e.lastTouches};
dblclick: function(evt) {
log.push({callback: "dblclick", type: evt.type});
}
};
var map = new OpenLayers.Map("map");
var layer = new OpenLayers.Layer(null, {isBaseLayer: true});
map.addLayer(layer);
map.setCenter(new OpenLayers.LonLat(0, 0), 1);
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.Click(
control, callbacks,
{'double': true, pixelTolerance: null});
{"double": true, single: true, pixelTolerance: 2, dblclickTolerance: 8}
);
handler.activate();
function handle(type, x, y) {
var touches = [];
if (x !== undefined && y !== undefined) {
touches.push({
clientX: x, clientY: y
});
}
map.events.handleBrowserEvent({
type: type, touches: touches
});
}
// test
log = [];
// sequence of two clicks on a touch device
// click 1
handle("touchstart", 10, 10);
handle("touchend");
handle("mousemove", 11, 10);
handle("mousedown", 10, 10);
handle("mouseup", 11, 10);
handle("click", 11, 10);
// click 2
handle("touchstart", 20, 10);
handle("touchend");
handle("mousedown", 20, 10);
handle("mouseup", 20, 10);
handle("click", 20, 10);
log = {};
handler.touchstart({xy: px(1, 1), touches: [{clientX:0, clientY:10}]});
handler.touchend({});
handler.touchstart({xy: px(1, 1), touches: [{clientX:0, clientY:10}]});
handler.touchend({});
t.eq(log.click, undefined, "click callback not called");
t.ok(log.dblclick != undefined, "dblclick callback called");
if(log.dblclick != undefined) {
t.eq(log.dblclick.x, 1, "evt.xy.x as expected");
t.eq(log.dblclick.y, 1, "evt.xy.y as expected");
t.ok(log.dblclick.lastTouches, "evt.lastTouches on evt");
}
t.eq(log.length, 0, "no callbacks called");
t.ok(!handler.timerId, "handler doesn't have a timerId waiting for click")
// tear down
map.destroy();