From 815f5b38c89cf9a9c6d85523024494d723c4bbfe Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 15 May 2014 16:43:27 -0600 Subject: [PATCH 1/2] Accept resolution or zoom related options for constrained views Any of minResolution, maxResolution, minZoom, or maxZoom can be used to constrain the view resolutions. Resolution options are given precedence over zoom options. --- externs/olx.js | 43 +++++++-- src/ol/view2d.js | 75 +++++++++++---- test/spec/ol/view2d.test.js | 179 ++++++++++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+), 28 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 3cf5845f20..97248c2df7 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/view2d.js b/src/ol/view2d.js index 799c8ff98b..76a7d4aa7a 100644 --- a/src/ol/view2d.js +++ b/src/ol/view2d.js @@ -657,30 +657,65 @@ ol.View2D.createResolutionConstraint_ = function(options) { resolutionConstraint = ol.ResolutionConstraint.createSnapToResolutions( resolutions); } else { + // TODO: move these to be ol constants + // see https://github.com/openlayers/ol3/issues/2076 + var defaultMaxZoom = 28; + var defaultMinZoom = 0; + var defaultZoomFactor = 2; + + // 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, defaultMinZoom); + + var defaultMinResolution = defaultMaxResolution / Math.pow( + defaultZoomFactor, defaultMaxZoom - defaultMinZoom); + + var minZoom = goog.isDef(options.minZoom) ? + options.minZoom : defaultMinZoom; + + var maxZoom = goog.isDef(options.maxZoom) ? + options.maxZoom : defaultMaxZoom; + + var zoomFactor = goog.isDef(options.zoomFactor) ? + options.zoomFactor : defaultZoomFactor; + + // 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}; diff --git a/test/spec/ol/view2d.test.js b/test/spec/ol/view2d.test.js index aca6b628c9..604a98527a 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() { From 3793f33acfd913eabff8c5fd5055c163920a52fd Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 15 May 2014 17:27:53 -0600 Subject: [PATCH 2/2] Handle minZoom in getZoom and setZoom --- examples/zoom-constrained.html | 52 +++++++++++++++++++++++++++ examples/zoom-constrained.js | 23 ++++++++++++ src/ol/ol.js | 8 +++++ src/ol/view2d.js | 53 ++++++++++++++++------------ test/spec/ol/view2d.test.js | 64 ++++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 23 deletions(-) create mode 100644 examples/zoom-constrained.html create mode 100644 examples/zoom-constrained.js 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/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 76a7d4aa7a..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,12 +679,6 @@ ol.View2D.createResolutionConstraint_ = function(options) { resolutionConstraint = ol.ResolutionConstraint.createSnapToResolutions( resolutions); } else { - // TODO: move these to be ol constants - // see https://github.com/openlayers/ol3/issues/2076 - var defaultMaxZoom = 28; - var defaultMinZoom = 0; - var defaultZoomFactor = 2; - // calculate the default min and max resolution var projection = options.projection; var extent = ol.proj.createProjection(projection, 'EPSG:3857').getExtent(); @@ -673,19 +689,10 @@ ol.View2D.createResolutionConstraint_ = function(options) { Math.max(ol.extent.getWidth(extent), ol.extent.getHeight(extent)); var defaultMaxResolution = size / ol.DEFAULT_TILE_SIZE / Math.pow( - defaultZoomFactor, defaultMinZoom); + defaultZoomFactor, ol.DEFAULT_MIN_ZOOM); var defaultMinResolution = defaultMaxResolution / Math.pow( - defaultZoomFactor, defaultMaxZoom - defaultMinZoom); - - var minZoom = goog.isDef(options.minZoom) ? - options.minZoom : defaultMinZoom; - - var maxZoom = goog.isDef(options.maxZoom) ? - options.maxZoom : defaultMaxZoom; - - var zoomFactor = goog.isDef(options.zoomFactor) ? - options.zoomFactor : defaultZoomFactor; + defaultZoomFactor, defaultMaxZoom - ol.DEFAULT_MIN_ZOOM); // user provided maxResolution takes precedence maxResolution = options.maxResolution; @@ -718,7 +725,7 @@ ol.View2D.createResolutionConstraint_ = function(options) { 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 604a98527a..b9605b8a37 100644 --- a/test/spec/ol/view2d.test.js +++ b/test/spec/ol/view2d.test.js @@ -282,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() {