Merge pull request #3523 from Leaflet/fractional-zoom-controls

Fractional zoom controls
This commit is contained in:
Vladimir Agafonkin 2016-02-08 16:39:04 +02:00
commit 09c3221211
11 changed files with 308 additions and 59 deletions

109
debug/map/zoom-delta.html Normal file
View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<head>
<title>Leaflet debug page</title>
<meta name="viewport" content="initial-scale=1.0" />
<link rel="stylesheet" href="../../dist/leaflet.css" />
<link rel="stylesheet" href="../css/mobile.css" />
<script type="text/javascript" src="../../build/deps.js"></script>
<script src="../leaflet-include.js"></script>
<style>
.container {
float:left; width: 600px; height: 600px;
position: relative;
border: 1px solid gray;
}
#map1, #map2 {
position:absolute;
top:2em;
bottom:2em;
left:0;
right:0;
}
#zoom1, #zoom2 {
position:absolute;
bottom:0;
left:0;
right:0;
}
</style>
</head>
<body>
<h1>Zoom delta test.</h1>
<p>Zooming with touch zoom, box zoom or flyTo then <code>map.stop()</code> must make the zoom level snap to the value of the <code>zoomSnap</code> option. Zoom interactions (keyboard, mouse wheel, zoom control buttons must change the zoom by the amount in the <code>zoomDelta</code> option.</p>
<div>
<button id="sf">SF</button>
<button id="trd">TRD</button>
<button id="stop">stop</button>
</div>
<div class='container'>
Snap: 0.25. Delta: 0.5.
<div id="map1"></div>
<span id="zoom1"></span>
</div>
<div class='container'>
Snap: 0 (off). Delta: 0.25.
<div id="map2"></div>
<span id="zoom2"></span>
</div>
<script type="text/javascript">
var sf = [37.77, -122.42],
trd = [63.41, 10.41];
var osmUrl = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
osmAttrib = '&copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors',
osm1 = L.tileLayer(osmUrl, {maxZoom: 18, attribution: osmAttrib}),
osm2 = L.tileLayer(osmUrl, {maxZoom: 18, attribution: osmAttrib}),
center = L.latLng(63.41, 10.41);
var map1 = new L.Map('map1', {
center: center,
layers: [osm1],
zoom: 5,
zoomSnap: 0.25,
zoomDelta: 0.5,
wheelPxPerZoomLevel: 50
});
var map2 = new L.Map('map2', {
center: center,
layers: [osm2],
zoom: 5,
zoomSnap: 0,
zoomDelta: 0.25,
wheelPxPerZoomLevel: 50
});
map1.on('zoomend',function(){
document.getElementById('zoom1').innerHTML = "Zoom level: " + map1.getZoom();
});
map2.on('zoomend',function(){
document.getElementById('zoom2').innerHTML = "Zoom level: " + map2.getZoom();
});
document.getElementById('sf').onclick = function () {
map1.flyTo(sf, 10, {duration: 20});
map2.flyTo(sf, 10, {duration: 20});
};
document.getElementById('trd').onclick = function () {
map1.flyTo(trd, 10, {duration: 20});
map2.flyTo(trd, 10, {duration: 20});
};
document.getElementById('stop').onclick = function () {
map1.stop();
map2.stop();
};
</script>
</body>
</html>

View File

@ -624,6 +624,142 @@ describe("Map", function () {
map.setView([0, 0], 0); map.setView([0, 0], 0);
map.once('zoomend', callback).flyTo(newCenter, newZoom); 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});
});
}); });

View File

@ -45,13 +45,13 @@ L.Control.Zoom = L.Control.extend({
_zoomIn: function (e) { _zoomIn: function (e) {
if (!this._disabled) { if (!this._disabled) {
this._map.zoomIn(e.shiftKey ? 3 : 1); this._map.zoomIn(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1));
} }
}, },
_zoomOut: function (e) { _zoomOut: function (e) {
if (!this._disabled) { if (!this._disabled) {
this._map.zoomOut(e.shiftKey ? 3 : 1); this._map.zoomOut(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1));
} }
}, },

View File

@ -17,7 +17,9 @@ L.Map = L.Evented.extend({
trackResize: true, trackResize: true,
markerZoomAnimation: true, markerZoomAnimation: true,
maxBoundsViscosity: 0.0, 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) initialize: function (id, options) { // (HTMLElement or String, Object)
@ -72,11 +74,13 @@ L.Map = L.Evented.extend({
}, },
zoomIn: function (delta, options) { 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) { 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) { setZoomAround: function (latlng, zoom, options) {
@ -232,11 +236,8 @@ L.Map = L.Evented.extend({
}, },
stop: function () { stop: function () {
L.Util.cancelAnimFrame(this._flyToFrame); this.setZoom(this._limitZoom(this._zoom));
if (this._panAnim) { return this._stop();
this._panAnim.stop();
}
return this;
}, },
// TODO handler.addTo // TODO handler.addTo
@ -330,31 +331,25 @@ L.Map = L.Evented.extend({
getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
bounds = L.latLngBounds(bounds); 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]); padding = L.point(padding || [0, 0]);
do { var zoom = this.getZoom(),
zoom++; min = this.getMinZoom(),
boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding).floor(); max = this.getMaxZoom(),
zoomNotFound = !inside ? size.contains(boundsSize) : boundsSize.x < size.x || boundsSize.y < size.y; 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) { if (snap) {
return null; 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 () { getSize: function () {
@ -582,6 +577,14 @@ L.Map = L.Evented.extend({
return this.fire('moveend'); return this.fire('moveend');
}, },
_stop: function () {
L.Util.cancelAnimFrame(this._flyToFrame);
if (this._panAnim) {
this._panAnim.stop();
}
return this;
},
_rawPanBy: function (offset) { _rawPanBy: function (offset) {
L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
}, },
@ -837,9 +840,11 @@ L.Map = L.Evented.extend({
_limitZoom: function (zoom) { _limitZoom: function (zoom) {
var min = this.getMinZoom(), var min = this.getMinZoom(),
max = this.getMaxZoom(); max = this.getMaxZoom(),
if (!L.Browser.any3d) { zoom = Math.round(zoom); } snap = L.Browser.any3d ? this.options.zoomSnap : 1;
if (snap) {
zoom = Math.round(zoom / snap) * snap;
}
return Math.max(min, Math.min(max, zoom)); return Math.max(min, Math.min(max, zoom));
} }
}); });

View File

@ -7,7 +7,7 @@ L.Map.include({
return this.setView(targetCenter, targetZoom, options); return this.setView(targetCenter, targetZoom, options);
} }
this.stop(); this._stop();
var from = this.project(this.getCenter()), var from = this.project(this.getCenter()),
to = this.project(targetCenter), to = this.project(targetCenter),

View File

@ -10,7 +10,7 @@ L.Map.include({
center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds); center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds);
options = options || {}; options = options || {};
this.stop(); this._stop();
if (this._loaded && !options.reset && options !== true) { if (this._loaded && !options.reset && options !== true) {

View File

@ -18,7 +18,8 @@ L.Map.DoubleClickZoom = L.Handler.extend({
_onDoubleClick: function (e) { _onDoubleClick: function (e) {
var map = this._map, var map = this._map,
oldZoom = map.getZoom(), 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') { if (map.options.doubleClickZoom === 'center') {
map.setZoom(zoom); map.setZoom(zoom);

View File

@ -54,7 +54,7 @@ L.Map.Drag = L.Handler.extend({
}, },
_onDown: function () { _onDown: function () {
this._map.stop(); this._map._stop();
}, },
_onDragStart: function () { _onDragStart: function () {

View File

@ -4,8 +4,7 @@
L.Map.mergeOptions({ L.Map.mergeOptions({
keyboard: true, keyboard: true,
keyboardPanOffset: 80, keyboardPanDelta: 80
keyboardZoomOffset: 1
}); });
L.Map.Keyboard = L.Handler.extend({ L.Map.Keyboard = L.Handler.extend({
@ -22,8 +21,8 @@ L.Map.Keyboard = L.Handler.extend({
initialize: function (map) { initialize: function (map) {
this._map = map; this._map = map;
this._setPanOffset(map.options.keyboardPanOffset); this._setPanDelta(map.options.keyboardPanDelta);
this._setZoomOffset(map.options.keyboardZoomOffset); this._setZoomDelta(map.options.zoomDelta);
}, },
addHooks: function () { addHooks: function () {
@ -84,35 +83,35 @@ L.Map.Keyboard = L.Handler.extend({
this._map.fire('blur'); this._map.fire('blur');
}, },
_setPanOffset: function (pan) { _setPanDelta: function (panDelta) {
var keys = this._panKeys = {}, var keys = this._panKeys = {},
codes = this.keyCodes, codes = this.keyCodes,
i, len; i, len;
for (i = 0, len = codes.left.length; i < len; i++) { 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++) { 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++) { 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++) { 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 = {}, var keys = this._zoomKeys = {},
codes = this.keyCodes, codes = this.keyCodes,
i, len; i, len;
for (i = 0, len = codes.zoomIn.length; i < len; i++) { 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++) { for (i = 0, len = codes.zoomOut.length; i < len; i++) {
keys[codes.zoomOut[i]] = -zoom; keys[codes.zoomOut[i]] = -zoomDelta;
} }
}, },

View File

@ -4,7 +4,8 @@
L.Map.mergeOptions({ L.Map.mergeOptions({
scrollWheelZoom: true, scrollWheelZoom: true,
wheelDebounceTime: 40 wheelDebounceTime: 40,
wheelPxPerZoomLevel: 50
}); });
L.Map.ScrollWheelZoom = L.Handler.extend({ L.Map.ScrollWheelZoom = L.Handler.extend({
@ -40,14 +41,15 @@ L.Map.ScrollWheelZoom = L.Handler.extend({
_performZoom: function () { _performZoom: function () {
var map = this._map, var map = this._map,
delta = this._delta,
zoom = map.getZoom(); 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 // 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); var d2 = this._delta / (this._map.options.wheelPxPerZoomLevel * 4),
delta = map._limitZoom(zoom + (delta > 0 ? d2 : -d2)) - zoom; 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._delta = 0;
this._startTime = null; this._startTime = null;

View File

@ -36,7 +36,7 @@ L.Map.TouchZoom = L.Handler.extend({
this._moved = false; this._moved = false;
this._zooming = true; this._zooming = true;
map.stop(); map._stop();
L.DomEvent L.DomEvent
.on(document, 'touchmove', this._onTouchMove, this) .on(document, 'touchmove', this._onTouchMove, this)
@ -97,11 +97,8 @@ L.Map.TouchZoom = L.Handler.extend({
.off(document, 'touchmove', this._onTouchMove) .off(document, 'touchmove', this._onTouchMove)
.off(document, 'touchend', this._onTouchEnd); .off(document, 'touchend', this._onTouchEnd);
var zoom = this._zoom; // Pinch updates GridLayers' levels only when snapZoom is off, so snapZoom becomes noUpdate.
zoom = this._map._limitZoom(zoom - this._startZoom > 0 ? Math.ceil(zoom) : Math.floor(zoom)); this._map._animateZoom(this._center, this._map._limitZoom(this._zoom), true, this._map.options.snapZoom);
this._map._animateZoom(this._center, zoom, true, true);
} }
}); });