diff --git a/src/ol/View.js b/src/ol/View.js index 380196154d..bdfffe3a7b 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -123,6 +123,9 @@ import {createMinMaxResolution} from './resolutionconstraint'; * @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 {boolean} [smoothResolutionConstraint] If true, the resolution + * min/max values will be applied smoothly, i. e. allow the view to exceed slightly + * the given resolution or zoom bounds. Default is true. * @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 @@ -978,8 +981,7 @@ class View extends BaseObject { const zoomFactor = this.resolutions_[baseLevel] / this.resolutions_[baseLevel + 1]; return this.resolutions_[baseLevel] / Math.pow(zoomFactor, clamp(zoom - baseLevel, 0, 1)); } else { - return clamp(this.maxResolution_ / Math.pow(this.zoomFactor_, zoom - this.minZoom_), - this.minResolution_, this.maxResolution_); + return this.maxResolution_ / Math.pow(this.zoomFactor_, zoom - this.minZoom_); } } @@ -1203,7 +1205,7 @@ class View extends BaseObject { } /** - * Set the rotation for this view. + * Set the rotation for this view using an anchor. * @param {number} rotation The rotation of the view in radians. * @observable * @api @@ -1214,7 +1216,7 @@ class View extends BaseObject { } /** - * Zoom to a specific zoom level. + * Zoom to a specific zoom level using an anchor * @param {number} zoom Zoom level. * @api */ @@ -1263,7 +1265,7 @@ class View extends BaseObject { * @private */ resolveConstraints_(opt_duration, opt_resolutionDirection) { - const duration = opt_duration || 200; + const duration = opt_duration !== undefined ? opt_duration : 200; const direction = opt_resolutionDirection || 0; const newRotation = this.constraints_.rotation(this.targetRotation_); @@ -1289,7 +1291,7 @@ class View extends BaseObject { }); } } - + /** * Notify the View that an interaction has started. * @api @@ -1391,6 +1393,9 @@ export function createResolutionConstraint(options) { const zoomFactor = options.zoomFactor !== undefined ? options.zoomFactor : defaultZoomFactor; + const smooth = + options.smoothResolutionConstraint !== undefined ? options.smoothResolutionConstraint : true; + if (options.resolutions !== undefined) { const resolutions = options.resolutions; maxResolution = resolutions[minZoom]; @@ -1398,10 +1403,10 @@ export function createResolutionConstraint(options) { resolutions[maxZoom] : resolutions[resolutions.length - 1]; if (options.constrainResolution) { - resolutionConstraint = createSnapToResolutions(resolutions, + resolutionConstraint = createSnapToResolutions(resolutions, smooth, !options.constrainOnlyCenter && options.extent); } else { - resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, + resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, smooth, !options.constrainOnlyCenter && options.extent); } } else { @@ -1449,10 +1454,10 @@ export function createResolutionConstraint(options) { if (options.constrainResolution) { resolutionConstraint = createSnapToPower( - zoomFactor, maxResolution, minResolution, + zoomFactor, maxResolution, minResolution, smooth, !options.constrainOnlyCenter && options.extent); } else { - resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, + resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, smooth, !options.constrainOnlyCenter && options.extent); } } diff --git a/src/ol/centerconstraint.js b/src/ol/centerconstraint.js index cb57c34cec..0aee0057d0 100644 --- a/src/ol/centerconstraint.js +++ b/src/ol/centerconstraint.js @@ -33,9 +33,9 @@ export function createExtent(extent, onlyCenter, smooth) { const maxX = extent[2] - viewWidth / 2; const minY = extent[1] + viewHeight / 2; const maxY = extent[3] - viewHeight / 2; - let x = clamp(center[0], minX, maxX); - let y = clamp(center[1], minY, maxY); - let ratio = 30 * resolution; + let x = minX > maxX ? (maxX + minX) / 2 : clamp(center[0], minX, maxX); + let y = minY > maxY ? (maxY + minY) / 2 : clamp(center[1], minY, maxY); + const ratio = 30 * resolution; // during an interaction, allow some overscroll if (opt_isMoving && smooth) { diff --git a/src/ol/control/ZoomSlider.js b/src/ol/control/ZoomSlider.js index 124cf2130a..78d65fc07b 100644 --- a/src/ol/control/ZoomSlider.js +++ b/src/ol/control/ZoomSlider.js @@ -102,13 +102,13 @@ class ZoomSlider extends Control { * @type {number|undefined} * @private */ - this.previousX_; + this.startX_; /** * @type {number|undefined} * @private */ - this.previousY_; + this.startY_; /** * The calculated thumb size (border box plus margins). Set when initSlider_ @@ -234,9 +234,10 @@ class ZoomSlider extends Control { */ handleDraggerStart_(event) { if (!this.dragging_ && event.originalEvent.target === this.element.firstElementChild) { + const element = /** @type {HTMLElement} */ (this.element.firstElementChild); this.getMap().getView().beginInteraction(); - this.previousX_ = event.clientX; - this.previousY_ = event.clientY; + this.startX_ = event.clientX - parseFloat(element.style.left); + this.startY_ = event.clientY - parseFloat(element.style.top); this.dragging_ = true; if (this.dragListenerKeys_.length === 0) { @@ -260,15 +261,11 @@ class ZoomSlider extends Control { */ handleDraggerDrag_(event) { if (this.dragging_) { - const element = /** @type {HTMLElement} */ (this.element.firstElementChild); - const deltaX = event.clientX - this.previousX_ + parseFloat(element.style.left); - const deltaY = event.clientY - this.previousY_ + parseFloat(element.style.top); + const deltaX = event.clientX - this.startX_; + const deltaY = event.clientY - this.startY_; const relativePosition = this.getRelativePosition_(deltaX, deltaY); this.currentResolution_ = this.getResolutionForPosition_(relativePosition); this.getMap().getView().setResolution(this.currentResolution_); - this.setThumbPosition_(this.currentResolution_); - this.previousX_ = event.clientX; - this.previousY_ = event.clientY; } } @@ -282,18 +279,9 @@ class ZoomSlider extends Control { const view = this.getMap().getView(); view.endInteraction(); - const zoom = view.getValidZoomLevel( - view.getZoomForResolution(this.currentResolution_)); - - view.animate({ - zoom: zoom, - duration: this.duration_, - easing: easeOut - }); - this.dragging_ = false; - this.previousX_ = undefined; - this.previousY_ = undefined; + this.startX_ = undefined; + this.startY_ = undefined; this.dragListenerKeys_.forEach(unlistenByKey); this.dragListenerKeys_.length = 0; } @@ -360,7 +348,7 @@ class ZoomSlider extends Control { */ getPositionForResolution_(res) { const fn = this.getMap().getView().getValueForResolutionFunction(); - return 1 - fn(res); + return clamp(1 - fn(res), 0, 1); } } @@ -379,10 +367,8 @@ export function render(mapEvent) { this.initSlider_(); } const res = mapEvent.frameState.viewState.resolution; - if (res !== this.currentResolution_) { - this.currentResolution_ = res; - this.setThumbPosition_(res); - } + this.currentResolution_ = res; + this.setThumbPosition_(res); } diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 47c638464f..33f5ac4290 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -128,19 +128,12 @@ class MouseWheelZoom extends Interaction { */ this.trackpadDeltaPerZoom_ = 300; - /** - * The zoom factor by which scroll zooming is allowed to exceed the limits. - * @private - * @type {number} - */ - this.trackpadZoomBuffer_ = 1.5; - } /** * @private */ - decrementInteractingHint_() { + endInteraction_() { this.trackpadTimeoutId_ = undefined; const view = this.getMap().getView(); view.endInteraction(); @@ -211,7 +204,7 @@ class MouseWheelZoom extends Interaction { } else { view.beginInteraction(); } - this.trackpadTimeoutId_ = setTimeout(this.decrementInteractingHint_.bind(this), this.trackpadEventGap_); + this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.trackpadEventGap_); view.adjustZoom(-delta / this.trackpadDeltaPerZoom_, this.lastAnchor_); this.startTime_ = now; return false; diff --git a/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index c7198e9c4d..16384a3770 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -11,24 +11,45 @@ import {getHeight, getWidth} from './extent'; */ /** + * Returns a modified resolution taking into acocunt the viewport size and maximum + * allowed extent. * @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) { +function getViewportClampedResolution(resolution, maxExtent, viewportSize) { const xResolution = getWidth(maxExtent) / viewportSize[0]; const yResolution = getHeight(maxExtent) / viewportSize[1]; return Math.min(resolution, Math.min(xResolution, yResolution)); } +/** + * Returns a modified resolution to be between maxResolution and minResolution while + * still allowing the value to be slightly out of bounds. + * @param {number} resolution Resolution. + * @param {number} maxResolution Max resolution. + * @param {number} minResolution Min resolution. + * @return {number} Smoothed resolution. + */ +function getSmoothClampedResolution(resolution, maxResolution, minResolution) { + let result = Math.min(resolution, maxResolution); + + result *= Math.log(Math.max(1, resolution / maxResolution)) * 0.1 + 1; + if (minResolution) { + result = Math.max(result, minResolution); + result /= Math.log(Math.max(1, minResolution / resolution)) * 0.1 + 1; + } + return result; +} /** * @param {Array} resolutions Resolutions. + * @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true. * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. * @return {Type} Zoom function. */ -export function createSnapToResolutions(resolutions, opt_maxExtent) { +export function createSnapToResolutions(resolutions, opt_smooth, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. @@ -39,16 +60,23 @@ export function createSnapToResolutions(resolutions, opt_maxExtent) { */ function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { - const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution; + const maxResolution = resolutions[0]; + const minResolution = resolutions[resolutions.length - 1]; + const cappedMaxRes = opt_maxExtent ? + getViewportClampedResolution(maxResolution, opt_maxExtent, size) : + maxResolution; // during interacting or animating, allow intermediary values if (opt_isMoving) { - const maxResolution = resolutions[0]; - const minResolution = resolutions[resolutions.length - 1]; - return clamp(cappedRes, minResolution, maxResolution); + const smooth = opt_smooth !== undefined ? opt_smooth : true; + if (!smooth) { + return clamp(resolution, minResolution, cappedMaxRes); + } + return getSmoothClampedResolution(resolution, cappedMaxRes, minResolution); } - let z = Math.floor(linearFindNearest(resolutions, cappedRes, direction)); + const capped = Math.min(cappedMaxRes, resolution); + const z = Math.floor(linearFindNearest(resolutions, capped, direction)); return resolutions[z]; } else { return undefined; @@ -62,10 +90,11 @@ export function createSnapToResolutions(resolutions, opt_maxExtent) { * @param {number} power Power. * @param {number} maxResolution Maximum resolution. * @param {number=} opt_minResolution Minimum resolution. + * @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true. * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. * @return {Type} Zoom function. */ -export function createSnapToPower(power, maxResolution, opt_minResolution, opt_maxExtent) { +export function createSnapToPower(power, maxResolution, opt_minResolution, opt_smooth, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. @@ -76,20 +105,26 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_m */ function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { - const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution; + const cappedMaxRes = opt_maxExtent ? + getViewportClampedResolution(maxResolution, opt_maxExtent, size) : + maxResolution; + const minResolution = opt_minResolution !== undefined ? opt_minResolution : 0; // during interacting or animating, allow intermediary values if (opt_isMoving) { - return opt_minResolution !== undefined ? Math.max(opt_minResolution, cappedRes) : cappedRes; + const smooth = opt_smooth !== undefined ? opt_smooth : true; + if (!smooth) { + return clamp(resolution, minResolution, cappedMaxRes); + } + return getSmoothClampedResolution(resolution, cappedMaxRes, minResolution); } const offset = -direction * (0.5 - 1e-9) + 0.5; + const capped = Math.min(cappedMaxRes, resolution); const zoomLevel = Math.floor( - Math.log(maxResolution / cappedRes) / Math.log(power) + offset); + Math.log(maxResolution / capped) / Math.log(power) + offset); let newResolution = maxResolution / Math.pow(power, zoomLevel); - return opt_minResolution !== undefined ? - clamp(newResolution, opt_minResolution, maxResolution) : - Math.min(maxResolution, newResolution); + return clamp(newResolution, minResolution, cappedMaxRes); } else { return undefined; } @@ -99,10 +134,11 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_m /** * @param {number} maxResolution Max resolution. * @param {number} minResolution Min resolution. + * @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true. * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. * @return {Type} Zoom function. */ -export function createMinMaxResolution(maxResolution, minResolution, opt_maxExtent) { +export function createMinMaxResolution(maxResolution, minResolution, opt_smooth, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. @@ -113,8 +149,15 @@ export function createMinMaxResolution(maxResolution, minResolution, opt_maxExte */ function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { - const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution; - return clamp(cappedRes, minResolution, maxResolution); + const cappedMaxRes = opt_maxExtent ? + getViewportClampedResolution(maxResolution, opt_maxExtent, size) : + maxResolution; + const smooth = opt_smooth !== undefined ? opt_smooth : true; + + if (!smooth || !opt_isMoving) { + return clamp(resolution, minResolution, cappedMaxRes); + } + return getSmoothClampedResolution(resolution, cappedMaxRes, minResolution); } else { return undefined; } diff --git a/test/spec/ol/resolutionconstraint.test.js b/test/spec/ol/resolutionconstraint.test.js index 9ba06c788d..2ac0d8fb43 100644 --- a/test/spec/ol/resolutionconstraint.test.js +++ b/test/spec/ol/resolutionconstraint.test.js @@ -1,4 +1,5 @@ import {createSnapToResolutions, createSnapToPower} from '../../../src/ol/resolutionconstraint.js'; +import {createMinMaxResolution} from '../../../src/ol/resolutionconstraint'; describe('ol.resolutionconstraint', function() { @@ -238,4 +239,98 @@ describe('ol.resolutionconstraint', function() { }); }); }); + + describe('SnapToPower smooth constraint', function() { + + describe('snap to power, smooth constraint on', function() { + it('returns expected resolution value', function() { + let resolutionConstraint = createSnapToPower(2, 128, 16, true); + + expect(resolutionConstraint(150, 0, [100, 100], true)).to.be.greaterThan(128); + expect(resolutionConstraint(150, 0, [100, 100], true)).to.be.lessThan(150); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.be.greaterThan(128); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.be.lessThan(130); + expect(resolutionConstraint(128, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(16, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.be.greaterThan(15); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.be.lessThan(16); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.be.greaterThan(10); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.be.lessThan(16); + }); + }); + + describe('snap to power, smooth constraint off', function() { + it('returns expected resolution value', function() { + let resolutionConstraint = createSnapToPower(2, 128, 16, false); + + expect(resolutionConstraint(150, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(128, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(16, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.eql(16); + }); + }); + + describe('snap to resolutions, smooth constraint on', function() { + it('returns expected resolution value', function() { + let resolutionConstraint = createSnapToResolutions([128, 64, 32, 16], true); + + expect(resolutionConstraint(150, 0, [100, 100], true)).to.be.greaterThan(128); + expect(resolutionConstraint(150, 0, [100, 100], true)).to.be.lessThan(150); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.be.greaterThan(128); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.be.lessThan(130); + expect(resolutionConstraint(128, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(16, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.be.greaterThan(15); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.be.lessThan(16); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.be.greaterThan(10); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.be.lessThan(16); + }); + }); + + describe('snap to resolutions, smooth constraint off', function() { + it('returns expected resolution value', function() { + let resolutionConstraint = createSnapToResolutions([128, 64, 32, 16], false); + + expect(resolutionConstraint(150, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(128, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(16, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.eql(16); + }); + }); + + describe('min/max, smooth constraint on', function() { + it('returns expected resolution value', function() { + let resolutionConstraint = createMinMaxResolution(128, 16, true); + + expect(resolutionConstraint(150, 0, [100, 100], true)).to.be.greaterThan(128); + expect(resolutionConstraint(150, 0, [100, 100], true)).to.be.lessThan(150); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.be.greaterThan(128); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.be.lessThan(130); + expect(resolutionConstraint(128, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(16, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.be.greaterThan(15); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.be.lessThan(16); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.be.greaterThan(10); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.be.lessThan(16); + }); + }); + + describe('min/max, smooth constraint off', function() { + it('returns expected resolution value', function() { + let resolutionConstraint = createMinMaxResolution(128, 16, false); + + expect(resolutionConstraint(150, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(130, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(128, 0, [100, 100], true)).to.eql(128); + expect(resolutionConstraint(16, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(15, 0, [100, 100], true)).to.eql(16); + expect(resolutionConstraint(10, 0, [100, 100], true)).to.eql(16); + }); + }); + }); + });