From 815f5b38c89cf9a9c6d85523024494d723c4bbfe Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 15 May 2014 16:43:27 -0600 Subject: [PATCH] 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() {