diff --git a/debug/map/zoom-delta.html b/debug/map/zoom-delta.html new file mode 100644 index 00000000..5c86dd82 --- /dev/null +++ b/debug/map/zoom-delta.html @@ -0,0 +1,109 @@ + + + + Leaflet debug page + + + + + + + + + + + + + +

Zoom delta test.

+ +

Zooming with touch zoom, box zoom or flyTo then map.stop() must make the zoom level snap to the value of the zoomSnap option. Zoom interactions (keyboard, mouse wheel, zoom control buttons must change the zoom by the amount in the zoomDelta option.

+ +
+ + + +
+ +
+ Snap: 0.25. Delta: 0.5. +
+ +
+
+ Snap: 0 (off). Delta: 0.25. +
+ +
+ + + + diff --git a/spec/suites/map/MapSpec.js b/spec/suites/map/MapSpec.js index 71db179d..57e26de8 100644 --- a/spec/suites/map/MapSpec.js +++ b/spec/suites/map/MapSpec.js @@ -624,6 +624,142 @@ describe("Map", function () { map.setView([0, 0], 0); map.once('zoomend', callback).flyTo(newCenter, newZoom); }); + }); + + describe('#zoomIn and #zoomOut', function () { + var center = L.latLng(22, 33); + beforeEach(function () { + map.setView(center, 10); + }); + + it('zoomIn zooms by 1 zoom level by default', function (done) { + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(11); + expect(map.getCenter()).to.eql(center); + done(); + }); + map.zoomIn(null, {animate: false}); + }); + + it('zoomOut zooms by 1 zoom level by default', function (done) { + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(9); + expect(map.getCenter()).to.eql(center); + done(); + }); + map.zoomOut(null, {animate: false}); + }); + + it('zoomIn ignores the zoomDelta option on non-any3d browsers', function (done) { + L.Browser.any3d = false; + map.options.zoomSnap = 0.25; + map.options.zoomDelta = 0.25; + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(11); + expect(map.getCenter()).to.eql(center); + done(); + }); + map.zoomIn(null, {animate: false}); + }); + + it('zoomIn respects the zoomDelta option on any3d browsers', function (done) { + L.Browser.any3d = true; + map.options.zoomSnap = 0.25; + map.options.zoomDelta = 0.25; + map.setView(center, 10); + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(10.25); + expect(map.getCenter()).to.eql(center); + done(); + }); + map.zoomIn(null, {animate: false}); + }); + + it('zoomOut respects the zoomDelta option on any3d browsers', function (done) { + L.Browser.any3d = true; + map.options.zoomSnap = 0.25; + map.options.zoomDelta = 0.25; + map.setView(center, 10); + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(9.75); + expect(map.getCenter()).to.eql(center); + done(); + }); + map.zoomOut(null, {animate: false}); + }); + + it('zoomIn snaps to zoomSnap on any3d browsers', function (done) { + map.options.zoomSnap = 0.25; + map.setView(center, 10); + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(10.25); + expect(map.getCenter()).to.eql(center); + done(); + }); + L.Browser.any3d = true; + map.zoomIn(0.22, {animate: false}); + }); + + it('zoomOut snaps to zoomSnap on any3d browsers', function (done) { + map.options.zoomSnap = 0.25; + map.setView(center, 10); + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(9.75); + expect(map.getCenter()).to.eql(center); + done(); + }); + L.Browser.any3d = true; + map.zoomOut(0.22, {animate: false}); + }); + }); + + describe('#fitBounds', function () { + var center = L.latLng(22, 33), + bounds = L.latLngBounds(L.latLng(1, 102), L.latLng(11, 122)), + boundsCenter = bounds.getCenter(); + + beforeEach(function () { + // fitBounds needs a map container with non-null area + var container = map.getContainer(); + container.style.width = container.style.height = "100px"; + document.body.appendChild(container); + map.setView(center, 10); + }); + + afterEach(function () { + document.body.removeChild(map.getContainer()); + }); + + it('Snaps zoom level to integer by default', function (done) { + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(2); + expect(map.getCenter().equals(boundsCenter, 0.05)).to.eql(true); + done(); + }); + map.fitBounds(bounds, {animate: false}); + }); + + it('Snaps zoom to zoomSnap on any3d browsers', function (done) { + map.options.zoomSnap = 0.25; + L.Browser.any3d = true; + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(2.75); + expect(map.getCenter().equals(boundsCenter, 0.05)).to.eql(true); + done(); + }); + map.fitBounds(bounds, {animate: false}); + }); + + it('Ignores zoomSnap on non-any3d browsers', function (done) { + map.options.zoomSnap = 0.25; + L.Browser.any3d = false; + map.once('zoomend', function () { + expect(map.getZoom()).to.eql(2); + expect(map.getCenter().equals(boundsCenter, 0.05)).to.eql(true); + done(); + }); + map.fitBounds(bounds, {animate: false}); + }); }); diff --git a/src/control/Control.Zoom.js b/src/control/Control.Zoom.js index 629f81db..c972a856 100644 --- a/src/control/Control.Zoom.js +++ b/src/control/Control.Zoom.js @@ -45,13 +45,13 @@ L.Control.Zoom = L.Control.extend({ _zoomIn: function (e) { if (!this._disabled) { - this._map.zoomIn(e.shiftKey ? 3 : 1); + this._map.zoomIn(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1)); } }, _zoomOut: function (e) { if (!this._disabled) { - this._map.zoomOut(e.shiftKey ? 3 : 1); + this._map.zoomOut(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1)); } }, diff --git a/src/map/Map.js b/src/map/Map.js index 4a288e50..8a5d5f8d 100644 --- a/src/map/Map.js +++ b/src/map/Map.js @@ -17,7 +17,9 @@ L.Map = L.Evented.extend({ trackResize: true, markerZoomAnimation: true, maxBoundsViscosity: 0.0, - transform3DLimit: 8388608 // Precision limit of a 32-bit float + transform3DLimit: 8388608, // Precision limit of a 32-bit float + zoomSnap: 1, + zoomDelta: 1 }, initialize: function (id, options) { // (HTMLElement or String, Object) @@ -72,11 +74,13 @@ L.Map = L.Evented.extend({ }, zoomIn: function (delta, options) { - return this.setZoom(this._zoom + (delta || 1), options); + delta = delta || (L.Browser.any3d ? this.options.zoomDelta : 1); + return this.setZoom(this._zoom + delta, options); }, zoomOut: function (delta, options) { - return this.setZoom(this._zoom - (delta || 1), options); + delta = delta || (L.Browser.any3d ? this.options.zoomDelta : 1); + return this.setZoom(this._zoom - delta, options); }, setZoomAround: function (latlng, zoom, options) { @@ -232,11 +236,8 @@ L.Map = L.Evented.extend({ }, stop: function () { - L.Util.cancelAnimFrame(this._flyToFrame); - if (this._panAnim) { - this._panAnim.stop(); - } - return this; + this.setZoom(this._limitZoom(this._zoom)); + return this._stop(); }, // TODO handler.addTo @@ -330,31 +331,25 @@ L.Map = L.Evented.extend({ getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number bounds = L.latLngBounds(bounds); - - var zoom = this.getMinZoom() - (inside ? 1 : 0), - maxZoom = this.getMaxZoom(), - size = this.getSize(), - - nw = bounds.getNorthWest(), - se = bounds.getSouthEast(), - - zoomNotFound = true, - boundsSize; - padding = L.point(padding || [0, 0]); - do { - zoom++; - boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding).floor(); - zoomNotFound = !inside ? size.contains(boundsSize) : boundsSize.x < size.x || boundsSize.y < size.y; + var zoom = this.getZoom(), + min = this.getMinZoom(), + max = this.getMaxZoom(), + nw = bounds.getNorthWest(), + se = bounds.getSouthEast(), + size = this.getSize(), + boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding), + snap = L.Browser.any3d ? this.options.zoomSnap : 1; - } while (zoomNotFound && zoom <= maxZoom); + var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y); + zoom = this.getScaleZoom(scale, zoom); - if (zoomNotFound && inside) { - return null; + if (snap) { + zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap; } - return inside ? zoom : zoom - 1; + return Math.max(min, Math.min(max, zoom)); }, getSize: function () { @@ -582,6 +577,14 @@ L.Map = L.Evented.extend({ return this.fire('moveend'); }, + _stop: function () { + L.Util.cancelAnimFrame(this._flyToFrame); + if (this._panAnim) { + this._panAnim.stop(); + } + return this; + }, + _rawPanBy: function (offset) { L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); }, @@ -837,9 +840,11 @@ L.Map = L.Evented.extend({ _limitZoom: function (zoom) { var min = this.getMinZoom(), - max = this.getMaxZoom(); - if (!L.Browser.any3d) { zoom = Math.round(zoom); } - + max = this.getMaxZoom(), + snap = L.Browser.any3d ? this.options.zoomSnap : 1; + if (snap) { + zoom = Math.round(zoom / snap) * snap; + } return Math.max(min, Math.min(max, zoom)); } }); diff --git a/src/map/anim/Map.FlyTo.js b/src/map/anim/Map.FlyTo.js index a238919f..a0077d1f 100644 --- a/src/map/anim/Map.FlyTo.js +++ b/src/map/anim/Map.FlyTo.js @@ -7,7 +7,7 @@ L.Map.include({ return this.setView(targetCenter, targetZoom, options); } - this.stop(); + this._stop(); var from = this.project(this.getCenter()), to = this.project(targetCenter), diff --git a/src/map/anim/Map.PanAnimation.js b/src/map/anim/Map.PanAnimation.js index 5b6fdd2f..552f7c57 100644 --- a/src/map/anim/Map.PanAnimation.js +++ b/src/map/anim/Map.PanAnimation.js @@ -10,7 +10,7 @@ L.Map.include({ center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds); options = options || {}; - this.stop(); + this._stop(); if (this._loaded && !options.reset && options !== true) { diff --git a/src/map/handler/Map.DoubleClickZoom.js b/src/map/handler/Map.DoubleClickZoom.js index 8f381406..1af4baae 100644 --- a/src/map/handler/Map.DoubleClickZoom.js +++ b/src/map/handler/Map.DoubleClickZoom.js @@ -18,7 +18,8 @@ L.Map.DoubleClickZoom = L.Handler.extend({ _onDoubleClick: function (e) { var map = this._map, oldZoom = map.getZoom(), - zoom = e.originalEvent.shiftKey ? Math.ceil(oldZoom) - 1 : Math.floor(oldZoom) + 1; + delta = map.options.zoomDelta, + zoom = e.originalEvent.shiftKey ? oldZoom - delta : oldZoom + delta; if (map.options.doubleClickZoom === 'center') { map.setZoom(zoom); diff --git a/src/map/handler/Map.Drag.js b/src/map/handler/Map.Drag.js index 44cf40ea..3d9cc166 100644 --- a/src/map/handler/Map.Drag.js +++ b/src/map/handler/Map.Drag.js @@ -54,7 +54,7 @@ L.Map.Drag = L.Handler.extend({ }, _onDown: function () { - this._map.stop(); + this._map._stop(); }, _onDragStart: function () { diff --git a/src/map/handler/Map.Keyboard.js b/src/map/handler/Map.Keyboard.js index 7a4e7ccd..d82a152e 100644 --- a/src/map/handler/Map.Keyboard.js +++ b/src/map/handler/Map.Keyboard.js @@ -4,8 +4,7 @@ L.Map.mergeOptions({ keyboard: true, - keyboardPanOffset: 80, - keyboardZoomOffset: 1 + keyboardPanDelta: 80 }); L.Map.Keyboard = L.Handler.extend({ @@ -22,8 +21,8 @@ L.Map.Keyboard = L.Handler.extend({ initialize: function (map) { this._map = map; - this._setPanOffset(map.options.keyboardPanOffset); - this._setZoomOffset(map.options.keyboardZoomOffset); + this._setPanDelta(map.options.keyboardPanDelta); + this._setZoomDelta(map.options.zoomDelta); }, addHooks: function () { @@ -84,35 +83,35 @@ L.Map.Keyboard = L.Handler.extend({ this._map.fire('blur'); }, - _setPanOffset: function (pan) { + _setPanDelta: function (panDelta) { var keys = this._panKeys = {}, codes = this.keyCodes, i, len; for (i = 0, len = codes.left.length; i < len; i++) { - keys[codes.left[i]] = [-1 * pan, 0]; + keys[codes.left[i]] = [-1 * panDelta, 0]; } for (i = 0, len = codes.right.length; i < len; i++) { - keys[codes.right[i]] = [pan, 0]; + keys[codes.right[i]] = [panDelta, 0]; } for (i = 0, len = codes.down.length; i < len; i++) { - keys[codes.down[i]] = [0, pan]; + keys[codes.down[i]] = [0, panDelta]; } for (i = 0, len = codes.up.length; i < len; i++) { - keys[codes.up[i]] = [0, -1 * pan]; + keys[codes.up[i]] = [0, -1 * panDelta]; } }, - _setZoomOffset: function (zoom) { + _setZoomDelta: function (zoomDelta) { var keys = this._zoomKeys = {}, codes = this.keyCodes, i, len; for (i = 0, len = codes.zoomIn.length; i < len; i++) { - keys[codes.zoomIn[i]] = zoom; + keys[codes.zoomIn[i]] = zoomDelta; } for (i = 0, len = codes.zoomOut.length; i < len; i++) { - keys[codes.zoomOut[i]] = -zoom; + keys[codes.zoomOut[i]] = -zoomDelta; } }, diff --git a/src/map/handler/Map.ScrollWheelZoom.js b/src/map/handler/Map.ScrollWheelZoom.js index bb8d94eb..55a3afbc 100644 --- a/src/map/handler/Map.ScrollWheelZoom.js +++ b/src/map/handler/Map.ScrollWheelZoom.js @@ -4,7 +4,8 @@ L.Map.mergeOptions({ scrollWheelZoom: true, - wheelDebounceTime: 40 + wheelDebounceTime: 40, + wheelPxPerZoomLevel: 50 }); L.Map.ScrollWheelZoom = L.Handler.extend({ @@ -40,14 +41,15 @@ L.Map.ScrollWheelZoom = L.Handler.extend({ _performZoom: function () { var map = this._map, - delta = this._delta, zoom = map.getZoom(); - map.stop(); // stop panning and fly animations if any + map._stop(); // stop panning and fly animations if any // map the delta with a sigmoid function to -4..4 range leaning on -1..1 - var d2 = Math.ceil(4 * Math.log(2 / (1 + Math.exp(-Math.abs(delta / 200)))) / Math.LN2); - delta = map._limitZoom(zoom + (delta > 0 ? d2 : -d2)) - zoom; + var d2 = this._delta / (this._map.options.wheelPxPerZoomLevel * 4), + d3 = 4 * Math.log(2 / (1 + Math.exp(-Math.abs(d2)))) / Math.LN2, + d4 = Math.max(d3, this._map.options.zoomSnap || 0), + delta = map._limitZoom(zoom + (this._delta > 0 ? d4 : -d4)) - zoom; this._delta = 0; this._startTime = null; diff --git a/src/map/handler/Map.TouchZoom.js b/src/map/handler/Map.TouchZoom.js index f142e1a4..daaa86c7 100644 --- a/src/map/handler/Map.TouchZoom.js +++ b/src/map/handler/Map.TouchZoom.js @@ -36,7 +36,7 @@ L.Map.TouchZoom = L.Handler.extend({ this._moved = false; this._zooming = true; - map.stop(); + map._stop(); L.DomEvent .on(document, 'touchmove', this._onTouchMove, this) @@ -97,11 +97,8 @@ L.Map.TouchZoom = L.Handler.extend({ .off(document, 'touchmove', this._onTouchMove) .off(document, 'touchend', this._onTouchEnd); - var zoom = this._zoom; - zoom = this._map._limitZoom(zoom - this._startZoom > 0 ? Math.ceil(zoom) : Math.floor(zoom)); - - - this._map._animateZoom(this._center, zoom, true, true); + // Pinch updates GridLayers' levels only when snapZoom is off, so snapZoom becomes noUpdate. + this._map._animateZoom(this._center, this._map._limitZoom(this._zoom), true, this._map.options.snapZoom); } });