diff --git a/debug/map/zoom-delta.html b/debug/map/zoom-delta.html new file mode 100644 index 00000000..ee449a6c --- /dev/null +++ b/debug/map/zoom-delta.html @@ -0,0 +1,107 @@ + + + + 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..3346fe2d 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/map/Map.js b/src/map/Map.js index 4a288e50..327a59e7 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 @@ -331,9 +332,10 @@ 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), + var zoom, maxZoom = this.getMaxZoom(), size = this.getSize(), + snap = L.Browser.any3d ? this.options.zoomSnap : 1, nw = bounds.getNorthWest(), se = bounds.getSouthEast(), @@ -343,8 +345,17 @@ L.Map = L.Evented.extend({ padding = L.point(padding || [0, 0]); + if (snap <= 0) { + zoom = this.getZoom(); + boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding); + var scale = Math.min(size.x / boundsSize.x , size.y / boundsSize.y); + return this.getScaleZoom(scale, zoom); + } + + zoom = this.getMinZoom() - (inside ? snap : 0); + do { - zoom++; + zoom += snap; 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; @@ -354,7 +365,7 @@ L.Map = L.Evented.extend({ return null; } - return inside ? zoom : zoom - 1; + return inside ? zoom : zoom - snap; }, getSize: function () { @@ -582,6 +593,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 +856,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..27aaad12 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,7 +83,7 @@ L.Map.Keyboard = L.Handler.extend({ this._map.fire('blur'); }, - _setPanOffset: function (pan) { + _setPanDelta: function (pan) { var keys = this._panKeys = {}, codes = this.keyCodes, i, len; @@ -103,7 +102,7 @@ L.Map.Keyboard = L.Handler.extend({ } }, - _setZoomOffset: function (zoom) { + _setZoomDelta: function (zoom) { var keys = this._zoomKeys = {}, codes = this.keyCodes, i, len; diff --git a/src/map/handler/Map.ScrollWheelZoom.js b/src/map/handler/Map.ScrollWheelZoom.js index bb8d94eb..a04ab5d9 100644 --- a/src/map/handler/Map.ScrollWheelZoom.js +++ b/src/map/handler/Map.ScrollWheelZoom.js @@ -40,10 +40,10 @@ L.Map.ScrollWheelZoom = L.Handler.extend({ _performZoom: function () { var map = this._map, - delta = this._delta, + delta = this._delta * this._map.options.zoomDelta, 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); diff --git a/src/map/handler/Map.TouchZoom.js b/src/map/handler/Map.TouchZoom.js index f142e1a4..b773acdc 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 shall update GridLayers' levels only when snapzoom is off. + this._map._animateZoom(this._center, this._map._limitZoom(this._zoom), true, this._map.options.snapZoom); } });