diff --git a/examples/zoom-constrained.html b/examples/zoom-constrained.html new file mode 100644 index 0000000000..aea4b44a71 --- /dev/null +++ b/examples/zoom-constrained.html @@ -0,0 +1,52 @@ + + + + + + + + + + + Zoom Constrained Example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Zoom constrained example

+

Example of a zoom constrained view.

+
+

This map has a view that is constrained between zoom levels 9 and 13. This is done using the minZoom and maxZoom view options.

+

See the zoom-constrained.js source for details.

+
+
bing, zoom, minZoom, maxZoom
+
+ +
+ +
+ + + + + + + diff --git a/examples/zoom-constrained.js b/examples/zoom-constrained.js new file mode 100644 index 0000000000..a8ad02ddcc --- /dev/null +++ b/examples/zoom-constrained.js @@ -0,0 +1,23 @@ +goog.require('ol.Map'); +goog.require('ol.View2D'); +goog.require('ol.layer.Tile'); +goog.require('ol.source.BingMaps'); + + +var map = new ol.Map({ + target: 'map', + layers: [ + new ol.layer.Tile({ + source: new ol.source.BingMaps({ + key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3', + imagerySet: 'Aerial' + }) + }) + ], + view: new ol.View2D({ + center: [-13553864, 5918250], + zoom: 11, + minZoom: 9, + maxZoom: 13 + }) +}); diff --git a/externs/olx.js b/externs/olx.js index fe010184e3..820674337e 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -351,7 +351,9 @@ olx.ProjectionOptions.prototype.global; * constrainRotation: (boolean|number|undefined), * enableRotation: (boolean|undefined), * extent: (ol.Extent|undefined), + * minResolution: (number|undefined), * maxResolution: (number|undefined), + * minZoom: (number|undefined), * maxZoom: (number|undefined), * projection: ol.proj.ProjectionLike, * resolution: (number|undefined), @@ -400,23 +402,48 @@ olx.View2DOptions.prototype.extent; /** * The maximum resolution used to determine the resolution constraint. It is - * used together with `maxZoom` and `zoomFactor`. If unspecified it is - * calculated in such a way that the projection's validity extent fits in a - * 256x256 px tile. If the projection is Spherical Mercator (the default) then - * `maxResolution` defaults to `40075016.68557849 / 256 = 156543.03392804097`. + * used together with `minResolution` (or `maxZoom`) and `zoomFactor`. If + * unspecified it is calculated in such a way that the projection's validity + * extent fits in a 256x256 px tile. If the projection is Spherical Mercator + * (the default) then `maxResolution` defaults to `40075016.68557849 / 256 = + * 156543.03392804097`. * @type {number|undefined} */ olx.View2DOptions.prototype.maxResolution; +/** + * The minimum resolution used to determine the resolution constraint. It is + * used together with `maxResolution` (or `minZoom`) and `zoomFactor`. If + * unspecified it is calculated assuming 29 zoom levels (with a factor of 2). + * If the projection is Spherical Mercator (the default) then `minResolution` + * defaults to `40075016.68557849 / 256 / Math.pow(2, 28) = + * 0.0005831682455839253`. + * @type {number|undefined} + */ +olx.View2DOptions.prototype.minResolution; + + /** * The maximum zoom level used to determine the resolution constraint. It is - * used together with `maxResolution` and `zoomFactor`. Default is `28`. + * used together with `minZoom` (or `maxResolution`) and `zoomFactor`. Default + * is `28`. Note that if `minResolution` is also provided, it is given + * precedence over `maxZoom`. * @type {number|undefined} */ olx.View2DOptions.prototype.maxZoom; +/** + * The minimum zoom level used to determine the resolution constraint. It is + * used together with `maxZoom` (or `minResolution`) and `zoomFactor`. Default + * is `0`. Note that if `maxResolution` is also provided, it is given + * precedence over `minZoom`. + * @type {number|undefined} + */ +olx.View2DOptions.prototype.minZoom; + + /** * The projection. Default is `EPSG:3857` (Spherical Mercator). * @type {ol.proj.ProjectionLike} @@ -436,7 +463,8 @@ olx.View2DOptions.prototype.resolution; /** * Resolutions to determine the resolution constraint. If set the - * `maxResolution`, `maxZoom` and `zoomFactor` options are ignored. + * `maxResolution`, `minResolution`, `minZoom`, `maxZoom`, and `zoomFactor` + * options are ignored. * @type {Array.|undefined} */ olx.View2DOptions.prototype.resolutions; @@ -460,8 +488,7 @@ olx.View2DOptions.prototype.zoom; /** - * The zoom factor used to determine the resolution constraint. Used together - * with `maxResolution` and `maxZoom`. Default is `2`. + * The zoom factor used to determine the resolution constraint. Default is `2`. * @type {number|undefined} */ olx.View2DOptions.prototype.zoomFactor; diff --git a/src/ol/ol.js b/src/ol/ol.js index 68ab072af0..56c0874dc6 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -16,11 +16,19 @@ ol.BUFFER_REPLACE_UNUSED_ENTRIES_WITH_NANS = goog.DEBUG; /** + * TODO: rename this to something having to do with tile grids + * see https://github.com/openlayers/ol3/issues/2076 * @define {number} Default maximum zoom for default tile grids. */ ol.DEFAULT_MAX_ZOOM = 42; +/** + * @define {number} Default min zoom level for the map view. Default is `0`. + */ +ol.DEFAULT_MIN_ZOOM = 0; + + /** * @define {number} Default high water mark. */ diff --git a/src/ol/view2d.js b/src/ol/view2d.js index 799c8ff98b..a7ceb527c5 100644 --- a/src/ol/view2d.js +++ b/src/ol/view2d.js @@ -116,6 +116,12 @@ ol.View2D = function(opt_options) { */ this.minResolution_ = resolutionConstraintInfo.minResolution; + /** + * @private + * @type {number} + */ + this.minZoom_ = resolutionConstraintInfo.minZoom; + var centerConstraint = ol.View2D.createCenterConstraint_(options); var resolutionConstraint = resolutionConstraintInfo.constraint; var rotationConstraint = ol.View2D.createRotationConstraint_(options); @@ -131,7 +137,7 @@ ol.View2D = function(opt_options) { values[ol.View2DProperty.RESOLUTION] = options.resolution; } else if (goog.isDef(options.zoom)) { values[ol.View2DProperty.RESOLUTION] = this.constrainResolution( - this.maxResolution_, options.zoom); + this.maxResolution_, options.zoom - this.minZoom_); } values[ol.View2DProperty.ROTATION] = goog.isDef(options.rotation) ? options.rotation : 0; @@ -396,7 +402,7 @@ ol.View2D.prototype.getView3D = function() { * @todo api */ ol.View2D.prototype.getZoom = function() { - var zoom; + var offset; var resolution = this.getResolution(); if (goog.isDef(resolution)) { @@ -404,14 +410,14 @@ ol.View2D.prototype.getZoom = function() { do { res = this.constrainResolution(this.maxResolution_, z); if (res == resolution) { - zoom = z; + offset = z; break; } ++z; } while (res > this.minResolution_); } - return zoom; + return goog.isDef(offset) ? this.minZoom_ + offset : offset; }; @@ -621,7 +627,8 @@ goog.exportProperty( * @todo api */ ol.View2D.prototype.setZoom = function(zoom) { - var resolution = this.constrainResolution(this.maxResolution_, zoom, 0); + var resolution = this.constrainResolution( + this.maxResolution_, zoom - this.minZoom_, 0); this.setResolution(resolution); }; @@ -650,6 +657,21 @@ ol.View2D.createResolutionConstraint_ = function(options) { var resolutionConstraint; var maxResolution; var minResolution; + + // TODO: move these to be ol constants + // see https://github.com/openlayers/ol3/issues/2076 + var defaultMaxZoom = 28; + var defaultZoomFactor = 2; + + var minZoom = goog.isDef(options.minZoom) ? + options.minZoom : ol.DEFAULT_MIN_ZOOM; + + var maxZoom = goog.isDef(options.maxZoom) ? + options.maxZoom : defaultMaxZoom; + + var zoomFactor = goog.isDef(options.zoomFactor) ? + options.zoomFactor : defaultZoomFactor; + if (goog.isDef(options.resolutions)) { var resolutions = options.resolutions; maxResolution = resolutions[0]; @@ -657,33 +679,53 @@ ol.View2D.createResolutionConstraint_ = function(options) { resolutionConstraint = ol.ResolutionConstraint.createSnapToResolutions( resolutions); } else { + // calculate the default min and max resolution + var projection = options.projection; + var extent = ol.proj.createProjection(projection, 'EPSG:3857').getExtent(); + var size = goog.isNull(extent) ? + // use an extent that can fit the whole world if need be + 360 * ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES] / + ol.proj.METERS_PER_UNIT[projection.getUnits()] : + Math.max(ol.extent.getWidth(extent), ol.extent.getHeight(extent)); + + var defaultMaxResolution = size / ol.DEFAULT_TILE_SIZE / Math.pow( + defaultZoomFactor, ol.DEFAULT_MIN_ZOOM); + + var defaultMinResolution = defaultMaxResolution / Math.pow( + defaultZoomFactor, defaultMaxZoom - ol.DEFAULT_MIN_ZOOM); + + // user provided maxResolution takes precedence maxResolution = options.maxResolution; - if (!goog.isDef(maxResolution)) { - var projection = options.projection; - var projectionExtent = ol.proj.createProjection(projection, 'EPSG:3857') - .getExtent(); - var size = goog.isNull(projectionExtent) ? - // use an extent that can fit the whole world if need be - 360 * ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES] / - ol.proj.METERS_PER_UNIT[projection.getUnits()] : - Math.max(projectionExtent[2] - projectionExtent[0], - projectionExtent[3] - projectionExtent[1]); - maxResolution = size / ol.DEFAULT_TILE_SIZE; + if (goog.isDef(maxResolution)) { + minZoom = 0; + } else { + maxResolution = defaultMaxResolution / Math.pow(zoomFactor, minZoom); } - var maxZoom = options.maxZoom; - if (!goog.isDef(maxZoom)) { - maxZoom = 28; + + // user provided minResolution takes precedence + minResolution = options.minResolution; + if (!goog.isDef(minResolution)) { + if (goog.isDef(options.maxZoom)) { + if (goog.isDef(options.maxResolution)) { + minResolution = maxResolution / Math.pow(zoomFactor, maxZoom); + } else { + minResolution = defaultMaxResolution / Math.pow(zoomFactor, maxZoom); + } + } else { + minResolution = defaultMinResolution; + } } - var zoomFactor = options.zoomFactor; - if (!goog.isDef(zoomFactor)) { - zoomFactor = 2; - } - minResolution = maxResolution / Math.pow(zoomFactor, maxZoom); + + // given discrete zoom levels, minResolution may be different than provided + maxZoom = minZoom + Math.floor( + Math.log(maxResolution / minResolution) / Math.log(zoomFactor)); + minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom); + resolutionConstraint = ol.ResolutionConstraint.createSnapToPower( - zoomFactor, maxResolution, maxZoom); + zoomFactor, maxResolution, maxZoom - minZoom); } return {constraint: resolutionConstraint, maxResolution: maxResolution, - minResolution: minResolution}; + minResolution: minResolution, minZoom: minZoom}; }; diff --git a/test/spec/ol/view2d.test.js b/test/spec/ol/view2d.test.js index aca6b628c9..b9605b8a37 100644 --- a/test/spec/ol/view2d.test.js +++ b/test/spec/ol/view2d.test.js @@ -58,6 +58,185 @@ describe('ol.View2D', function() { }); }); + describe('with zoom related options', function() { + + var defaultMaxRes = 156543.03392804097; + function getConstraint(options) { + return ol.View2D.createResolutionConstraint_(options).constraint; + } + + it('works with only maxZoom', function() { + var maxZoom = 10; + var constraint = getConstraint({ + maxZoom: maxZoom + }); + + expect(constraint(defaultMaxRes, 0, 0)).to.roughlyEqual( + defaultMaxRes, 1e-9); + + expect(constraint(0, 0, 0)).to.roughlyEqual( + defaultMaxRes / Math.pow(2, maxZoom), 1e-9); + }); + + it('works with only minZoom', function() { + var minZoom = 5; + var constraint = getConstraint({ + minZoom: minZoom + }); + + expect(constraint(defaultMaxRes, 0, 0)).to.roughlyEqual( + defaultMaxRes / Math.pow(2, minZoom), 1e-9); + + expect(constraint(0, 0, 0)).to.roughlyEqual( + defaultMaxRes / Math.pow(2, 28), 1e-9); + }); + + it('works with maxZoom and minZoom', function() { + var minZoom = 2; + var maxZoom = 11; + var constraint = getConstraint({ + minZoom: minZoom, + maxZoom: maxZoom + }); + + expect(constraint(defaultMaxRes, 0, 0)).to.roughlyEqual( + defaultMaxRes / Math.pow(2, minZoom), 1e-9); + + expect(constraint(0, 0, 0)).to.roughlyEqual( + defaultMaxRes / Math.pow(2, maxZoom), 1e-9); + }); + + it('works with maxZoom, minZoom, and zoomFactor', function() { + var minZoom = 4; + var maxZoom = 8; + var zoomFactor = 3; + var constraint = getConstraint({ + minZoom: minZoom, + maxZoom: maxZoom, + zoomFactor: zoomFactor + }); + + expect(constraint(defaultMaxRes, 0, 0)).to.roughlyEqual( + defaultMaxRes / Math.pow(zoomFactor, minZoom), 1e-9); + + expect(constraint(0, 0, 0)).to.roughlyEqual( + defaultMaxRes / Math.pow(zoomFactor, maxZoom), 1e-9); + }); + + }); + + describe('with resolution related options', function() { + + var defaultMaxRes = 156543.03392804097; + function getConstraint(options) { + return ol.View2D.createResolutionConstraint_(options).constraint; + } + + it('works with only maxResolution', function() { + var maxResolution = 10e6; + var constraint = getConstraint({ + maxResolution: maxResolution + }); + + expect(constraint(maxResolution * 3, 0, 0)).to.roughlyEqual( + maxResolution, 1e-9); + + var minResolution = constraint(0, 0, 0); + var defaultMinRes = defaultMaxRes / Math.pow(2, 28); + + expect(minResolution).to.be.greaterThan(defaultMinRes); + expect(minResolution / defaultMinRes).to.be.lessThan(2); + }); + + it('works with only minResolution', function() { + var minResolution = 100; + var constraint = getConstraint({ + minResolution: minResolution + }); + + expect(constraint(defaultMaxRes, 0, 0)).to.roughlyEqual( + defaultMaxRes, 1e-9); + + var constrainedMinRes = constraint(0, 0, 0); + expect(constrainedMinRes).to.be.greaterThan(minResolution); + expect(constrainedMinRes / minResolution).to.be.lessThan(2); + }); + + it('works with minResolution and maxResolution', function() { + var constraint = getConstraint({ + maxResolution: 500, + minResolution: 100 + }); + + expect(constraint(600, 0, 0)).to.be(500); + expect(constraint(500, 0, 0)).to.be(500); + expect(constraint(400, 0, 0)).to.be(500); + expect(constraint(300, 0, 0)).to.be(250); + expect(constraint(200, 0, 0)).to.be(250); + expect(constraint(100, 0, 0)).to.be(125); + expect(constraint(0, 0, 0)).to.be(125); + }); + + it('accepts minResolution, maxResolution, and zoomFactor', function() { + var constraint = getConstraint({ + maxResolution: 500, + minResolution: 1, + zoomFactor: 10 + }); + + expect(constraint(1000, 0, 0)).to.be(500); + expect(constraint(500, 0, 0)).to.be(500); + expect(constraint(100, 0, 0)).to.be(50); + expect(constraint(50, 0, 0)).to.be(50); + expect(constraint(10, 0, 0)).to.be(5); + expect(constraint(1, 0, 0)).to.be(5); + }); + + }); + + describe('overspecified options (prefers resolution)', function() { + + var defaultMaxRes = 156543.03392804097; + function getConstraint(options) { + return ol.View2D.createResolutionConstraint_(options).constraint; + } + + it('respects maxResolution over minZoom', function() { + var maxResolution = 10e6; + var minZoom = 8; + var constraint = getConstraint({ + maxResolution: maxResolution, + minZoom: minZoom + }); + + expect(constraint(maxResolution * 3, 0, 0)).to.roughlyEqual( + maxResolution, 1e-9); + + var minResolution = constraint(0, 0, 0); + var defaultMinRes = defaultMaxRes / Math.pow(2, 28); + + expect(minResolution).to.be.greaterThan(defaultMinRes); + expect(minResolution / defaultMinRes).to.be.lessThan(2); + }); + + it('respects minResolution over maxZoom', function() { + var minResolution = 100; + var maxZoom = 50; + var constraint = getConstraint({ + minResolution: minResolution, + maxZoom: maxZoom + }); + + expect(constraint(defaultMaxRes, 0, 0)).to.roughlyEqual( + defaultMaxRes, 1e-9); + + var constrainedMinRes = constraint(0, 0, 0); + expect(constrainedMinRes).to.be.greaterThan(minResolution); + expect(constrainedMinRes / minResolution).to.be.lessThan(2); + }); + + }); + }); describe('create rotation constraint', function() { @@ -103,6 +282,70 @@ describe('ol.View2D', function() { }); }); + describe('#getZoom() - constrained', function() { + it('returns correct zoom levels', function() { + var view = new ol.View2D({ + minZoom: 10, + maxZoom: 20 + }); + + view.setZoom(5); + expect(view.getZoom()).to.be(10); + + view.setZoom(10); + expect(view.getZoom()).to.be(10); + + view.setZoom(15); + expect(view.getZoom()).to.be(15); + + view.setZoom(20); + expect(view.getZoom()).to.be(20); + + view.setZoom(25); + expect(view.getZoom()).to.be(20); + }); + }); + + describe('#getZoom() - custom ol.DEFAULT_MIN_ZOOM', function() { + var defaultMinZoom = ol.DEFAULT_MIN_ZOOM; + + afterEach(function() { + ol.DEFAULT_MIN_ZOOM = defaultMinZoom; + }); + + it('respects custom ol.DEFAULT_MIN_ZOOM', function() { + ol.DEFAULT_MIN_ZOOM = 2; + + var view = new ol.View2D(); + + view.setZoom(1); + expect(view.getZoom()).to.be(2); + + view.setZoom(2); + expect(view.getZoom()).to.be(2); + + view.setZoom(3); + expect(view.getZoom()).to.be(3); + }); + }); + + describe('#getZoom() - overspecified', function() { + + it('gives maxResolution precedence over minZoom', function() { + + var view = new ol.View2D({ + maxResolution: 100, + minZoom: 2 // this should get ignored + }); + + view.setResolution(100); + expect(view.getZoom()).to.be(0); + + view.setZoom(0); + expect(view.getResolution()).to.be(100); + }); + }); + describe('fitGeometry', function() { var view; beforeEach(function() {