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() {