[OpenLayers-Commits] r11695 - in trunk/openlayers: lib/OpenLayers/Control lib/OpenLayers/Handler tests/Control tests/Handler

commits-20090109 at openlayers.org commits-20090109 at openlayers.org
Thu Mar 10 20:53:31 EST 2011


Author: tschaub
Date: 2011-03-10 17:53:26 -0800 (Thu, 10 Mar 2011)
New Revision: 11695

Modified:
   trunk/openlayers/lib/OpenLayers/Control/PinchZoom.js
   trunk/openlayers/lib/OpenLayers/Control/TouchNavigation.js
   trunk/openlayers/lib/OpenLayers/Handler/Click.js
   trunk/openlayers/lib/OpenLayers/Handler/Pinch.js
   trunk/openlayers/tests/Control/TouchNavigation.html
   trunk/openlayers/tests/Handler/Click.html
Log:
Reworking click and pinch handling to get better behavior on multi-touch devices.  r=ahocevar +testing from others (closes #3133)

Modified: trunk/openlayers/lib/OpenLayers/Control/PinchZoom.js
===================================================================
--- trunk/openlayers/lib/OpenLayers/Control/PinchZoom.js	2011-03-11 01:29:19 UTC (rev 11694)
+++ trunk/openlayers/lib/OpenLayers/Control/PinchZoom.js	2011-03-11 01:53:26 UTC (rev 11695)
@@ -124,6 +124,7 @@
      */
     pinchStart: function(evt, pinchData) {
         this.pinchOrigin = evt.xy;
+        this.currentCenter = evt.xy;
     },
     
     /**
@@ -170,21 +171,20 @@
      *     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);
-        var resolution = this.map.getResolutionForZoom(zoom);
+        if (zoom !== this.map.getZoom() || !this.currentCenter.equals(this.pinchOrigin)) {
+            var resolution = this.map.getResolutionForZoom(zoom);
 
-        var location = this.map.getLonLatFromPixel(this.pinchOrigin);
-        var zoomPixel = this.currentCenter;        
-        var size = this.map.getSize();
-        
-        location.lon += resolution * ((size.w / 2) - zoomPixel.x);
-        location.lat -= resolution * ((size.h / 2) - zoomPixel.y);
+            var location = this.map.getLonLatFromPixel(this.pinchOrigin);
+            var zoomPixel = this.currentCenter;        
+            var size = this.map.getSize();
 
-        this.map.setCenter(location, zoom);
+            location.lon += resolution * ((size.w / 2) - zoomPixel.x);
+            location.lat -= resolution * ((size.h / 2) - zoomPixel.y);
 
-        var style = this.map.layerContainerDiv.style;
-        style['-webkit-transform'] = "";
-        style['-moz-transform'] = "";
+            this.map.setCenter(location, zoom);
+        }
     },
 
     CLASS_NAME: "OpenLayers.Control.PinchZoom"

Modified: trunk/openlayers/lib/OpenLayers/Control/TouchNavigation.js
===================================================================
--- trunk/openlayers/lib/OpenLayers/Control/TouchNavigation.js	2011-03-11 01:29:19 UTC (rev 11694)
+++ trunk/openlayers/lib/OpenLayers/Control/TouchNavigation.js	2011-03-11 01:53:26 UTC (rev 11695)
@@ -45,6 +45,12 @@
     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.
      *     Default is false.
@@ -121,13 +127,14 @@
      */
     draw: function() {
         var clickCallbacks = {
-            'click': this.defaultClick,
-            'dblclick': this.defaultDblClick
+            click: this.defaultClick,
+            dblclick: this.defaultDblClick
         };
-        var clickOptions = {
-            'double': true,
-            'stopDouble': true
-        };
+        var clickOptions = OpenLayers.Util.extend({
+            "double": true,
+            stopDouble: true,
+            pixelTolerance: 2
+        }, this.clickHandlerOptions);
         this.handlers.click = new OpenLayers.Handler.Click(
             this, clickCallbacks, clickOptions
         );

Modified: trunk/openlayers/lib/OpenLayers/Handler/Click.js
===================================================================
--- trunk/openlayers/lib/OpenLayers/Handler/Click.js	2011-03-11 01:29:19 UTC (rev 11694)
+++ trunk/openlayers/lib/OpenLayers/Handler/Click.js	2011-03-11 01:53:26 UTC (rev 11695)
@@ -51,13 +51,21 @@
      *     constructed.
      */
     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,
     
@@ -82,6 +90,13 @@
      * {Number} The id of the timeout waiting to clear the <delayedCall>.
      */
     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
@@ -92,15 +107,22 @@
      */
     down: null,
 
-    /** 
+    /**
      * 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,35 +148,77 @@
      */
     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: touchstart
+     * Handle touchstart.
+     *
+     * Returns:
+     * {Boolean} Continue propagating this event.
+     */
+    touchstart: function(evt) {
+        if (!this.touch) {
+            this.unregisterMouseListeners();
+            this.touch = true;
         }
+        this.down = this.getEventInfo(evt);
+        this.last = this.getEventInfo(evt);
+        return true;
     },
     
     /**
-     * Method: mousedown
-     * Handle mousedown.  Only registered as a listener if pixelTolerance is
-     *     a non-zero value at construction.
+     * Method: touchmove
+     *    Store position of last move, because touchend event can have
+     *    an empty "touches" property.
      *
      * Returns:
      * {Boolean} Continue propagating this event.
      */
-    mousedown: null,
+    touchmove: function(evt) {
+        this.last = this.getEventInfo(evt);
+        return true;
+    },
 
     /**
-     * Method: touchstart
-     * Handle touchstart.
+     * 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.
      */
-    touchstart: function(evt) {
+    mousedown: function(evt) {
         this.down = this.getEventInfo(evt);
-        this.last = null;
+        this.last = this.getEventInfo(evt);
         return true;
     },
 
@@ -171,8 +235,7 @@
         // 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);
         }
@@ -194,7 +257,7 @@
            if(this.rightclickTimerId != null) {
                 //Second click received before timeout this must be 
                 // a double click
-                this.clearTimer();      
+                this.clearTimer();
                 this.callback('dblrightclick', [evt]);
                 return !this.stopDouble;
             } else { 
@@ -227,93 +290,93 @@
         if (evt) {
            this.callback('rightclick', [evt]);
         }
-        return !this.stopSingle;
     },
     
     /**
+     * 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;
+    },
+
+    /**
      * 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);
-    },
-
-    /**
-     * Method: touchend
-     *   Correctly set event xy property, and add lastTouches to have
-     *   touches property from last touchstart or touchmove
-     */
-    touchend: function(evt) {
-        var last = this.last || this.down;
-        if (!evt || !last) {
-            return false;
+    handleDouble: function(evt) {
+        if (this["double"] && this.passesDblclickTolerance(evt)) {
+            this.callback("dblclick", [evt]);
         }
-        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.
+    
+    /** 
+     * Method: handleSingle
+     * Handle single click sequence.
      */
-    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;
-        }
-        if(this.passesTolerance(evt)) {
-            if(this.timerId != null) {
+    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.timerId = window.setTimeout(
-                    OpenLayers.Function.bind(this.delayedCall, this, clickEvent),
-                    this.delay
-               );
+                this.queuePotentialClick(clickEvent);
             }
         }
-        return !this.stopSingle;
     },
     
+    /** 
+     * 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, evt),
+            this.delay
+        );
+    },
+
     /**
      * Method: passesTolerance
      * Determine whether the event is within the optional pixel tolerance.  Note
@@ -327,28 +390,67 @@
      */
     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 = false;
+        if (this.pixelTolerance != null && this.down && this.down.xy) {
+            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;
+    },
 
     /**
      * Method: clearTimer
      * Clear the timer and set <timerId> to null.
      */
     clearTimer: function() {
-        if(this.timerId != null) {
+        if (this.timerId != null) {
             window.clearTimeout(this.timerId);
             this.timerId = null;
         }
-        if(this.rightclickTimerId != null) {
+        if (this.rightclickTimerId != null) {
             window.clearTimeout(this.rightclickTimerId);
             this.rightclickTimerId = null;
         }
@@ -361,8 +463,8 @@
      */
     delayedCall: function(evt) {
         this.timerId = null;
-        if(evt) {
-            this.callback('click', [evt]);
+        if (evt) {
+            this.callback("click", [evt]);
         }
     },
 
@@ -407,7 +509,7 @@
         if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) {
             this.clearTimer();
             this.down = null;
-            this.last = null;
+            this.first = null;
             deactivated = true;
         }
         return deactivated;

Modified: trunk/openlayers/lib/OpenLayers/Handler/Pinch.js
===================================================================
--- trunk/openlayers/lib/OpenLayers/Handler/Pinch.js	2011-03-11 01:29:19 UTC (rev 11694)
+++ trunk/openlayers/lib/OpenLayers/Handler/Pinch.js	2011-03-11 01:53:26 UTC (rev 11695)
@@ -37,9 +37,9 @@
     /**
      * 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 @@
             this.start = null;
             this.last = null;
         }
+        // prevent document dragging
         OpenLayers.Event.stop(evt);
         return propagate;
     },
@@ -120,15 +121,15 @@
      * {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 @@
      * {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;
     },
 
     /**

Modified: trunk/openlayers/tests/Control/TouchNavigation.html
===================================================================
--- trunk/openlayers/tests/Control/TouchNavigation.html	2011-03-11 01:29:19 UTC (rev 11694)
+++ trunk/openlayers/tests/Control/TouchNavigation.html	2011-03-11 01:53:26 UTC (rev 11695)
@@ -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?");

Modified: trunk/openlayers/tests/Handler/Click.html
===================================================================
--- trunk/openlayers/tests/Handler/Click.html	2011-03-11 01:29:19 UTC (rev 11694)
+++ trunk/openlayers/tests/Handler/Click.html	2011-03-11 01:53:26 UTC (rev 11695)
@@ -102,12 +102,16 @@
     
     var callbackMap;
     function callbackSetup(log, options) {
-        callbackMap = new OpenLayers.Map('map', {controls: []});
+        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 control = {
-            map: callbackMap
-        };
-
         var callbacks = {
             "click": function(evt) {
                 log.push({callback: "click", evt: 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,84 +527,125 @@
         });
     }
 
-    function test_touch_ignoresimulatedclick(t) {
-        t.plan(2);
+    function test_touch_within_dblclickTolerance(t) {
+        t.plan(4);
 
-        // set up
-
         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});
+            control, callbacks,
+            {"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(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")
 
-        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");
-
         // tear down
         map.destroy();
     }
 
-    function test_touch_dblclick(t) {
-        t.plan(5);
+    function test_touch_outside_dblclickTolerance(t) {
+        t.plan(2);
 
-        // set up
-
         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});
+            control, callbacks,
+            {"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.length, 0, "no callbacks called");
+        t.ok(!handler.timerId, "handler doesn't have a timerId waiting for click")
 
-        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");
-        }
-
         // tear down
         map.destroy();
     }



More information about the Commits mailing list