diff --git a/examples/center.html b/examples/center.html index 34d6b8c4e3..11eae3ce7d 100644 --- a/examples/center.html +++ b/examples/center.html @@ -21,8 +21,6 @@ tags: "center, rotation, openstreetmap"
- (best fit),
- (respect resolution constraint).
- (nearest),
+ (best fit).
(with min resolution),
diff --git a/examples/center.js b/examples/center.js index 48602a94df..e1f2e029d5 100644 --- a/examples/center.js +++ b/examples/center.js @@ -47,29 +47,14 @@ const map = new Map({ view: view }); -const zoomtoswitzerlandbest = document.getElementById('zoomtoswitzerlandbest'); -zoomtoswitzerlandbest.addEventListener('click', function() { - const feature = source.getFeatures()[0]; - const polygon = /** @type {import("../src/ol/geom/SimpleGeometry.js").default} */ (feature.getGeometry()); - view.fit(polygon, {padding: [170, 50, 30, 150], constrainResolution: false}); -}, false); - -const zoomtoswitzerlandconstrained = - document.getElementById('zoomtoswitzerlandconstrained'); -zoomtoswitzerlandconstrained.addEventListener('click', function() { +const zoomtoswitzerland = + document.getElementById('zoomtoswitzerland'); +zoomtoswitzerland.addEventListener('click', function() { const feature = source.getFeatures()[0]; const polygon = /** @type {import("../src/ol/geom/SimpleGeometry.js").default} */ (feature.getGeometry()); view.fit(polygon, {padding: [170, 50, 30, 150]}); }, false); -const zoomtoswitzerlandnearest = - document.getElementById('zoomtoswitzerlandnearest'); -zoomtoswitzerlandnearest.addEventListener('click', function() { - const feature = source.getFeatures()[0]; - const polygon = /** @type {import("../src/ol/geom/SimpleGeometry.js").default} */ (feature.getGeometry()); - view.fit(polygon, {padding: [170, 50, 30, 150], nearest: true}); -}, false); - const zoomtolausanne = document.getElementById('zoomtolausanne'); zoomtolausanne.addEventListener('click', function() { const feature = source.getFeatures()[1]; diff --git a/examples/interaction-options.js b/examples/interaction-options.js index bc3cc90621..63fd71cc4b 100644 --- a/examples/interaction-options.js +++ b/examples/interaction-options.js @@ -7,7 +7,7 @@ import OSM from '../src/ol/source/OSM.js'; const map = new Map({ interactions: defaultInteractions({ - constrainResolution: true, onFocusOnly: true + onFocusOnly: true }), layers: [ new TileLayer({ diff --git a/examples/pinch-zoom.js b/examples/pinch-zoom.js index b8f6dd1a2c..0fba1794c9 100644 --- a/examples/pinch-zoom.js +++ b/examples/pinch-zoom.js @@ -6,10 +6,8 @@ import OSM from '../src/ol/source/OSM.js'; const map = new Map({ - interactions: defaultInteractions({pinchZoom: false}).extend([ - new PinchZoom({ - constrainResolution: true // force zooming to a integer zoom - }) + interactions: defaultInteractions().extend([ + new PinchZoom() ]), layers: [ new TileLayer({ @@ -19,6 +17,7 @@ const map = new Map({ target: 'map', view: new View({ center: [0, 0], - zoom: 2 + zoom: 2, + constrainResolution: true }) }); diff --git a/src/ol/View.js b/src/ol/View.js index f71ae3625f..caed07efbb 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -23,6 +23,7 @@ import {createProjection, METERS_PER_UNIT} from './proj.js'; import Units from './proj/Units.js'; import {equals} from './coordinate'; import {easeOut} from './easing'; +import {createMinMaxResolution} from './resolutionconstraint'; /** @@ -60,9 +61,8 @@ import {easeOut} from './easing'; * @property {!Array} [padding=[0, 0, 0, 0]] Padding (in pixels) to be * cleared inside the view. Values in the array are top, right, bottom and left * padding. - * @property {boolean} [constrainResolution=true] Constrain the resolution. - * @property {boolean} [nearest=false] If `constrainResolution` is `true`, get - * the nearest extent instead of the closest that actually fits the view. + * @property {boolean} [nearest=false] If the view `constrainResolution` option is `true`, + * get the nearest extent instead of the closest that actually fits the view. * @property {number} [minResolution=0] Minimum resolution that we zoom to. * @property {number} [maxZoom] Maximum zoom level that we zoom to. If * `minResolution` is given, this property is ignored. @@ -120,6 +120,9 @@ import {easeOut} from './easing'; * resolution constraint. It is used together with `maxZoom` (or * `minResolution`) and `zoomFactor`. Note that if `maxResolution` is also * provided, it is given precedence over `minZoom`. + * @property {boolean} [constrainResolution] If true, the view will always + * animate to the closest zoom level after an interaction; false means + * intermediary zoom levels are allowed. Default is false. * @property {import("./proj.js").ProjectionLike} [projection='EPSG:3857'] The * projection. The default is Spherical Mercator. * @property {number} [resolution] The initial resolution for the view. The @@ -621,7 +624,7 @@ class View extends BaseObject { } if (!this.getAnimating()) { - this.resolveConstraints_(); + setTimeout(this.resolveConstraints_.bind(this), 0); } } @@ -790,6 +793,15 @@ class View extends BaseObject { this.applyOptions_(this.getUpdatedOptions_({minZoom: zoom})); } + /** + * Set whether the view shoud allow intermediary zoom levels. + * @param {boolean} enabled Whether the resolution is constrained. + * @api + */ + setConstrainResolution(enabled) { + this.applyOptions_(this.getUpdatedOptions_({constrainResolution: enabled})); + } + /** * Get the view projection. * @return {import("./proj/Projection.js").default} The projection of the view. @@ -1001,8 +1013,6 @@ class View extends BaseObject { } const padding = options.padding !== undefined ? options.padding : [0, 0, 0, 0]; - const constrainResolution = options.constrainResolution !== undefined ? - options.constrainResolution : true; const nearest = options.nearest !== undefined ? options.nearest : false; let minResolution; if (options.minResolution !== undefined) { @@ -1038,9 +1048,7 @@ class View extends BaseObject { [size[0] - padding[1] - padding[3], size[1] - padding[0] - padding[2]]); resolution = isNaN(resolution) ? minResolution : Math.max(resolution, minResolution); - if (constrainResolution) { - resolution = this.getValidResolution(resolution, nearest ? 0 : 1); - } + resolution = this.getValidResolution(resolution, nearest ? 0 : 1); // calculate center sinAngle = -sinAngle; // go back to original rotation @@ -1346,8 +1354,14 @@ export function createResolutionConstraint(options) { maxResolution = resolutions[minZoom]; minResolution = resolutions[maxZoom] !== undefined ? resolutions[maxZoom] : resolutions[resolutions.length - 1]; - resolutionConstraint = createSnapToResolutions(resolutions, - !options.constrainOnlyCenter && options.extent); + + if (options.constrainResolution) { + resolutionConstraint = createSnapToResolutions(resolutions, + !options.constrainOnlyCenter && options.extent); + } else { + resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, + !options.constrainOnlyCenter && options.extent); + } } else { // calculate the default min and max resolution const projection = createProjection(options.projection, 'EPSG:3857'); @@ -1391,9 +1405,14 @@ export function createResolutionConstraint(options) { Math.log(maxResolution / minResolution) / Math.log(zoomFactor)); minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom); - resolutionConstraint = createSnapToPower( - zoomFactor, maxResolution, minResolution, - !options.constrainOnlyCenter && options.extent); + if (options.constrainResolution) { + resolutionConstraint = createSnapToPower( + zoomFactor, maxResolution, minResolution, + !options.constrainOnlyCenter && options.extent); + } else { + resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, + !options.constrainOnlyCenter && options.extent); + } } return {constraint: resolutionConstraint, maxResolution: maxResolution, minResolution: minResolution, minZoom: minZoom, zoomFactor: zoomFactor}; diff --git a/src/ol/interaction.js b/src/ol/interaction.js index 7a3c8b449e..3f79f9dac6 100644 --- a/src/ol/interaction.js +++ b/src/ol/interaction.js @@ -44,8 +44,6 @@ export {default as Translate} from './interaction/Translate.js'; * focus. This affects the `MouseWheelZoom` and `DragPan` interactions and is * useful when page scroll is desired for maps that do not have the browser's * focus. - * @property {boolean} [constrainResolution=false] Zoom to the closest integer - * zoom level after the wheel/trackpad or pinch gesture ends. * @property {boolean} [doubleClickZoom=true] Whether double click zoom is * desired. * @property {boolean} [keyboard=true] Whether keyboard interaction is desired. @@ -127,7 +125,6 @@ export function defaults(opt_options) { const pinchZoom = options.pinchZoom !== undefined ? options.pinchZoom : true; if (pinchZoom) { interactions.push(new PinchZoom({ - constrainResolution: options.constrainResolution, duration: options.zoomDuration })); } @@ -146,7 +143,6 @@ export function defaults(opt_options) { if (mouseWheelZoom) { interactions.push(new MouseWheelZoom({ condition: options.onFocusOnly ? focus : undefined, - constrainResolution: options.constrainResolution, duration: options.zoomDuration })); } diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 120d4e42c0..8ba3c68fd0 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -34,9 +34,6 @@ export const Mode = { * {@link module:ol/events/condition~always}. * @property {number} [duration=250] Animation duration in milliseconds. * @property {number} [timeout=80] Mouse wheel timeout duration in milliseconds. - * @property {boolean} [constrainResolution=false] When using a trackpad or - * magic mouse, zoom to the closest integer zoom level after the scroll gesture - * ends. * @property {boolean} [useAnchor=true] Enable zooming using the mouse's * location as the anchor. When set to `false`, zooming in and out will zoom to * the center of the screen instead of zooming on the mouse's location. @@ -82,12 +79,6 @@ class MouseWheelZoom extends Interaction { */ this.useAnchor_ = options.useAnchor !== undefined ? options.useAnchor : true; - /** - * @private - * @type {boolean} - */ - this.constrainResolution_ = options.constrainResolution || false; - /** * @private * @type {import("../events/condition.js").Condition} @@ -238,7 +229,7 @@ class MouseWheelZoom extends Interaction { } view.setResolution(resolution); - if (rebound === 0 && this.constrainResolution_) { + if (rebound === 0) { const zoomDelta = delta > 0 ? -1 : 1; const newZoom = view.getValidZoomLevel(view.getZoom() + zoomDelta); view.animate({ diff --git a/src/ol/interaction/PinchZoom.js b/src/ol/interaction/PinchZoom.js index 282d543138..b3c1e16c90 100644 --- a/src/ol/interaction/PinchZoom.js +++ b/src/ol/interaction/PinchZoom.js @@ -10,8 +10,6 @@ import PointerInteraction, {centroid as centroidFromPointers} from './Pointer.js /** * @typedef {Object} Options * @property {number} [duration=400] Animation duration in milliseconds. - * @property {boolean} [constrainResolution=false] Zoom to the closest integer - * zoom level after the pinch gesture ends. */ @@ -37,12 +35,6 @@ class PinchZoom extends PointerInteraction { super(pointerOptions); - /** - * @private - * @type {boolean} - */ - this.constrainResolution_ = options.constrainResolution || false; - /** * @private * @type {import("../coordinate.js").Coordinate} diff --git a/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index 575d603286..c7198e9c4d 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -4,13 +4,24 @@ import {linearFindNearest} from './array.js'; import {clamp} from './math.js'; import {getHeight, getWidth} from './extent'; -import {clamp} from './math'; /** * @typedef {function((number|undefined), number, import("./size.js").Size, boolean=): (number|undefined)} Type */ +/** + * @param {number} resolution Resolution + * @param {import("./extent.js").Extent=} maxExtent Maximum allowed extent. + * @param {import("./size.js").Size} viewportSize Viewport size. + * @return {number} Capped resolution. + */ +function getCappedResolution(resolution, maxExtent, viewportSize) { + const xResolution = getWidth(maxExtent) / viewportSize[0]; + const yResolution = getHeight(maxExtent) / viewportSize[1]; + return Math.min(resolution, Math.min(xResolution, yResolution)); +} + /** * @param {Array} resolutions Resolutions. @@ -28,14 +39,7 @@ export function createSnapToResolutions(resolutions, opt_maxExtent) { */ function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { - let cappedRes = resolution; - - // apply constraint related to max extent - if (opt_maxExtent) { - const xResolution = getWidth(opt_maxExtent) / size[0]; - const yResolution = getHeight(opt_maxExtent) / size[1]; - cappedRes = Math.min(cappedRes, Math.min(xResolution, yResolution)); - } + const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution; // during interacting or animating, allow intermediary values if (opt_isMoving) { @@ -72,14 +76,7 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_m */ function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { - let cappedRes = Math.min(resolution, maxResolution); - - // apply constraint related to max extent - if (opt_maxExtent) { - const xResolution = getWidth(opt_maxExtent) / size[0]; - const yResolution = getHeight(opt_maxExtent) / size[1]; - cappedRes = Math.min(cappedRes, Math.min(xResolution, yResolution)); - } + const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution; // during interacting or animating, allow intermediary values if (opt_isMoving) { @@ -98,3 +95,29 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_m } }); } + +/** + * @param {number} maxResolution Max resolution. + * @param {number} minResolution Min resolution. + * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. + * @return {Type} Zoom function. + */ +export function createMinMaxResolution(maxResolution, minResolution, opt_maxExtent) { + return ( + /** + * @param {number|undefined} resolution Resolution. + * @param {number} direction Direction. + * @param {import("./size.js").Size} size Viewport size. + * @param {boolean=} opt_isMoving True if an interaction or animation is in progress. + * @return {number|undefined} Resolution. + */ + function(resolution, direction, size, opt_isMoving) { + if (resolution !== undefined) { + const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution; + return clamp(cappedRes, minResolution, maxResolution); + } else { + return undefined; + } + } + ); +} diff --git a/test/spec/ol/map.test.js b/test/spec/ol/map.test.js index 8d145682b7..9a929bc24e 100644 --- a/test/spec/ol/map.test.js +++ b/test/spec/ol/map.test.js @@ -565,7 +565,6 @@ describe('ol.Map', function() { const interactions = defaultInteractions(options); expect(interactions.getLength()).to.eql(1); expect(interactions.item(0)).to.be.a(MouseWheelZoom); - expect(interactions.item(0).constrainResolution_).to.eql(false); expect(interactions.item(0).useAnchor_).to.eql(true); interactions.item(0).setMouseAnchor(false); expect(interactions.item(0).useAnchor_).to.eql(false); @@ -601,21 +600,6 @@ describe('ol.Map', function() { const interactions = defaultInteractions(options); expect(interactions.getLength()).to.eql(1); expect(interactions.item(0)).to.be.a(PinchZoom); - expect(interactions.item(0).constrainResolution_).to.eql(false); - }); - }); - - describe('set constrainResolution option', function() { - it('set constrainResolution option', function() { - options.pinchZoom = true; - options.mouseWheelZoom = true; - options.constrainResolution = true; - const interactions = defaultInteractions(options); - expect(interactions.getLength()).to.eql(2); - expect(interactions.item(0)).to.be.a(PinchZoom); - expect(interactions.item(0).constrainResolution_).to.eql(true); - expect(interactions.item(1)).to.be.a(MouseWheelZoom); - expect(interactions.item(1).constrainResolution_).to.eql(true); }); }); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 796e320386..669a12ab7c 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -239,7 +239,8 @@ describe('ol.View', function() { it('works with minResolution and maxResolution', function() { const constraint = getConstraint({ maxResolution: 500, - minResolution: 100 + minResolution: 100, + constrainResolution: true }); expect(constraint(600, 0, 0)).to.be(500); @@ -255,7 +256,8 @@ describe('ol.View', function() { const constraint = getConstraint({ maxResolution: 500, minResolution: 1, - zoomFactor: 10 + zoomFactor: 10, + constrainResolution: true }); expect(constraint(1000, 0, 0)).to.be(500); @@ -1327,14 +1329,40 @@ describe('ol.View', function() { zoom: 5 }); }); - it('fits correctly to the geometry', function() { + it('fits correctly to the geometry (with unconstrained resolution)', function() { view.fit( new LineString([[6000, 46000], [6000, 47100], [7000, 46000]]), - {size: [200, 200], padding: [100, 0, 0, 100], constrainResolution: false}); + {size: [200, 200], padding: [100, 0, 0, 100]}); expect(view.getResolution()).to.be(11); expect(view.getCenter()[0]).to.be(5950); expect(view.getCenter()[1]).to.be(47100); + view.fit( + new Circle([6000, 46000], 1000), + {size: [200, 200]}); + expect(view.getResolution()).to.be(10); + expect(view.getCenter()[0]).to.be(6000); + expect(view.getCenter()[1]).to.be(46000); + + view.setRotation(Math.PI / 8); + view.fit( + new Circle([6000, 46000], 1000), + {size: [200, 200]}); + expect(view.getResolution()).to.roughlyEqual(10, 1e-9); + expect(view.getCenter()[0]).to.roughlyEqual(6000, 1e-9); + expect(view.getCenter()[1]).to.roughlyEqual(46000, 1e-9); + + view.setRotation(Math.PI / 4); + view.fit( + new LineString([[6000, 46000], [6000, 47100], [7000, 46000]]), + {size: [200, 200], padding: [100, 0, 0, 100]}); + expect(view.getResolution()).to.roughlyEqual(14.849242404917458, 1e-9); + expect(view.getCenter()[0]).to.roughlyEqual(5200, 1e-9); + expect(view.getCenter()[1]).to.roughlyEqual(46300, 1e-9); + }); + it('fits correctly to the geometry', function() { + view.setConstrainResolution(true); + view.fit( new LineString([[6000, 46000], [6000, 47100], [7000, 46000]]), {size: [200, 200], padding: [100, 0, 0, 100]}); @@ -1363,30 +1391,8 @@ describe('ol.View', function() { expect(view.getZoom()).to.be(6); expect(view.getCenter()[0]).to.be(5900); expect(view.getCenter()[1]).to.be(46100); - - view.fit( - new Circle([6000, 46000], 1000), - {size: [200, 200], constrainResolution: false}); - expect(view.getResolution()).to.be(10); - expect(view.getCenter()[0]).to.be(6000); - expect(view.getCenter()[1]).to.be(46000); - - view.setRotation(Math.PI / 8); - view.fit( - new Circle([6000, 46000], 1000), - {size: [200, 200], constrainResolution: false}); - expect(view.getResolution()).to.roughlyEqual(10, 1e-9); - expect(view.getCenter()[0]).to.roughlyEqual(6000, 1e-9); - expect(view.getCenter()[1]).to.roughlyEqual(46000, 1e-9); - - view.setRotation(Math.PI / 4); - view.fit( - new LineString([[6000, 46000], [6000, 47100], [7000, 46000]]), - {size: [200, 200], padding: [100, 0, 0, 100], constrainResolution: false}); - expect(view.getResolution()).to.roughlyEqual(14.849242404917458, 1e-9); - expect(view.getCenter()[0]).to.roughlyEqual(5200, 1e-9); - expect(view.getCenter()[1]).to.roughlyEqual(46300, 1e-9); }); + it('fits correctly to the extent', function() { view.fit([1000, 1000, 2000, 2000], {size: [200, 200]}); expect(view.getResolution()).to.be(5); @@ -1409,7 +1415,6 @@ describe('ol.View', function() { { size: [200, 200], padding: [100, 0, 0, 100], - constrainResolution: false, duration: 25 }); @@ -1531,7 +1536,8 @@ describe('ol.View', function() { view = new View({ maxResolution: 2500, zoomFactor: 5, - maxZoom: 4 + maxZoom: 4, + constrainResolution: true }); expect(view.getValidResolution(90, 1)).to.be(100); expect(view.getValidResolution(90, -1)).to.be(20); @@ -1542,7 +1548,8 @@ describe('ol.View', function() { it('works correctly with a specific resolution set', function() { view = new View({ zoom: 0, - resolutions: [512, 256, 128, 64, 32, 16, 8] + resolutions: [512, 256, 128, 64, 32, 16, 8], + constrainResolution: true }); expect(view.getValidResolution(1000, 1)).to.be(512); expect(view.getValidResolution(260, 1)).to.be(512);