diff --git a/src/ol/View.js b/src/ol/View.js index 67c63fa475..f71ae3625f 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -97,6 +97,9 @@ import {easeOut} from './easing'; * view, in other words, nothing outside of this extent can be visible on the map * @property {boolean} [constrainOnlyCenter] If true, the extent * constraint will only apply to the center and not the whole view. + * @property {boolean} [smoothExtentConstraint] If true, the extent + * constraint will be applied smoothly, i. e. allow the view to go slightly outside + * of the given `extent`. Default is true. * @property {number} [maxResolution] The maximum resolution used to determine * the resolution constraint. It is used together with `minResolution` (or * `maxZoom`) and `zoomFactor`. If unspecified it is calculated in such a way @@ -616,6 +619,10 @@ class View extends BaseObject { if (more && this.updateAnimationKey_ === undefined) { this.updateAnimationKey_ = requestAnimationFrame(this.updateAnimations_); } + + if (!this.getAnimating()) { + this.resolveConstraints_(); + } } /** @@ -1094,6 +1101,16 @@ class View extends BaseObject { return !!this.getCenter() && this.getResolution() !== undefined; } + /** + * Adds relative coordinates to the center of the view. + * @param {import("./coordinate.js").Coordinate} deltaCoordinates Relative value to add. + * @api + */ + adjustCenter(deltaCoordinates) { + const center = this.targetCenter_; + this.setCenter([center[0] + deltaCoordinates[0], center[1] + deltaCoordinates[1]]); + } + /** * Rotate the view around a given coordinate. * @param {number} rotation New rotation value for the view. @@ -1196,7 +1213,7 @@ class View extends BaseObject { * @private */ resolveConstraints_(opt_duration, opt_resolutionDirection) { - const duration = opt_duration || 250; + const duration = opt_duration || 200; const direction = opt_resolutionDirection || 0; const newRotation = this.constraints_.rotation(this.targetRotation_); @@ -1206,6 +1223,7 @@ class View extends BaseObject { if (this.getResolution() !== newResolution || this.getRotation() !== newRotation || + !this.getCenter() || !equals(this.getCenter(), newCenter)) { if (this.getAnimating()) { @@ -1221,7 +1239,7 @@ class View extends BaseObject { }); } } - + /** * Notify the View that an interaction has started. * @api @@ -1291,7 +1309,8 @@ function animationCallback(callback, returnValue) { */ export function createCenterConstraint(options) { if (options.extent !== undefined) { - return createExtent(options.extent, options.constrainOnlyCenter); + return createExtent(options.extent, options.constrainOnlyCenter, + options.smoothExtentConstraint !== undefined ? options.smoothExtentConstraint : true); } else { return centerNone; } diff --git a/src/ol/centerconstraint.js b/src/ol/centerconstraint.js index eda8973ddd..cb57c34cec 100644 --- a/src/ol/centerconstraint.js +++ b/src/ol/centerconstraint.js @@ -12,9 +12,11 @@ import {clamp} from './math.js'; /** * @param {import("./extent.js").Extent} extent Extent. * @param {boolean} onlyCenter If true, the constraint will only apply to the view center. + * @param {boolean} smooth If true, the view will be able to go slightly out of the given extent + * (only during interaction and animation). * @return {Type} The constraint. */ -export function createExtent(extent, onlyCenter) { +export function createExtent(extent, onlyCenter, smooth) { return ( /** * @param {import("./coordinate.js").Coordinate|undefined} center Center. @@ -27,11 +29,23 @@ export function createExtent(extent, onlyCenter) { if (center) { let viewWidth = onlyCenter ? 0 : size[0] * resolution; let viewHeight = onlyCenter ? 0 : size[1] * resolution; + const minX = extent[0] + viewWidth / 2; + 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; - return [ - clamp(center[0], extent[0] + viewWidth / 2, extent[2] - viewWidth / 2), - clamp(center[1], extent[1] + viewHeight / 2, extent[3] - viewHeight / 2) - ]; + // during an interaction, allow some overscroll + if (opt_isMoving && smooth) { + x += -ratio * Math.log(1 + Math.max(0, minX - center[0]) / ratio) + + ratio * Math.log(1 + Math.max(0, center[0] - maxX) / ratio); + y += -ratio * Math.log(1 + Math.max(0, minY - center[1]) / ratio) + + ratio * Math.log(1 + Math.max(0, center[1] - maxY) / ratio); + } + + return [x, y]; } else { return undefined; } diff --git a/src/ol/interaction/DragPan.js b/src/ol/interaction/DragPan.js index 6f18683a40..6bf2fb820f 100644 --- a/src/ol/interaction/DragPan.js +++ b/src/ol/interaction/DragPan.js @@ -81,15 +81,15 @@ class DragPan extends PointerInteraction { this.kinetic_.update(centroid[0], centroid[1]); } if (this.lastCentroid) { - const deltaX = this.lastCentroid[0] - centroid[0]; - const deltaY = centroid[1] - this.lastCentroid[1]; + const delta = [ + this.lastCentroid[0] - centroid[0], + centroid[1] - this.lastCentroid[1] + ]; const map = mapBrowserEvent.map; const view = map.getView(); - let center = [deltaX, deltaY]; - scaleCoordinate(center, view.getResolution()); - rotateCoordinate(center, view.getRotation()); - addCoordinate(center, view.getCenter()); - view.setCenter(center); + scaleCoordinate(delta, view.getResolution()); + rotateCoordinate(delta, view.getRotation()); + view.adjustCenter(delta); } } else if (this.kinetic_) { // reset so we don't overestimate the kinetic energy after diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index fcc5cd64d8..796e320386 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -42,7 +42,7 @@ describe('ol.View', function() { }); }); - describe('with extent option', function() { + describe('with extent option and center only', function() { it('gives a correct center constraint function', function() { const options = { extent: [0, 0, 1, 1], @@ -55,6 +55,26 @@ describe('ol.View', function() { }); }); + describe('with extent option', function() { + it('gives a correct center constraint function', function() { + const options = { + extent: [0, 0, 1, 1] + }; + const fn = createCenterConstraint(options); + const res = 1; + const size = [0.15, 0.1]; + expect(fn([0, 0], res, size)).to.eql([0.075, 0.05]); + expect(fn([0.5, 0.5], res, size)).to.eql([0.5, 0.5]); + expect(fn([10, 10], res, size)).to.eql([0.925, 0.95]); + + const overshootCenter = fn([10, 10], res, size, true); + expect(overshootCenter[0] > 0.925).to.eql(true); + expect(overshootCenter[1] > 0.95).to.eql(true); + expect(overshootCenter[0] < 9).to.eql(true); + expect(overshootCenter[1] < 9).to.eql(true); + }); + }); + }); describe('create resolution constraint', function() {