Leaflet: Jumpy zooming with Magic Mouse

Created on 11 Apr 2016  路  17Comments  路  Source: Leaflet/Leaflet

This is a follow-up to #2154 . As requested there, I am opening a new issue as the problem still persists.

Test Setup:

  • Latest master (63870f1) together with the code of the quick start example
  • MacBook Pro Retina with OSX Yosemite and Magic Mouse

Problems:

  • Zooming with the Magic Mouse is very sensitive. It is almost impossible to zoom by only one zoom level
  • When zooming for more than one zoom level (e.g. with a quick swipe), the animation looks jumpy as it moves to each zoom level instead of one smooth zoom from the initial to the final level before reloading all of the tiles
brainmelt bug compatibility needs investigation

Most helpful comment

I gave it a go using Lethargy.

This is working for me too:

var lethargy = new Lethargy(7, 50, 0.05);
L.Map.ScrollWheelZoom.prototype._onWheelScroll = function (e) {
  L.DomEvent.stop(e);
  if (lethargy.check(e) === false) {
      console.log('stopping zoom events from inertia')
      return
  }
  var delta = L.DomEvent.getWheelDelta(e);
  if (delta <= -0.25) delta = -0.25;
  if (delta >= 0.25) delta = 0.25;
  this._delta += delta;
  this._lastMousePos = this._map.mouseEventToContainerPoint(e);

  this._performZoom()
}

This solution offloads the effort of detecting inertia scrolling to an external library. That's a good thing because that library will probably attract more developers.

Update: The ES6 / Webpack solution. Didn't need to reduce the delta at all btw.

import L from 'leaflet'
import { Lethargy } from 'lethargy'

const lethargy = new Lethargy(7, 50, 0.05)
const isInertialScroll = (e) => lethargy.check(e) === false

L.Map.ScrollWheelZoom.prototype._onWheelScroll = function (e) {
  L.DomEvent.stop(e)
  if (isInertialScroll(e)) return

  this._delta += L.DomEvent.getWheelDelta(e)
  this._lastMousePos = this._map.mouseEventToContainerPoint(e)
  this._performZoom()
}

All 17 comments

@DennisSchiefer: Any chance you could debug what's going on in debug L.DomEvent.getMouseDelta() ? I'd be good to know if the pixel values are OK-ish, and whether the magic mouse sends several wheel events when doing a quick swipe, or just one.

How would I do this?
I assume, I would need to checkout Leaflet and build it myself, or can this be done just with the zip provided on the download page?

@DennisSchiefer I whipped up a quick playground for this: http://playground-leaflet.rhcloud.com/lod/edit?html,console,output

That should be enough to spy on the values of your mouse events, so please let us know if you see something weird after experimenting with different mice/touchpads.

If you want to establish breakpoints, you might need to check out the source and set up a similar example in the debug/ directory.

Thank you for setting up that playground.
I tested it with the Magic Mouse and the Apple Trackpad on the Mac (Safari, Firefox) and with a Microsoft IntelliMouse Explorer 3 on Windows (Firefox).

Apple:
Even a slight swipe triggers 10 events, each with deltaY =1, wheelDelta = -3; a fast swipe triggers even more events with deltaY going up to 2 or 3 and wheelDelta respectively going to -6 or -9.

Microsoft:
Slight turn of the wheel triggered one event, fast turn triggered at most 4 events. Each event had a larger deltaY and wheelDelta value.
Note: Even for the 4 events in Windows land, the animation looked fine.

Even a slight swipe triggers 10 events, each with deltaY =1, wheelDelta = -3;

What's the timing of this? (Add some console.log( performance.now() ) to see how many milliseconds between events). Also, is the target of the event always the map container?

I wonder if the events are at least one frame apart, or in the same frame (which could indicate something weird, like the same wheel event propagating through HTML elements and triggering the same event handler several times).

I'm not really sure what's going on, so please excuse me if I'm shooting blanks over the debugging process.

@DennisSchiefer can you have a look at my test build for smooth zooming? I did it for a smoother zoom on macbook trackpads: http://jsbin.com/vowazoqopo/1/edit?output

Sorry for the late reply, I only ever saw the mail at home where I don't have the MacBook around.

@IvanSanchez : Time between events varies wildly, most often it is around 10-15ms, though (can go down as low as 2ms and as high as 60ms).

@hyperknot : That feels much better than before. There is still some un-smoothness in there, but this could be tile reloading effects. It feels like night and day! (tested it with the Magic Mouse and the trackpad).

I'm having problems with the magic mouse too. I was looking to use leaflet in a project and came accross these problems while checking out the examples. Looked around for similar issues and found a few bug tickets and ended up here.

I was testing with the playground posted above. If I scroll and immediately try to drag the map or vice versa it starts jumping all around and seems to loose track of what it was doing. If i'm very careful and slowly move one little step at a time and pause for a second I can get things to work somewhat. But if I try and use it like I would any map it starts to become uncooperative and frustrating.

No good solution in the air. Moving to 1.0 milestone as non blocker for rc2.

Leaflet 0.7 has the same issue. I solved it 2 years ago using a workaround below.
This also works well on Leaflet 1.*.

var lastScroll = new Date().getTime();
L.Map.ScrollWheelZoom.prototype._onWheelScroll = function (e) {
  if (new Date().getTime() - lastScroll < 600) { 
    e.preventDefault();
    return;
  }
  var delta = L.DomEvent.getWheelDelta(e);
  var debounce = this._map.options.wheelDebounceTime;

  if (delta >= -0.15 && delta <= 0.15) {
    e.preventDefault();
    return;
  }
  if (delta <= -0.25) delta = -0.25;
  if (delta >= 0.25) delta = 0.25;
  this._delta += delta;
  this._lastMousePos = this._map.mouseEventToContainerPoint(e);

  if (!this._startTime) {
      this._startTime = +new Date();
  }

  var left = Math.max(debounce - (+new Date() - this._startTime), 0);

  clearTimeout(this._timer);
  lastScroll = new Date().getTime();
  this._timer = setTimeout(L.bind(this._performZoom, this), left);

  L.DomEvent.stop(e);
}

FYI, I've used @bcalik patch from https://github.com/Leaflet/Leaflet/issues/4410#issuecomment-234133427, and some Mac users reported it worked.
I've set the delta in line 3 from 600 to 400, so it doesn't hamper regular users that much.

Thank you very much for the fix in https://github.com/Leaflet/Leaflet/issues/4410#issuecomment-234133427. I tested it with a non-Retina MacBook Pro with an Apple Magic Mouse and Firefox. Previously, simple clicks often triggered a zoom change since it was so sensitive. Fiddling with zoomDelta or zoomSnap didn't help. Copying and pasting the above code fixed it!

The fix works for me too (Mac mini + magic mouse).
Thanks!

On pan the map zoom in and out. The Apple Magic Mouse is so sensitive. On mouseup the finger slides a little bit so I added a small delay before enabling scrollWheelZoom.

Created a workaround:

if (navigator.platform.indexOf('Mac') === 0) {
    map.on("mousedown", function () {
        map["scrollWheelZoom"].disable();
    });
    map.on("mouseup", function () {
        setTimeout(function () {
            map["scrollWheelZoom"].enable();
        }, 200);
    });
}

Comment: User reports that the workaround did not solve the problem. It was maybe a little better(?)

@SveinLoken This is not all about sensitivity. It's also about zoom events that continue to occur if you swipe on the surface of the Magic Mouse and then release the finger. That's like giving a regular scroll wheel a push and letting it spin. As long as the (virtual) wheel is spinning, zoom events continue to occur. This is called inertial scrolling on the Mac.

Inertial scrolling is causing the Jumpy zoom. To prove this, disable inertial scrolling on your Mac and try to zoom again.

https://github.com/d4nyll/lethargy is a Javascript library that can differentiate scroll events from intent and inertia. This should allow us to disable zooming from inertia.

I gave it a go using Lethargy.

This is working for me too:

var lethargy = new Lethargy(7, 50, 0.05);
L.Map.ScrollWheelZoom.prototype._onWheelScroll = function (e) {
  L.DomEvent.stop(e);
  if (lethargy.check(e) === false) {
      console.log('stopping zoom events from inertia')
      return
  }
  var delta = L.DomEvent.getWheelDelta(e);
  if (delta <= -0.25) delta = -0.25;
  if (delta >= 0.25) delta = 0.25;
  this._delta += delta;
  this._lastMousePos = this._map.mouseEventToContainerPoint(e);

  this._performZoom()
}

This solution offloads the effort of detecting inertia scrolling to an external library. That's a good thing because that library will probably attract more developers.

Update: The ES6 / Webpack solution. Didn't need to reduce the delta at all btw.

import L from 'leaflet'
import { Lethargy } from 'lethargy'

const lethargy = new Lethargy(7, 50, 0.05)
const isInertialScroll = (e) => lethargy.check(e) === false

L.Map.ScrollWheelZoom.prototype._onWheelScroll = function (e) {
  L.DomEvent.stop(e)
  if (isInertialScroll(e)) return

  this._delta += L.DomEvent.getWheelDelta(e)
  this._lastMousePos = this._map.mouseEventToContainerPoint(e)
  this._performZoom()
}

Thank you @devotis , this is awesome! You're example worked beautifully. I did find both the default values and the ones you use to drop a few too many events, so I dialed it down to 7, 10, 0.05 which works well for me. Might be a personal preference.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gdbd picture gdbd  路  3Comments

walterfn2 picture walterfn2  路  4Comments

viswaug picture viswaug  路  4Comments

prbaron picture prbaron  路  3Comments

ssured picture ssured  路  3Comments