From 1cb934dbe3f915359aa88f326e3ef89c8f8cf289 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sat, 5 Jan 2019 23:07:28 +0100 Subject: [PATCH 01/25] View / implement intermediate values for center/rot/res The view now has targetCenter, targetRotation and targetResolution members. These hold the new values given by set* methods. The actual view parameters are then changed by calling `applyParameters_`. --- src/ol/View.js | 66 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 0b33692e0c..96d1195371 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -262,6 +262,24 @@ class View extends BaseObject { */ this.projection_ = createProjection(options.projection, 'EPSG:3857'); + /** + * @private + * @type {import("./coordinate.js").Coordinate|undefined} + */ + this.targetCenter_ = null; + + /** + * @private + * @type {number|undefined} + */ + this.targetResolution_; + + /** + * @private + * @type {number|undefined} + */ + this.targetRotation_; + this.applyOptions_(options); } @@ -275,8 +293,6 @@ class View extends BaseObject { * @type {Object} */ const properties = {}; - properties[ViewProperty.CENTER] = options.center !== undefined ? - options.center : null; const resolutionConstraintInfo = createResolutionConstraint(options); @@ -324,19 +340,21 @@ class View extends BaseObject { rotation: rotationConstraint }; + this.setRotation(options.rotation !== undefined ? options.rotation : 0); + this.setCenter(options.center !== undefined ? options.center : null); if (options.resolution !== undefined) { - properties[ViewProperty.RESOLUTION] = options.resolution; + this.setResolution(options.resolution); } else if (options.zoom !== undefined) { - properties[ViewProperty.RESOLUTION] = this.constrainResolution( - this.maxResolution_, options.zoom - this.minZoom_); + this.setResolution(this.constrainResolution( + this.maxResolution_, options.zoom - this.minZoom_)); if (this.resolutions_) { // in case map zoom is out of min/max zoom range - properties[ViewProperty.RESOLUTION] = clamp( + this.setResolution(clamp( Number(this.getResolution() || properties[ViewProperty.RESOLUTION]), - this.minResolution_, this.maxResolution_); + this.minResolution_, this.maxResolution_)); } } - properties[ViewProperty.ROTATION] = options.rotation !== undefined ? options.rotation : 0; + this.setProperties(properties); /** @@ -1124,10 +1142,8 @@ class View extends BaseObject { * @api */ setCenter(center) { - this.set(ViewProperty.CENTER, center); - if (this.getAnimating()) { - this.cancelAnimations(); - } + this.targetCenter_ = center; + this.applyParameters_(); } /** @@ -1148,10 +1164,8 @@ class View extends BaseObject { * @api */ setResolution(resolution) { - this.set(ViewProperty.RESOLUTION, resolution); - if (this.getAnimating()) { - this.cancelAnimations(); - } + this.targetResolution_ = resolution; + this.applyParameters_(); } /** @@ -1161,10 +1175,8 @@ class View extends BaseObject { * @api */ setRotation(rotation) { - this.set(ViewProperty.ROTATION, rotation); - if (this.getAnimating()) { - this.cancelAnimations(); - } + this.targetRotation_ = rotation; + this.applyParameters_(); } /** @@ -1175,6 +1187,20 @@ class View extends BaseObject { setZoom(zoom) { this.setResolution(this.getResolutionForZoom(zoom)); } + + /** + * Recompute rotation/resolution/center based on target values. + * @private + */ + applyParameters_() { + this.set(ViewProperty.ROTATION, this.targetRotation_); + this.set(ViewProperty.RESOLUTION, this.targetResolution_); + this.set(ViewProperty.CENTER, this.targetCenter_); + + if (this.getAnimating()) { + this.cancelAnimations(); + } + } } From 4e1ece16edf8b7121e75d815d9e759b1e6eaee5d Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 6 Jan 2019 10:46:52 +0100 Subject: [PATCH 02/25] View / implemented begin- and endInteraction methods --- src/ol/View.js | 16 ++++++++++++++++ src/ol/control/ZoomSlider.js | 4 ++-- src/ol/interaction/DragPan.js | 4 ++-- src/ol/interaction/DragRotate.js | 4 ++-- src/ol/interaction/DragRotateAndZoom.js | 4 ++-- src/ol/interaction/MouseWheelZoom.js | 4 ++-- src/ol/interaction/PinchRotate.js | 4 ++-- src/ol/interaction/PinchZoom.js | 4 ++-- test/spec/ol/view.test.js | 17 +++++++++++++++++ 9 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 96d1195371..81c5aa07e5 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -1201,6 +1201,22 @@ class View extends BaseObject { this.cancelAnimations(); } } + + /** + * Notify the View that an interaction has started. + * @api + */ + beginInteraction() { + this.setHint(ViewHint.INTERACTING, 1); + } + + /** + * Notify the View that an interaction has ended. + * @api + */ + endInteraction() { + this.setHint(ViewHint.INTERACTING, -1); + } } diff --git a/src/ol/control/ZoomSlider.js b/src/ol/control/ZoomSlider.js index fa0b3560cb..d46ae6eff9 100644 --- a/src/ol/control/ZoomSlider.js +++ b/src/ol/control/ZoomSlider.js @@ -233,7 +233,7 @@ class ZoomSlider extends Control { */ handleDraggerStart_(event) { if (!this.dragging_ && event.originalEvent.target === this.element.firstElementChild) { - this.getMap().getView().setHint(ViewHint.INTERACTING, 1); + this.getMap().getView().beginInteraction(); this.previousX_ = event.clientX; this.previousY_ = event.clientY; this.dragging_ = true; @@ -279,7 +279,7 @@ class ZoomSlider extends Control { handleDraggerEnd_(event) { if (this.dragging_) { const view = this.getMap().getView(); - view.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(); view.animate({ resolution: view.constrainResolution(this.currentResolution_), diff --git a/src/ol/interaction/DragPan.js b/src/ol/interaction/DragPan.js index d889830f33..80d9905562 100644 --- a/src/ol/interaction/DragPan.js +++ b/src/ol/interaction/DragPan.js @@ -76,7 +76,7 @@ class DragPan extends PointerInteraction { handleDragEvent(mapBrowserEvent) { if (!this.panning_) { this.panning_ = true; - this.getMap().getView().setHint(ViewHint.INTERACTING, 1); + this.getMap().getView().beginInteraction(); } const targetPointers = this.targetPointers; const centroid = centroidFromPointers(targetPointers); @@ -129,7 +129,7 @@ class DragPan extends PointerInteraction { } if (this.panning_) { this.panning_ = false; - view.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(); } return false; } else { diff --git a/src/ol/interaction/DragRotate.js b/src/ol/interaction/DragRotate.js index 68995ccf08..bc10813b08 100644 --- a/src/ol/interaction/DragRotate.js +++ b/src/ol/interaction/DragRotate.js @@ -97,7 +97,7 @@ class DragRotate extends PointerInteraction { const map = mapBrowserEvent.map; const view = map.getView(); - view.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(); const rotation = view.getRotation(); rotate(view, rotation, undefined, this.duration_); return false; @@ -114,7 +114,7 @@ class DragRotate extends PointerInteraction { if (mouseActionButton(mapBrowserEvent) && this.condition_(mapBrowserEvent)) { const map = mapBrowserEvent.map; - map.getView().setHint(ViewHint.INTERACTING, 1); + map.getView().beginInteraction(); this.lastAngle_ = undefined; return true; } else { diff --git a/src/ol/interaction/DragRotateAndZoom.js b/src/ol/interaction/DragRotateAndZoom.js index cf04606327..328e135d4b 100644 --- a/src/ol/interaction/DragRotateAndZoom.js +++ b/src/ol/interaction/DragRotateAndZoom.js @@ -113,7 +113,7 @@ class DragRotateAndZoom extends PointerInteraction { const map = mapBrowserEvent.map; const view = map.getView(); - view.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(); const direction = this.lastScaleDelta_ - 1; rotate(view, view.getRotation()); zoom(view, view.getResolution(), undefined, this.duration_, direction); @@ -130,7 +130,7 @@ class DragRotateAndZoom extends PointerInteraction { } if (this.condition_(mapBrowserEvent)) { - mapBrowserEvent.map.getView().setHint(ViewHint.INTERACTING, 1); + mapBrowserEvent.map.getView().beginInteraction(); this.lastAngle_ = undefined; this.lastMagnitude_ = undefined; return true; diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 726ee1ba62..e9fb98c8af 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -152,7 +152,7 @@ class MouseWheelZoom extends Interaction { decrementInteractingHint_() { this.trackpadTimeoutId_ = undefined; const view = this.getMap().getView(); - view.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(); } /** @@ -218,7 +218,7 @@ class MouseWheelZoom extends Interaction { if (this.trackpadTimeoutId_) { clearTimeout(this.trackpadTimeoutId_); } else { - view.setHint(ViewHint.INTERACTING, 1); + view.beginInteraction(); } this.trackpadTimeoutId_ = setTimeout(this.decrementInteractingHint_.bind(this), this.trackpadEventGap_); let resolution = view.getResolution() * Math.pow(2, delta / this.trackpadDeltaPerZoom_); diff --git a/src/ol/interaction/PinchRotate.js b/src/ol/interaction/PinchRotate.js index 82f8c254e5..6ea8de2ebe 100644 --- a/src/ol/interaction/PinchRotate.js +++ b/src/ol/interaction/PinchRotate.js @@ -131,7 +131,7 @@ class PinchRotate extends PointerInteraction { if (this.targetPointers.length < 2) { const map = mapBrowserEvent.map; const view = map.getView(); - view.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(); if (this.rotating_) { const rotation = view.getRotation(); rotate(view, rotation, this.anchor_, this.duration_); @@ -153,7 +153,7 @@ class PinchRotate extends PointerInteraction { this.rotating_ = false; this.rotationDelta_ = 0.0; if (!this.handlingDownUpSequence) { - map.getView().setHint(ViewHint.INTERACTING, 1); + map.getView().beginInteraction(); } return true; } else { diff --git a/src/ol/interaction/PinchZoom.js b/src/ol/interaction/PinchZoom.js index ed9446e318..b6ced12d60 100644 --- a/src/ol/interaction/PinchZoom.js +++ b/src/ol/interaction/PinchZoom.js @@ -126,7 +126,7 @@ class PinchZoom extends PointerInteraction { if (this.targetPointers.length < 2) { const map = mapBrowserEvent.map; const view = map.getView(); - view.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(); const resolution = view.getResolution(); if (this.constrainResolution_ || resolution < view.getMinResolution() || @@ -153,7 +153,7 @@ class PinchZoom extends PointerInteraction { this.lastDistance_ = undefined; this.lastScaleDelta_ = 1; if (!this.handlingDownUpSequence) { - map.getView().setHint(ViewHint.INTERACTING, 1); + map.getView().beginInteraction(); } return true; } else { diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 321c69667c..fed967810b 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -1422,6 +1422,23 @@ describe('ol.View', function() { expect(view.getCenter()[1]).to.roughlyEqual(46000, 1e-9); }); }); + + describe('#beginInteraction() and endInteraction()', function() { + let view; + beforeEach(function() { + view = new View() + }); + + it('correctly changes the view hint', function() { + view.beginInteraction(); + expect(view.getHints()[1]).to.be(1); + view.beginInteraction(); + expect(view.getHints()[1]).to.be(2); + view.endInteraction(); + view.endInteraction(); + expect(view.getHints()[1]).to.be(0); + }); + }); }); describe('ol.View.isNoopAnimation()', function() { From 3c1e3779e2ec789b48f134d0e5f75d507b8581e9 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 6 Jan 2019 14:25:00 +0100 Subject: [PATCH 03/25] View / add a method to compute a valid zoom level The `getValidZoomLevel` apply the current resolution constraint to return a value that is guaranteed valid. This is used for interactions & controls which need a target value to work: the +/- buttons, the zoom clider, the dragbox zoom and the mouse wheel zoom. --- src/ol/View.js | 17 +++++++++++ src/ol/control/Zoom.js | 10 +++---- src/ol/control/ZoomSlider.js | 8 ++++-- src/ol/interaction/DragZoom.js | 6 ++-- src/ol/interaction/Interaction.js | 43 +++++++++++++++++++--------- src/ol/interaction/MouseWheelZoom.js | 4 ++- test/spec/ol/view.test.js | 29 +++++++++++++++++++ 7 files changed, 92 insertions(+), 25 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 81c5aa07e5..e114b8e715 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -1217,6 +1217,23 @@ class View extends BaseObject { endInteraction() { this.setHint(ViewHint.INTERACTING, -1); } + + /** + * Get a valid zoom level according to the current view constraints. + * @param {number|undefined} targetZoom Target resolution. + * @param {number=} opt_direction Direction. Default is `0`. Specify `-1` or `1` to return + * the available value respectively lower or greater than the target one. Leaving `0` will simply choose + * the nearest available value. + * @return {number|undefined} Valid zoom level. + * @api + */ + getValidZoomLevel(targetZoom, opt_direction) { + const direction = opt_direction || 0; + const currentRes = this.getResolution(); + const currentZoom = this.getZoom(); + return this.getZoomForResolution( + this.constraints_.resolution(currentRes, targetZoom - currentZoom, direction)); + } } diff --git a/src/ol/control/Zoom.js b/src/ol/control/Zoom.js index 5e016950a9..f84254f904 100644 --- a/src/ol/control/Zoom.js +++ b/src/ol/control/Zoom.js @@ -114,20 +114,20 @@ class Zoom extends Control { // upon it return; } - const currentResolution = view.getResolution(); - if (currentResolution) { - const newResolution = view.constrainResolution(currentResolution, delta); + const currentZoom = view.getZoom(); + if (currentZoom !== undefined) { + const newZoom = view.getValidZoomLevel(currentZoom + delta); if (this.duration_ > 0) { if (view.getAnimating()) { view.cancelAnimations(); } view.animate({ - resolution: newResolution, + zoom: newZoom, duration: this.duration_, easing: easeOut }); } else { - view.setResolution(newResolution); + view.setZoom(newZoom); } } } diff --git a/src/ol/control/ZoomSlider.js b/src/ol/control/ZoomSlider.js index d46ae6eff9..124cf2130a 100644 --- a/src/ol/control/ZoomSlider.js +++ b/src/ol/control/ZoomSlider.js @@ -218,9 +218,10 @@ class ZoomSlider extends Control { event.offsetY - this.thumbSize_[1] / 2); const resolution = this.getResolutionForPosition_(relativePosition); + const zoom = view.getValidZoomLevel(view.getZoomForResolution(resolution)); view.animate({ - resolution: view.constrainResolution(resolution), + zoom: zoom, duration: this.duration_, easing: easeOut }); @@ -281,8 +282,11 @@ class ZoomSlider extends Control { const view = this.getMap().getView(); view.endInteraction(); + const zoom = view.getValidZoomLevel( + view.getZoomForResolution(this.currentResolution_)); + view.animate({ - resolution: view.constrainResolution(this.currentResolution_), + zoom: zoom, duration: this.duration_, easing: easeOut }); diff --git a/src/ol/interaction/DragZoom.js b/src/ol/interaction/DragZoom.js index 83b70c5b47..406e9c8419 100644 --- a/src/ol/interaction/DragZoom.js +++ b/src/ol/interaction/DragZoom.js @@ -80,14 +80,14 @@ function onBoxEnd() { extent = mapExtent; } - const resolution = view.constrainResolution( - view.getResolutionForExtent(extent, size)); + const resolution = view.getResolutionForExtent(extent, size); + const zoom = view.getValidZoomLevel(view.getZoomForResolution(resolution)); let center = getCenter(extent); center = view.constrainCenter(center); view.animate({ - resolution: resolution, + zoom: zoom, center: center, duration: this.duration_, easing: easeOut diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index 6a13a03857..09eb70a397 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -189,34 +189,49 @@ export function zoom(view, resolution, opt_anchor, opt_duration, opt_direction) * @param {number=} opt_duration Duration. */ export function zoomByDelta(view, delta, opt_anchor, opt_duration) { + const currentZoom = view.getZoom(); const currentResolution = view.getResolution(); - let resolution = view.constrainResolution(currentResolution, delta, 0); - if (resolution !== undefined) { - const resolutions = view.getResolutions(); - resolution = clamp( - resolution, - view.getMinResolution() || resolutions[resolutions.length - 1], - view.getMaxResolution() || resolutions[0]); + if (currentZoom === undefined) { + return; } + const newZoom = view.getValidZoomLevel(currentZoom + delta); + const newResolution = view.getResolutionForZoom(newZoom); + // If we have a constraint on center, we need to change the anchor so that the // new center is within the extent. We first calculate the new center, apply // the constraint to it, and then calculate back the anchor - if (opt_anchor && resolution !== undefined && resolution !== currentResolution) { + if (opt_anchor) { const currentCenter = view.getCenter(); - let center = view.calculateCenterZoom(resolution, opt_anchor); + let center = view.calculateCenterZoom(newResolution, opt_anchor); center = view.constrainCenter(center); opt_anchor = [ - (resolution * currentCenter[0] - currentResolution * center[0]) / - (resolution - currentResolution), - (resolution * currentCenter[1] - currentResolution * center[1]) / - (resolution - currentResolution) + (newResolution * currentCenter[0] - currentResolution * center[0]) / + (newResolution - currentResolution), + (newResolution * currentCenter[1] - currentResolution * center[1]) / + (newResolution - currentResolution) ]; } - zoomWithoutConstraints(view, resolution, opt_anchor, opt_duration); + if (opt_duration > 0) { + if (view.getAnimating()) { + view.cancelAnimations(); + } + view.animate({ + resolution: newResolution, + anchor: opt_anchor, + duration: opt_duration, + easing: easeOut + }); + } else { + if (opt_anchor) { + const center = view.calculateCenterZoom(newResolution, opt_anchor); + view.setCenter(center); + } + view.setResolution(newResolution); + } } diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index e9fb98c8af..24d29fd099 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -239,8 +239,10 @@ class MouseWheelZoom extends Interaction { view.setResolution(resolution); if (rebound === 0 && this.constrainResolution_) { + const zoomDelta = delta > 0 ? -1 : 1; + const newZoom = view.getValidZoomLevel(view.getZoom() + zoomDelta); view.animate({ - resolution: view.constrainResolution(resolution, delta > 0 ? -1 : 1), + resolution: view.getResolutionForZoom(newZoom), easing: easeOut, anchor: this.lastAnchor_, duration: this.duration_ diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index fed967810b..926748e6fa 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -1439,6 +1439,35 @@ describe('ol.View', function() { expect(view.getHints()[1]).to.be(0); }); }); + + describe('#getValidZoomLevel()', function() { + let view; + + it('works correctly without constraint', function() { + view = new View({ + zoom: 0 + }); + expect(view.getValidZoomLevel(3)).to.be(3); + }); + it('works correctly with resolution constraints', function() { + view = new View({ + zoom: 4, + minZoom: 4, + maxZoom: 8 + }); + expect(view.getValidZoomLevel(3)).to.be(4); + expect(view.getValidZoomLevel(10)).to.be(8); + }); + it('works correctly with a specific resolution set', function() { + view = new View({ + zoom: 0, + resolutions: [512, 256, 128, 64, 32, 16, 8] + }); + expect(view.getValidZoomLevel(0)).to.be(0); + expect(view.getValidZoomLevel(4)).to.be(4); + expect(view.getValidZoomLevel(8)).to.be(6); + }); + }); }); describe('ol.View.isNoopAnimation()', function() { From e6c4b2ffd17098b04146be44e279759e3ae3c2d9 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 10 Jan 2019 17:38:37 +0100 Subject: [PATCH 04/25] View / make the `constrainResolution` function private Other classes should not need to worry about constraining the resolution or not, as the View will eventually do this on its own. --- src/ol/View.js | 9 ++--- src/ol/interaction/DragRotateAndZoom.js | 4 +- src/ol/interaction/Interaction.js | 50 ++++++++++--------------- src/ol/interaction/PinchZoom.js | 4 +- 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index e114b8e715..ebb4e78395 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -345,13 +345,12 @@ class View extends BaseObject { if (options.resolution !== undefined) { this.setResolution(options.resolution); } else if (options.zoom !== undefined) { - this.setResolution(this.constrainResolution( - this.maxResolution_, options.zoom - this.minZoom_)); - if (this.resolutions_) { // in case map zoom is out of min/max zoom range - this.setResolution(clamp( - Number(this.getResolution() || properties[ViewProperty.RESOLUTION]), + const resolution = this.getResolutionForZoom(options.zoom); + this.setResolution(clamp(resolution, this.minResolution_, this.maxResolution_)); + } else { + this.setZoom(options.zoom); } } diff --git a/src/ol/interaction/DragRotateAndZoom.js b/src/ol/interaction/DragRotateAndZoom.js index 328e135d4b..5225d73001 100644 --- a/src/ol/interaction/DragRotateAndZoom.js +++ b/src/ol/interaction/DragRotateAndZoom.js @@ -4,7 +4,7 @@ import {disable} from '../rotationconstraint.js'; import ViewHint from '../ViewHint.js'; import {shiftKeyOnly, mouseOnly} from '../events/condition.js'; -import {rotate, rotateWithoutConstraints, zoom, zoomWithoutConstraints} from './Interaction.js'; +import {rotate, rotateWithoutConstraints, zoom} from './Interaction.js'; import PointerInteraction from './Pointer.js'; @@ -95,7 +95,7 @@ class DragRotateAndZoom extends PointerInteraction { this.lastAngle_ = theta; if (this.lastMagnitude_ !== undefined) { const resolution = this.lastMagnitude_ * (view.getResolution() / magnitude); - zoomWithoutConstraints(view, resolution); + zoom(view, resolution); } if (this.lastMagnitude_ !== undefined) { this.lastScaleDelta_ = this.lastMagnitude_ / magnitude; diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index 09eb70a397..9a7b817525 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -177,8 +177,25 @@ export function rotateWithoutConstraints(view, rotation, opt_anchor, opt_duratio * assumed. */ export function zoom(view, resolution, opt_anchor, opt_duration, opt_direction) { - resolution = view.constrainResolution(resolution, 0, opt_direction); - zoomWithoutConstraints(view, resolution, opt_anchor, opt_duration); + if (resolution) { + const currentResolution = view.getResolution(); + const currentCenter = view.getCenter(); + if (currentResolution !== undefined && currentCenter && + resolution !== currentResolution && opt_duration) { + view.animate({ + resolution: resolution, + anchor: opt_anchor, + duration: opt_duration, + easing: easeOut + }); + } else { + if (opt_anchor) { + const center = view.calculateCenterZoom(resolution, opt_anchor); + view.setCenter(center); + } + view.setResolution(resolution); + } + } } @@ -234,33 +251,4 @@ export function zoomByDelta(view, delta, opt_anchor, opt_duration) { } } - -/** - * @param {import("../View.js").default} view View. - * @param {number|undefined} resolution Resolution to go to. - * @param {import("../coordinate.js").Coordinate=} opt_anchor Anchor coordinate. - * @param {number=} opt_duration Duration. - */ -export function zoomWithoutConstraints(view, resolution, opt_anchor, opt_duration) { - if (resolution) { - const currentResolution = view.getResolution(); - const currentCenter = view.getCenter(); - if (currentResolution !== undefined && currentCenter && - resolution !== currentResolution && opt_duration) { - view.animate({ - resolution: resolution, - anchor: opt_anchor, - duration: opt_duration, - easing: easeOut - }); - } else { - if (opt_anchor) { - const center = view.calculateCenterZoom(resolution, opt_anchor); - view.setCenter(center); - } - view.setResolution(resolution); - } - } -} - export default Interaction; diff --git a/src/ol/interaction/PinchZoom.js b/src/ol/interaction/PinchZoom.js index b6ced12d60..885e69b492 100644 --- a/src/ol/interaction/PinchZoom.js +++ b/src/ol/interaction/PinchZoom.js @@ -3,7 +3,7 @@ */ import ViewHint from '../ViewHint.js'; import {FALSE} from '../functions.js'; -import {zoom, zoomWithoutConstraints} from './Interaction.js'; +import {zoom} from './Interaction.js'; import PointerInteraction, {centroid as centroidFromPointers} from './Pointer.js'; @@ -116,7 +116,7 @@ class PinchZoom extends PointerInteraction { // scale, bypass the resolution constraint map.render(); - zoomWithoutConstraints(view, newResolution, this.anchor_); + zoom(view, newResolution, this.anchor_); } /** From c2c1aa01d35443e7899c5e4ed3ce380f8159d71e Mon Sep 17 00:00:00 2001 From: jahow Date: Sat, 12 Jan 2019 15:41:20 +0100 Subject: [PATCH 05/25] View / removed the constrainRotation method --- src/ol/View.js | 14 +------------- src/ol/interaction/DragRotate.js | 4 ++-- src/ol/interaction/DragRotateAndZoom.js | 4 ++-- src/ol/interaction/Interaction.js | 12 ------------ src/ol/interaction/PinchRotate.js | 4 ++-- 5 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index ebb4e78395..16091a3a04 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -681,7 +681,7 @@ class View extends BaseObject { * @param {number=} opt_delta Delta. Default is `0`. * @param {number=} opt_direction Direction. Default is `0`. * @return {number|undefined} Constrained resolution. - * @api + * @private */ constrainResolution(resolution, opt_delta, opt_direction) { const delta = opt_delta || 0; @@ -689,18 +689,6 @@ class View extends BaseObject { return this.constraints_.resolution(resolution, delta, direction); } - /** - * Get the constrained rotation of this view. - * @param {number|undefined} rotation Rotation. - * @param {number=} opt_delta Delta. Default is `0`. - * @return {number|undefined} Constrained rotation. - * @api - */ - constrainRotation(rotation, opt_delta) { - const delta = opt_delta || 0; - return this.constraints_.rotation(rotation, delta); - } - /** * Get the view center. * @return {import("./coordinate.js").Coordinate|undefined} The center of the view. diff --git a/src/ol/interaction/DragRotate.js b/src/ol/interaction/DragRotate.js index bc10813b08..09143c0d79 100644 --- a/src/ol/interaction/DragRotate.js +++ b/src/ol/interaction/DragRotate.js @@ -5,7 +5,7 @@ import {disable} from '../rotationconstraint.js'; import ViewHint from '../ViewHint.js'; import {altShiftKeysOnly, mouseOnly, mouseActionButton} from '../events/condition.js'; import {FALSE} from '../functions.js'; -import {rotate, rotateWithoutConstraints} from './Interaction.js'; +import {rotate} from './Interaction.js'; import PointerInteraction from './Pointer.js'; @@ -81,7 +81,7 @@ class DragRotate extends PointerInteraction { if (this.lastAngle_ !== undefined) { const delta = theta - this.lastAngle_; const rotation = view.getRotation(); - rotateWithoutConstraints(view, rotation - delta); + rotate(view, rotation - delta); } this.lastAngle_ = theta; } diff --git a/src/ol/interaction/DragRotateAndZoom.js b/src/ol/interaction/DragRotateAndZoom.js index 5225d73001..0bd024ed67 100644 --- a/src/ol/interaction/DragRotateAndZoom.js +++ b/src/ol/interaction/DragRotateAndZoom.js @@ -4,7 +4,7 @@ import {disable} from '../rotationconstraint.js'; import ViewHint from '../ViewHint.js'; import {shiftKeyOnly, mouseOnly} from '../events/condition.js'; -import {rotate, rotateWithoutConstraints, zoom} from './Interaction.js'; +import {rotate, zoom} from './Interaction.js'; import PointerInteraction from './Pointer.js'; @@ -90,7 +90,7 @@ class DragRotateAndZoom extends PointerInteraction { const view = map.getView(); if (view.getConstraints().rotation !== disable && this.lastAngle_ !== undefined) { const angleDelta = theta - this.lastAngle_; - rotateWithoutConstraints(view, view.getRotation() - angleDelta); + rotate(view, view.getRotation() - angleDelta); } this.lastAngle_ = theta; if (this.lastMagnitude_ !== undefined) { diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index 9a7b817525..42ec35deb8 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -133,18 +133,6 @@ export function pan(view, delta, opt_duration) { * @param {number=} opt_duration Duration. */ export function rotate(view, rotation, opt_anchor, opt_duration) { - rotation = view.constrainRotation(rotation, 0); - rotateWithoutConstraints(view, rotation, opt_anchor, opt_duration); -} - - -/** - * @param {import("../View.js").default} view View. - * @param {number|undefined} rotation Rotation. - * @param {import("../coordinate.js").Coordinate=} opt_anchor Anchor coordinate. - * @param {number=} opt_duration Duration. - */ -export function rotateWithoutConstraints(view, rotation, opt_anchor, opt_duration) { if (rotation !== undefined) { const currentRotation = view.getRotation(); const currentCenter = view.getCenter(); diff --git a/src/ol/interaction/PinchRotate.js b/src/ol/interaction/PinchRotate.js index 6ea8de2ebe..fc40d7f851 100644 --- a/src/ol/interaction/PinchRotate.js +++ b/src/ol/interaction/PinchRotate.js @@ -3,7 +3,7 @@ */ import ViewHint from '../ViewHint.js'; import {FALSE} from '../functions.js'; -import {rotate, rotateWithoutConstraints} from './Interaction.js'; +import {rotate} from './Interaction.js'; import PointerInteraction, {centroid as centroidFromPointers} from './Pointer.js'; import {disable} from '../rotationconstraint.js'; @@ -120,7 +120,7 @@ class PinchRotate extends PointerInteraction { if (this.rotating_) { const rotation = view.getRotation(); map.render(); - rotateWithoutConstraints(view, rotation + rotationDelta, this.anchor_); + rotate(view, rotation + rotationDelta, this.anchor_); } } From d991dfa54af0a73ecf3b61507084079639ee6112 Mon Sep 17 00:00:00 2001 From: jahow Date: Sat, 12 Jan 2019 15:49:36 +0100 Subject: [PATCH 06/25] View / remove constrainCenter method --- src/ol/View.js | 10 ---------- src/ol/interaction/DragPan.js | 3 +-- src/ol/interaction/DragZoom.js | 3 +-- src/ol/interaction/Interaction.js | 6 ++---- src/ol/interaction/MouseWheelZoom.js | 2 +- 5 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 16091a3a04..d697dc66bc 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -665,16 +665,6 @@ class View extends BaseObject { return size; } - /** - * Get the constrained center of this view. - * @param {import("./coordinate.js").Coordinate|undefined} center Center. - * @return {import("./coordinate.js").Coordinate|undefined} Constrained center. - * @api - */ - constrainCenter(center) { - return this.constraints_.center(center); - } - /** * Get the constrained resolution of this view. * @param {number|undefined} resolution Resolution. diff --git a/src/ol/interaction/DragPan.js b/src/ol/interaction/DragPan.js index 80d9905562..020eb509ef 100644 --- a/src/ol/interaction/DragPan.js +++ b/src/ol/interaction/DragPan.js @@ -93,7 +93,6 @@ class DragPan extends PointerInteraction { scaleCoordinate(center, view.getResolution()); rotateCoordinate(center, view.getRotation()); addCoordinate(center, view.getCenter()); - center = view.constrainCenter(center); view.setCenter(center); } } else if (this.kinetic_) { @@ -122,7 +121,7 @@ class DragPan extends PointerInteraction { centerpx[1] - distance * Math.sin(angle) ]); view.animate({ - center: view.constrainCenter(dest), + center: dest, duration: 500, easing: easeOut }); diff --git a/src/ol/interaction/DragZoom.js b/src/ol/interaction/DragZoom.js index 406e9c8419..393db36660 100644 --- a/src/ol/interaction/DragZoom.js +++ b/src/ol/interaction/DragZoom.js @@ -83,8 +83,7 @@ function onBoxEnd() { const resolution = view.getResolutionForExtent(extent, size); const zoom = view.getValidZoomLevel(view.getZoomForResolution(resolution)); - let center = getCenter(extent); - center = view.constrainCenter(center); + const center = getCenter(extent); view.animate({ zoom: zoom, diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index 42ec35deb8..405e986891 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -111,8 +111,7 @@ class Interaction extends BaseObject { export function pan(view, delta, opt_duration) { const currentCenter = view.getCenter(); if (currentCenter) { - const center = view.constrainCenter( - [currentCenter[0] + delta[0], currentCenter[1] + delta[1]]); + const center = [currentCenter[0] + delta[0], currentCenter[1] + delta[1]]; if (opt_duration) { view.animate({ duration: opt_duration, @@ -209,8 +208,7 @@ export function zoomByDelta(view, delta, opt_anchor, opt_duration) { // the constraint to it, and then calculate back the anchor if (opt_anchor) { const currentCenter = view.getCenter(); - let center = view.calculateCenterZoom(newResolution, opt_anchor); - center = view.constrainCenter(center); + const center = view.calculateCenterZoom(newResolution, opt_anchor); opt_anchor = [ (newResolution * currentCenter[0] - currentResolution * center[0]) / diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 24d29fd099..120d4e42c0 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -234,7 +234,7 @@ class MouseWheelZoom extends Interaction { } if (this.lastAnchor_) { const center = view.calculateCenterZoom(resolution, this.lastAnchor_); - view.setCenter(view.constrainCenter(center)); + view.setCenter(center); } view.setResolution(resolution); From e52fab636c13c8da6937abb4fcb7814da890d0b9 Mon Sep 17 00:00:00 2001 From: jahow Date: Sat, 12 Jan 2019 23:47:02 +0100 Subject: [PATCH 07/25] View / apply constraints automatically based on hints All constraints can now function differently if they are applied during interaction or animation. --- src/ol/View.js | 51 +++++++++++++++++++--------------- src/ol/centerconstraint.js | 2 +- src/ol/resolutionconstraint.js | 16 +++++++++-- src/ol/rotationconstraint.js | 12 ++++++-- test/spec/ol/view.test.js | 1 + 5 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index d697dc66bc..ee0cbb1eb4 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -449,9 +449,9 @@ class View extends BaseObject { return; } let start = Date.now(); - let center = this.getCenter().slice(); - let resolution = this.getResolution(); - let rotation = this.getRotation(); + let center = this.targetCenter_.slice(); + let resolution = this.targetResolution_; + let rotation = this.targetRotation_; const series = []; for (let i = 0; i < animationCount; ++i) { const options = /** @type {AnimationOptions} */ (arguments[i]); @@ -573,28 +573,27 @@ class View extends BaseObject { const y1 = animation.targetCenter[1]; const x = x0 + progress * (x1 - x0); const y = y0 + progress * (y1 - y0); - this.set(ViewProperty.CENTER, [x, y]); + this.targetCenter_ = [x, y]; } if (animation.sourceResolution && animation.targetResolution) { const resolution = progress === 1 ? animation.targetResolution : animation.sourceResolution + progress * (animation.targetResolution - animation.sourceResolution); if (animation.anchor) { - this.set(ViewProperty.CENTER, - this.calculateCenterZoom(resolution, animation.anchor)); + this.targetCenter_ = this.calculateCenterZoom(resolution, animation.anchor); } - this.set(ViewProperty.RESOLUTION, resolution); + this.targetResolution_ = resolution; } if (animation.sourceRotation !== undefined && animation.targetRotation !== undefined) { const rotation = progress === 1 ? modulo(animation.targetRotation + Math.PI, 2 * Math.PI) - Math.PI : animation.sourceRotation + progress * (animation.targetRotation - animation.sourceRotation); if (animation.anchor) { - this.set(ViewProperty.CENTER, - this.calculateCenterRotate(rotation, animation.anchor)); + this.targetCenter_ = this.calculateCenterRotate(rotation, animation.anchor); } - this.set(ViewProperty.ROTATION, rotation); + this.targetRotation_ = rotation; } + this.applyParameters_(true); more = true; if (!animation.complete) { break; @@ -623,10 +622,10 @@ class View extends BaseObject { */ calculateCenterRotate(rotation, anchor) { let center; - const currentCenter = this.getCenter(); + const currentCenter = this.targetCenter_; if (currentCenter !== undefined) { center = [currentCenter[0] - anchor[0], currentCenter[1] - anchor[1]]; - rotateCoordinate(center, rotation - this.getRotation()); + rotateCoordinate(center, rotation - this.targetRotation_); addCoordinate(center, anchor); } return center; @@ -639,8 +638,8 @@ class View extends BaseObject { */ calculateCenterZoom(resolution, anchor) { let center; - const currentCenter = this.getCenter(); - const currentResolution = this.getResolution(); + const currentCenter = this.targetCenter_; + const currentResolution = this.targetResolution_; if (currentCenter !== undefined && currentResolution !== undefined) { const x = anchor[0] - resolution * (anchor[0] - currentCenter[0]) / currentResolution; const y = anchor[1] - resolution * (anchor[1] - currentCenter[1]) / currentResolution; @@ -917,7 +916,7 @@ class View extends BaseObject { */ getZoom() { let zoom; - const resolution = this.getResolution(); + const resolution = this.targetResolution_; if (resolution !== undefined) { zoom = this.getZoomForResolution(resolution); } @@ -1059,8 +1058,9 @@ class View extends BaseObject { easing: options.easing }, callback); } else { - this.setResolution(resolution); - this.setCenter(center); + this.targetResolution_ = resolution; + this.targetCenter_ = center; + this.applyParameters_(false, true); animationCallback(callback, true); } } @@ -1167,14 +1167,21 @@ class View extends BaseObject { /** * Recompute rotation/resolution/center based on target values. + * @param {boolean=} opt_doNotCancelAnims Do not cancel animations. + * @param {boolean=} opt_forceMoving Apply constraints as if the view is moving. * @private */ - applyParameters_() { - this.set(ViewProperty.ROTATION, this.targetRotation_); - this.set(ViewProperty.RESOLUTION, this.targetResolution_); - this.set(ViewProperty.CENTER, this.targetCenter_); + applyParameters_(opt_doNotCancelAnims, opt_forceMoving) { + const isMoving = this.getAnimating() || this.getInteracting() || opt_forceMoving; + const newRotation = this.constraints_.rotation(this.targetRotation_, 0, isMoving); + const newResolution = this.constraints_.resolution(this.targetResolution_, 0, 0, isMoving); + const newCenter = this.constraints_.center(this.targetCenter_, isMoving); - if (this.getAnimating()) { + this.set(ViewProperty.ROTATION, newRotation); + this.set(ViewProperty.RESOLUTION, newResolution); + this.set(ViewProperty.CENTER, newCenter); + + if (this.getAnimating() && !opt_doNotCancelAnims) { this.cancelAnimations(); } } diff --git a/src/ol/centerconstraint.js b/src/ol/centerconstraint.js index 3d24463c8b..4c4fcfe23f 100644 --- a/src/ol/centerconstraint.js +++ b/src/ol/centerconstraint.js @@ -19,7 +19,7 @@ export function createExtent(extent) { * @param {import("./coordinate.js").Coordinate=} center Center. * @return {import("./coordinate.js").Coordinate|undefined} Center. */ - function(center) { + function(center, opt_isMoving) { if (center) { return [ clamp(center[0], extent[0], extent[2]), diff --git a/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index 8e85618832..9a7d713888 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -22,8 +22,14 @@ export function createSnapToResolutions(resolutions) { * @param {number} direction Direction. * @return {number|undefined} Resolution. */ - function(resolution, delta, direction) { + function(resolution, delta, direction, opt_isMoving) { if (resolution !== undefined) { + // during interacting or animating, allow intermediary values + if (opt_isMoving) { + // TODO: actually take delta and direction into account + return resolution; + } + let z = linearFindNearest(resolutions, resolution, direction); z = clamp(z + delta, 0, resolutions.length - 1); const index = Math.floor(z); @@ -55,8 +61,14 @@ export function createSnapToPower(power, maxResolution, opt_maxLevel) { * @param {number} direction Direction. * @return {number|undefined} Resolution. */ - function(resolution, delta, direction) { + function(resolution, delta, direction, opt_isMoving) { if (resolution !== undefined) { + // during interacting or animating, allow intermediary values + if (opt_isMoving) { + // TODO: actually take delta and direction into account + return resolution; + } + const offset = -direction / 2 + 0.5; const oldLevel = Math.floor( Math.log(maxResolution / resolution) / Math.log(power) + offset); diff --git a/src/ol/rotationconstraint.js b/src/ol/rotationconstraint.js index 6abc294e42..5183f28572 100644 --- a/src/ol/rotationconstraint.js +++ b/src/ol/rotationconstraint.js @@ -49,7 +49,11 @@ export function createSnapToN(n) { * @param {number} delta Delta. * @return {number|undefined} Rotation. */ - function(rotation, delta) { + function(rotation, delta, opt_isMoving) { + if (opt_isMoving) { + return rotation; + } + if (rotation !== undefined) { rotation = Math.floor((rotation + delta) / theta + 0.5) * theta; return rotation; @@ -72,7 +76,11 @@ export function createSnapToZero(opt_tolerance) { * @param {number} delta Delta. * @return {number|undefined} Rotation. */ - function(rotation, delta) { + function(rotation, delta, opt_isMoving) { + if (opt_isMoving) { + return rotation; + } + if (rotation !== undefined) { if (Math.abs(rotation + delta) <= tolerance) { return 0; diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 926748e6fa..453ca5d623 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -365,6 +365,7 @@ describe('ol.View', function() { it('applies the current resolution if resolution was originally supplied', function() { const view = new View({ center: [0, 0], + maxResolution: 2000, resolution: 1000 }); view.setResolution(500); From 1f379a06a4671b8aaf21ec6985240d9bac52387e Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 14 Jan 2019 13:36:20 +0100 Subject: [PATCH 08/25] View / add support for viewport extent constraint This introduces a breaking change: The `extent` view option now constrains the whole viewport and not just the view center. The option `constrainOnlyCenter` was added to keep the previous behaviour. Constraining the whole viewport and not only the view center means that the center and resolution constraints must be applied with a knowledge of the viewport size. --- examples/extent-constrained.html | 9 ++++ examples/extent-constrained.js | 25 +++++++++ src/ol/View.js | 53 ++++++++++++++++---- src/ol/centerconstraint.js | 19 ++++--- src/ol/resolutionconstraint.js | 43 ++++++++++++---- src/ol/rotationconstraint.js | 3 +- test/spec/ol/interaction/interaction.test.js | 1 + test/spec/ol/view.test.js | 3 +- 8 files changed, 128 insertions(+), 28 deletions(-) create mode 100644 examples/extent-constrained.html create mode 100644 examples/extent-constrained.js diff --git a/examples/extent-constrained.html b/examples/extent-constrained.html new file mode 100644 index 0000000000..7c228d6c1b --- /dev/null +++ b/examples/extent-constrained.html @@ -0,0 +1,9 @@ +--- +layout: example.html +title: Constrained Extent +shortdesc: Example of a view with a constrained extent. +docs: > + This map has a view that is constrained in an extent. This is done using the `extent` view option. Please note that specifying `restrictOnlyCenter: true` would only apply the extent restriction to the view center. +tags: "view, extent, constrain, restrict" +--- +
diff --git a/examples/extent-constrained.js b/examples/extent-constrained.js new file mode 100644 index 0000000000..8fc6e68c77 --- /dev/null +++ b/examples/extent-constrained.js @@ -0,0 +1,25 @@ +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import TileLayer from '../src/ol/layer/Tile.js'; +import OSM from '../src/ol/source/OSM.js'; +import {defaults as defaultControls} from '../src/ol/control/util'; +import ZoomSlider from '../src/ol/control/ZoomSlider'; + +const view = new View({ + center: [328627.563458, 5921296.662223], + zoom: 8, + extent: [-572513.341856, 5211017.966314, + 916327.095083, 6636950.728974] +}); + +new Map({ + layers: [ + new TileLayer({ + source: new OSM() + }) + ], + keyboardEventTarget: document, + target: 'map', + view: view, + controls: defaultControls().extend([new ZoomSlider()]) +}); diff --git a/src/ol/View.js b/src/ol/View.js index ee0cbb1eb4..4c03566bdc 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -92,7 +92,9 @@ import Units from './proj/Units.js'; * used. The `constrainRotation` option has no effect if `enableRotation` is * `false`. * @property {import("./extent.js").Extent} [extent] The extent that constrains the - * center, in other words, center cannot be set outside this extent. + * 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 {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 @@ -473,8 +475,7 @@ class View extends BaseObject { if (options.zoom !== undefined) { animation.sourceResolution = resolution; - animation.targetResolution = this.constrainResolution( - this.maxResolution_, options.zoom - this.minZoom_, 0); + animation.targetResolution = this.getResolutionForZoom(options.zoom); resolution = animation.targetResolution; } else if (options.resolution) { animation.sourceResolution = resolution; @@ -675,7 +676,15 @@ class View extends BaseObject { constrainResolution(resolution, opt_delta, opt_direction) { const delta = opt_delta || 0; const direction = opt_direction || 0; - return this.constraints_.resolution(resolution, delta, direction); + + const size = this.getSizeFromViewport_(); + const rotation = this.getRotation() || 0; + const rotatedSize = [ + Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), + Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) + ]; + + return this.constraints_.resolution(resolution, delta, direction, rotatedSize); } /** @@ -1167,15 +1176,28 @@ class View extends BaseObject { /** * Recompute rotation/resolution/center based on target values. + * Note: we have to compute rotation first, then resolution and center considering that + * parameters can influence one another in case a view extent constraint is present. * @param {boolean=} opt_doNotCancelAnims Do not cancel animations. * @param {boolean=} opt_forceMoving Apply constraints as if the view is moving. * @private */ applyParameters_(opt_doNotCancelAnims, opt_forceMoving) { const isMoving = this.getAnimating() || this.getInteracting() || opt_forceMoving; + + // compute rotation const newRotation = this.constraints_.rotation(this.targetRotation_, 0, isMoving); - const newResolution = this.constraints_.resolution(this.targetResolution_, 0, 0, isMoving); - const newCenter = this.constraints_.center(this.targetCenter_, isMoving); + + // compute viewport size with rotation + const size = this.getSizeFromViewport_(); + const rotation = this.getRotation() || 0; + const rotatedSize = [ + Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), + Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) + ]; + + const newResolution = this.constraints_.resolution(this.targetResolution_, 0, 0, rotatedSize, isMoving); + const newCenter = this.constraints_.center(this.targetCenter_, newResolution, rotatedSize, isMoving); this.set(ViewProperty.ROTATION, newRotation); this.set(ViewProperty.RESOLUTION, newResolution); @@ -1215,8 +1237,16 @@ class View extends BaseObject { const direction = opt_direction || 0; const currentRes = this.getResolution(); const currentZoom = this.getZoom(); + + const size = this.getSizeFromViewport_(); + const rotation = this.getRotation() || 0; + const rotatedSize = [ + Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), + Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) + ]; + return this.getZoomForResolution( - this.constraints_.resolution(currentRes, targetZoom - currentZoom, direction)); + this.constraints_.resolution(currentRes, targetZoom - currentZoom, direction, rotatedSize)); } } @@ -1238,7 +1268,7 @@ function animationCallback(callback, returnValue) { */ export function createCenterConstraint(options) { if (options.extent !== undefined) { - return createExtent(options.extent); + return createExtent(options.extent, options.constrainOnlyCenter); } else { return centerNone; } @@ -1274,8 +1304,8 @@ export function createResolutionConstraint(options) { maxResolution = resolutions[minZoom]; minResolution = resolutions[maxZoom] !== undefined ? resolutions[maxZoom] : resolutions[resolutions.length - 1]; - resolutionConstraint = createSnapToResolutions( - resolutions); + resolutionConstraint = createSnapToResolutions(resolutions, + !options.constrainOnlyCenter && options.extent); } else { // calculate the default min and max resolution const projection = createProjection(options.projection, 'EPSG:3857'); @@ -1320,7 +1350,8 @@ export function createResolutionConstraint(options) { minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom); resolutionConstraint = createSnapToPower( - zoomFactor, maxResolution, maxZoom - minZoom); + zoomFactor, maxResolution, maxZoom - minZoom, + !options.constrainOnlyCenter && options.extent); } return {constraint: resolutionConstraint, maxResolution: maxResolution, minResolution: minResolution, minZoom: minZoom, zoomFactor: zoomFactor}; diff --git a/src/ol/centerconstraint.js b/src/ol/centerconstraint.js index 4c4fcfe23f..eda8973ddd 100644 --- a/src/ol/centerconstraint.js +++ b/src/ol/centerconstraint.js @@ -5,25 +5,32 @@ import {clamp} from './math.js'; /** - * @typedef {function((import("./coordinate.js").Coordinate|undefined)): (import("./coordinate.js").Coordinate|undefined)} Type + * @typedef {function((import("./coordinate.js").Coordinate|undefined), number, import("./size.js").Size, boolean=): (import("./coordinate.js").Coordinate|undefined)} Type */ /** * @param {import("./extent.js").Extent} extent Extent. + * @param {boolean} onlyCenter If true, the constraint will only apply to the view center. * @return {Type} The constraint. */ -export function createExtent(extent) { +export function createExtent(extent, onlyCenter) { return ( /** - * @param {import("./coordinate.js").Coordinate=} center Center. + * @param {import("./coordinate.js").Coordinate|undefined} center Center. + * @param {number} resolution Resolution. + * @param {import("./size.js").Size} size Viewport size; unused if `onlyCenter` was specified. + * @param {boolean=} opt_isMoving True if an interaction or animation is in progress. * @return {import("./coordinate.js").Coordinate|undefined} Center. */ - function(center, opt_isMoving) { + function(center, resolution, size, opt_isMoving) { if (center) { + let viewWidth = onlyCenter ? 0 : size[0] * resolution; + let viewHeight = onlyCenter ? 0 : size[1] * resolution; + return [ - clamp(center[0], extent[0], extent[2]), - clamp(center[1], extent[1], extent[3]) + clamp(center[0], extent[0] + viewWidth / 2, extent[2] - viewWidth / 2), + clamp(center[1], extent[1] + viewHeight / 2, extent[3] - viewHeight / 2) ]; } else { return undefined; diff --git a/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index 9a7d713888..d007b81dc9 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -3,34 +3,47 @@ */ import {linearFindNearest} from './array.js'; import {clamp} from './math.js'; +import {getHeight, getWidth} from './extent'; /** - * @typedef {function((number|undefined), number, number): (number|undefined)} Type + * @typedef {function((number|undefined), number, number, import("./size.js").Size, boolean=): (number|undefined)} Type */ /** * @param {Array} resolutions Resolutions. + * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. * @return {Type} Zoom function. */ -export function createSnapToResolutions(resolutions) { +export function createSnapToResolutions(resolutions, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. * @param {number} delta Delta. * @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, delta, direction, opt_isMoving) { + function(resolution, delta, 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)); + } + // during interacting or animating, allow intermediary values if (opt_isMoving) { // TODO: actually take delta and direction into account - return resolution; + return Math.min(resolution, cappedRes); } - let z = linearFindNearest(resolutions, resolution, direction); + let z = linearFindNearest(resolutions, cappedRes, direction); z = clamp(z + delta, 0, resolutions.length - 1); const index = Math.floor(z); if (z != index && index < resolutions.length - 1) { @@ -51,27 +64,39 @@ export function createSnapToResolutions(resolutions) { * @param {number} power Power. * @param {number} maxResolution Maximum resolution. * @param {number=} opt_maxLevel Maximum level. + * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. * @return {Type} Zoom function. */ -export function createSnapToPower(power, maxResolution, opt_maxLevel) { +export function createSnapToPower(power, maxResolution, opt_maxLevel, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. * @param {number} delta Delta. * @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, delta, direction, opt_isMoving) { + function(resolution, delta, 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)); + } + // during interacting or animating, allow intermediary values if (opt_isMoving) { // TODO: actually take delta and direction into account - return resolution; + return Math.min(resolution, cappedRes); } const offset = -direction / 2 + 0.5; const oldLevel = Math.floor( - Math.log(maxResolution / resolution) / Math.log(power) + offset); + Math.log(maxResolution / cappedRes) / Math.log(power) + offset); let newLevel = Math.max(oldLevel + delta, 0); if (opt_maxLevel !== undefined) { newLevel = Math.min(newLevel, opt_maxLevel); diff --git a/src/ol/rotationconstraint.js b/src/ol/rotationconstraint.js index 5183f28572..1efcc7f734 100644 --- a/src/ol/rotationconstraint.js +++ b/src/ol/rotationconstraint.js @@ -5,7 +5,7 @@ import {toRadians} from './math.js'; /** - * @typedef {function((number|undefined), number): (number|undefined)} Type + * @typedef {function((number|undefined), number, boolean=): (number|undefined)} Type */ @@ -47,6 +47,7 @@ export function createSnapToN(n) { /** * @param {number|undefined} rotation Rotation. * @param {number} delta Delta. + * @param {boolean=} opt_isMoving True if an interaction or animation is in progress. * @return {number|undefined} Rotation. */ function(rotation, delta, opt_isMoving) { diff --git a/test/spec/ol/interaction/interaction.test.js b/test/spec/ol/interaction/interaction.test.js index 000a575eda..3d2d04e91a 100644 --- a/test/spec/ol/interaction/interaction.test.js +++ b/test/spec/ol/interaction/interaction.test.js @@ -132,6 +132,7 @@ describe('ol.interaction.Interaction', function() { const view = new View({ center: [0, 0], extent: [-2.5, -2.5, 2.5, 2.5], + constrainOnlyCenter: true, resolution: 1, resolutions: [4, 2, 1, 0.5, 0.25] }); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 453ca5d623..88b18e4718 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -45,7 +45,8 @@ describe('ol.View', function() { describe('with extent option', function() { it('gives a correct center constraint function', function() { const options = { - extent: [0, 0, 1, 1] + extent: [0, 0, 1, 1], + constrainOnlyCenter: true }; const fn = createCenterConstraint(options); expect(fn([0, 0])).to.eql([0, 0]); From 1c5fd62e43c5fcebc86e884dd941dd48d98d24fe Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 14 Jan 2019 16:34:29 +0100 Subject: [PATCH 09/25] View / refactor how zoom and resolution are computed This commit aims to simplify the computation of zoom and resolution in the View class. Previously zoom levels and resolution computations were mixed in different places, ie resolution constraints, initial values, etc. Now the View class only has the `getZoomForResolution` and `getResolutionForZoom` methods to convert from one system to another. Other than that, most computations use the resolution system internally. The `constrainResolution` method also does not exist anymore, and is replaced by `getValidResolution` and `getValidZoomLevel` public methods. --- src/ol/View.js | 68 ++--- src/ol/resolutionconstraint.js | 47 ++-- test/spec/ol/interaction/dragzoom.test.js | 2 +- test/spec/ol/resolutionconstraint.test.js | 296 +++++++++++----------- test/spec/ol/view.test.js | 56 ++++ 5 files changed, 252 insertions(+), 217 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 4c03566bdc..c3645663d1 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -665,28 +665,6 @@ class View extends BaseObject { return size; } - /** - * Get the constrained resolution of this view. - * @param {number|undefined} resolution Resolution. - * @param {number=} opt_delta Delta. Default is `0`. - * @param {number=} opt_direction Direction. Default is `0`. - * @return {number|undefined} Constrained resolution. - * @private - */ - constrainResolution(resolution, opt_delta, opt_direction) { - const delta = opt_delta || 0; - const direction = opt_direction || 0; - - const size = this.getSizeFromViewport_(); - const rotation = this.getRotation() || 0; - const rotatedSize = [ - Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), - Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) - ]; - - return this.constraints_.resolution(resolution, delta, direction, rotatedSize); - } - /** * Get the view center. * @return {import("./coordinate.js").Coordinate|undefined} The center of the view. @@ -964,8 +942,14 @@ class View extends BaseObject { * @api */ getResolutionForZoom(zoom) { - return /** @type {number} */ (this.constrainResolution( - this.maxResolution_, zoom - this.minZoom_, 0)); + if (this.resolutions_) { + const baseLevel = clamp(Math.floor(zoom), 0, this.resolutions_.length - 2); + 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_); + } } /** @@ -1008,8 +992,7 @@ class View extends BaseObject { if (options.minResolution !== undefined) { minResolution = options.minResolution; } else if (options.maxZoom !== undefined) { - minResolution = this.constrainResolution( - this.maxResolution_, options.maxZoom - this.minZoom_, 0); + minResolution = this.getResolutionForZoom(options.maxZoom); } else { minResolution = 0; } @@ -1040,12 +1023,7 @@ class View extends BaseObject { resolution = isNaN(resolution) ? minResolution : Math.max(resolution, minResolution); if (constrainResolution) { - let constrainedResolution = this.constrainResolution(resolution, 0, 0); - if (!nearest && constrainedResolution < resolution) { - constrainedResolution = this.constrainResolution( - constrainedResolution, -1, 0); - } - resolution = constrainedResolution; + resolution = this.getValidResolution(resolution, nearest ? 0 : 1); } // calculate center @@ -1196,7 +1174,7 @@ class View extends BaseObject { Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) ]; - const newResolution = this.constraints_.resolution(this.targetResolution_, 0, 0, rotatedSize, isMoving); + const newResolution = this.constraints_.resolution(this.targetResolution_, 0, rotatedSize, isMoving); const newCenter = this.constraints_.center(this.targetCenter_, newResolution, rotatedSize, isMoving); this.set(ViewProperty.ROTATION, newRotation); @@ -1226,7 +1204,7 @@ class View extends BaseObject { /** * Get a valid zoom level according to the current view constraints. - * @param {number|undefined} targetZoom Target resolution. + * @param {number|undefined} targetZoom Target zoom. * @param {number=} opt_direction Direction. Default is `0`. Specify `-1` or `1` to return * the available value respectively lower or greater than the target one. Leaving `0` will simply choose * the nearest available value. @@ -1234,10 +1212,21 @@ class View extends BaseObject { * @api */ getValidZoomLevel(targetZoom, opt_direction) { - const direction = opt_direction || 0; - const currentRes = this.getResolution(); - const currentZoom = this.getZoom(); + const targetRes = this.getResolutionForZoom(targetZoom); + return this.getZoomForResolution(this.getValidResolution(targetRes)); + } + /** + * Get a valid resolution according to the current view constraints. + * @param {number|undefined} targetResolution Target resolution. + * @param {number=} opt_direction Direction. Default is `0`. Specify `-1` or `1` to return + * the available value respectively lower or greater than the target one. Leaving `0` will simply choose + * the nearest available value. + * @return {number|undefined} Valid resolution. + * @api + */ + getValidResolution(targetResolution, opt_direction) { + const direction = opt_direction || 0; const size = this.getSizeFromViewport_(); const rotation = this.getRotation() || 0; const rotatedSize = [ @@ -1245,8 +1234,7 @@ class View extends BaseObject { Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) ]; - return this.getZoomForResolution( - this.constraints_.resolution(currentRes, targetZoom - currentZoom, direction, rotatedSize)); + return(this.constraints_.resolution(targetResolution, direction, rotatedSize)); } } @@ -1350,7 +1338,7 @@ export function createResolutionConstraint(options) { minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom); resolutionConstraint = createSnapToPower( - zoomFactor, maxResolution, maxZoom - minZoom, + zoomFactor, maxResolution, minResolution, !options.constrainOnlyCenter && options.extent); } return {constraint: resolutionConstraint, maxResolution: maxResolution, diff --git a/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index d007b81dc9..575d603286 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -4,10 +4,11 @@ import {linearFindNearest} from './array.js'; import {clamp} from './math.js'; import {getHeight, getWidth} from './extent'; +import {clamp} from './math'; /** - * @typedef {function((number|undefined), number, number, import("./size.js").Size, boolean=): (number|undefined)} Type + * @typedef {function((number|undefined), number, import("./size.js").Size, boolean=): (number|undefined)} Type */ @@ -20,13 +21,12 @@ export function createSnapToResolutions(resolutions, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. - * @param {number} delta Delta. * @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, delta, direction, size, opt_isMoving) { + function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { let cappedRes = resolution; @@ -39,19 +39,13 @@ export function createSnapToResolutions(resolutions, opt_maxExtent) { // during interacting or animating, allow intermediary values if (opt_isMoving) { - // TODO: actually take delta and direction into account - return Math.min(resolution, cappedRes); + const maxResolution = resolutions[0]; + const minResolution = resolutions[resolutions.length - 1]; + return clamp(cappedRes, minResolution, maxResolution); } - let z = linearFindNearest(resolutions, cappedRes, direction); - z = clamp(z + delta, 0, resolutions.length - 1); - const index = Math.floor(z); - if (z != index && index < resolutions.length - 1) { - const power = resolutions[index] / resolutions[index + 1]; - return resolutions[index] / Math.pow(power, z - index); - } else { - return resolutions[index]; - } + let z = Math.floor(linearFindNearest(resolutions, cappedRes, direction)); + return resolutions[z]; } else { return undefined; } @@ -63,23 +57,22 @@ export function createSnapToResolutions(resolutions, opt_maxExtent) { /** * @param {number} power Power. * @param {number} maxResolution Maximum resolution. - * @param {number=} opt_maxLevel Maximum level. + * @param {number=} opt_minResolution Minimum resolution. * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. * @return {Type} Zoom function. */ -export function createSnapToPower(power, maxResolution, opt_maxLevel, opt_maxExtent) { +export function createSnapToPower(power, maxResolution, opt_minResolution, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. - * @param {number} delta Delta. * @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, delta, direction, size, opt_isMoving) { + function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { - let cappedRes = resolution; + let cappedRes = Math.min(resolution, maxResolution); // apply constraint related to max extent if (opt_maxExtent) { @@ -90,18 +83,16 @@ export function createSnapToPower(power, maxResolution, opt_maxLevel, opt_maxExt // during interacting or animating, allow intermediary values if (opt_isMoving) { - // TODO: actually take delta and direction into account - return Math.min(resolution, cappedRes); + return opt_minResolution !== undefined ? Math.max(opt_minResolution, cappedRes) : cappedRes; } - const offset = -direction / 2 + 0.5; - const oldLevel = Math.floor( + const offset = -direction * (0.5 - 1e-9) + 0.5; + const zoomLevel = Math.floor( Math.log(maxResolution / cappedRes) / Math.log(power) + offset); - let newLevel = Math.max(oldLevel + delta, 0); - if (opt_maxLevel !== undefined) { - newLevel = Math.min(newLevel, opt_maxLevel); - } - return maxResolution / Math.pow(power, newLevel); + let newResolution = maxResolution / Math.pow(power, zoomLevel); + return opt_minResolution !== undefined ? + clamp(newResolution, opt_minResolution, maxResolution) : + Math.min(maxResolution, newResolution); } else { return undefined; } diff --git a/test/spec/ol/interaction/dragzoom.test.js b/test/spec/ol/interaction/dragzoom.test.js index de9f3ec6af..77201d253e 100644 --- a/test/spec/ol/interaction/dragzoom.test.js +++ b/test/spec/ol/interaction/dragzoom.test.js @@ -103,7 +103,7 @@ describe('ol.interaction.DragZoom', function() { setTimeout(function() { const view = map.getView(); const resolution = view.getResolution(); - expect(resolution).to.eql(view.constrainResolution(0.5)); + expect(resolution).to.eql(view.getValidResolution(0.5)); done(); }, 50); }, 50); diff --git a/test/spec/ol/resolutionconstraint.test.js b/test/spec/ol/resolutionconstraint.test.js index c48ea0fe10..9ba06c788d 100644 --- a/test/spec/ol/resolutionconstraint.test.js +++ b/test/spec/ol/resolutionconstraint.test.js @@ -12,30 +12,30 @@ describe('ol.resolutionconstraint', function() { [1000, 500, 250, 100]); }); - describe('delta 0', function() { + describe('direction 0', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1000, 0, 0)).to.eql(1000); - expect(resolutionConstraint(500, 0, 0)).to.eql(500); - expect(resolutionConstraint(250, 0, 0)).to.eql(250); - expect(resolutionConstraint(100, 0, 0)).to.eql(100); + expect(resolutionConstraint(1000, 0)).to.eql(1000); + expect(resolutionConstraint(500, 0)).to.eql(500); + expect(resolutionConstraint(250, 0)).to.eql(250); + expect(resolutionConstraint(100, 0)).to.eql(100); }); }); - describe('zoom in', function() { + describe('direction 1', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1000, 1, 0)).to.eql(500); - expect(resolutionConstraint(500, 1, 0)).to.eql(250); - expect(resolutionConstraint(250, 1, 0)).to.eql(100); - expect(resolutionConstraint(100, 1, 0)).to.eql(100); + expect(resolutionConstraint(1000, 1)).to.eql(1000); + expect(resolutionConstraint(500, 1)).to.eql(500); + expect(resolutionConstraint(250, 1)).to.eql(250); + expect(resolutionConstraint(100, 1)).to.eql(100); }); }); - describe('zoom out', function() { + describe('direction -1', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1000, -1, 0)).to.eql(1000); - expect(resolutionConstraint(500, -1, 0)).to.eql(1000); - expect(resolutionConstraint(250, -1, 0)).to.eql(500); - expect(resolutionConstraint(100, -1, 0)).to.eql(250); + expect(resolutionConstraint(1000, -1)).to.eql(1000); + expect(resolutionConstraint(500, -1)).to.eql(500); + expect(resolutionConstraint(250, -1)).to.eql(250); + expect(resolutionConstraint(100, -1)).to.eql(100); }); }); }); @@ -50,42 +50,42 @@ describe('ol.resolutionconstraint', function() { [1000, 500, 250, 100]); }); - describe('delta 0', function() { + describe('direction 0', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1050, 0, 0)).to.eql(1000); - expect(resolutionConstraint(950, 0, 0)).to.eql(1000); - expect(resolutionConstraint(550, 0, 0)).to.eql(500); - expect(resolutionConstraint(400, 0, 0)).to.eql(500); - expect(resolutionConstraint(300, 0, 0)).to.eql(250); - expect(resolutionConstraint(200, 0, 0)).to.eql(250); - expect(resolutionConstraint(150, 0, 0)).to.eql(100); - expect(resolutionConstraint(50, 0, 0)).to.eql(100); + expect(resolutionConstraint(1050, 0)).to.eql(1000); + expect(resolutionConstraint(950, 0)).to.eql(1000); + expect(resolutionConstraint(550, 0)).to.eql(500); + expect(resolutionConstraint(400, 0)).to.eql(500); + expect(resolutionConstraint(300, 0)).to.eql(250); + expect(resolutionConstraint(200, 0)).to.eql(250); + expect(resolutionConstraint(150, 0)).to.eql(100); + expect(resolutionConstraint(50, 0)).to.eql(100); }); }); - describe('zoom in', function() { + describe('direction 1', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1050, 1, 0)).to.eql(500); - expect(resolutionConstraint(950, 1, 0)).to.eql(500); - expect(resolutionConstraint(550, 1, 0)).to.eql(250); - expect(resolutionConstraint(450, 1, 0)).to.eql(250); - expect(resolutionConstraint(300, 1, 0)).to.eql(100); - expect(resolutionConstraint(200, 1, 0)).to.eql(100); - expect(resolutionConstraint(150, 1, 0)).to.eql(100); - expect(resolutionConstraint(50, 1, 0)).to.eql(100); + expect(resolutionConstraint(1050, 1)).to.eql(1000); + expect(resolutionConstraint(950, 1)).to.eql(1000); + expect(resolutionConstraint(550, 1)).to.eql(1000); + expect(resolutionConstraint(450, 1)).to.eql(500); + expect(resolutionConstraint(300, 1)).to.eql(500); + expect(resolutionConstraint(200, 1)).to.eql(250); + expect(resolutionConstraint(150, 1)).to.eql(250); + expect(resolutionConstraint(50, 1)).to.eql(100); }); }); - describe('zoom out', function() { + describe('direction -1', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1050, -1, 0)).to.eql(1000); - expect(resolutionConstraint(950, -1, 0)).to.eql(1000); - expect(resolutionConstraint(550, -1, 0)).to.eql(1000); - expect(resolutionConstraint(450, -1, 0)).to.eql(1000); - expect(resolutionConstraint(300, -1, 0)).to.eql(500); - expect(resolutionConstraint(200, -1, 0)).to.eql(500); - expect(resolutionConstraint(150, -1, 0)).to.eql(250); - expect(resolutionConstraint(50, -1, 0)).to.eql(250); + expect(resolutionConstraint(1050, -1)).to.eql(1000); + expect(resolutionConstraint(950, -1)).to.eql(500); + expect(resolutionConstraint(550, -1)).to.eql(500); + expect(resolutionConstraint(450, -1)).to.eql(250); + expect(resolutionConstraint(300, -1)).to.eql(250); + expect(resolutionConstraint(200, -1)).to.eql(100); + expect(resolutionConstraint(150, -1)).to.eql(100); + expect(resolutionConstraint(50, -1)).to.eql(100); }); }); }); @@ -96,54 +96,54 @@ describe('ol.resolutionconstraint', function() { beforeEach(function() { resolutionConstraint = - createSnapToPower(2, 1024, 10); + createSnapToPower(2, 1024, 1); }); describe('delta 0', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1024, 0, 0)).to.eql(1024); - expect(resolutionConstraint(512, 0, 0)).to.eql(512); - expect(resolutionConstraint(256, 0, 0)).to.eql(256); - expect(resolutionConstraint(128, 0, 0)).to.eql(128); - expect(resolutionConstraint(64, 0, 0)).to.eql(64); - expect(resolutionConstraint(32, 0, 0)).to.eql(32); - expect(resolutionConstraint(16, 0, 0)).to.eql(16); - expect(resolutionConstraint(8, 0, 0)).to.eql(8); - expect(resolutionConstraint(4, 0, 0)).to.eql(4); - expect(resolutionConstraint(2, 0, 0)).to.eql(2); - expect(resolutionConstraint(1, 0, 0)).to.eql(1); + expect(resolutionConstraint(1024, 0)).to.eql(1024); + expect(resolutionConstraint(512, 0)).to.eql(512); + expect(resolutionConstraint(256, 0)).to.eql(256); + expect(resolutionConstraint(128, 0)).to.eql(128); + expect(resolutionConstraint(64, 0)).to.eql(64); + expect(resolutionConstraint(32, 0)).to.eql(32); + expect(resolutionConstraint(16, 0)).to.eql(16); + expect(resolutionConstraint(8, 0)).to.eql(8); + expect(resolutionConstraint(4, 0)).to.eql(4); + expect(resolutionConstraint(2, 0)).to.eql(2); + expect(resolutionConstraint(1, 0)).to.eql(1); }); }); - describe('zoom in', function() { + describe('direction 1', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1024, 1, 0)).to.eql(512); - expect(resolutionConstraint(512, 1, 0)).to.eql(256); - expect(resolutionConstraint(256, 1, 0)).to.eql(128); - expect(resolutionConstraint(128, 1, 0)).to.eql(64); - expect(resolutionConstraint(64, 1, 0)).to.eql(32); - expect(resolutionConstraint(32, 1, 0)).to.eql(16); - expect(resolutionConstraint(16, 1, 0)).to.eql(8); - expect(resolutionConstraint(8, 1, 0)).to.eql(4); - expect(resolutionConstraint(4, 1, 0)).to.eql(2); - expect(resolutionConstraint(2, 1, 0)).to.eql(1); - expect(resolutionConstraint(1, 1, 0)).to.eql(1); + expect(resolutionConstraint(1024, 1)).to.eql(1024); + expect(resolutionConstraint(512, 1)).to.eql(512); + expect(resolutionConstraint(256, 1)).to.eql(256); + expect(resolutionConstraint(128, 1)).to.eql(128); + expect(resolutionConstraint(64, 1)).to.eql(64); + expect(resolutionConstraint(32, 1)).to.eql(32); + expect(resolutionConstraint(16, 1)).to.eql(16); + expect(resolutionConstraint(8, 1)).to.eql(8); + expect(resolutionConstraint(4, 1)).to.eql(4); + expect(resolutionConstraint(2, 1)).to.eql(2); + expect(resolutionConstraint(1, 1)).to.eql(1); }); }); - describe('zoom out', function() { + describe('direction -1', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1024, -1, 0)).to.eql(1024); - expect(resolutionConstraint(512, -1, 0)).to.eql(1024); - expect(resolutionConstraint(256, -1, 0)).to.eql(512); - expect(resolutionConstraint(128, -1, 0)).to.eql(256); - expect(resolutionConstraint(64, -1, 0)).to.eql(128); - expect(resolutionConstraint(32, -1, 0)).to.eql(64); - expect(resolutionConstraint(16, -1, 0)).to.eql(32); - expect(resolutionConstraint(8, -1, 0)).to.eql(16); - expect(resolutionConstraint(4, -1, 0)).to.eql(8); - expect(resolutionConstraint(2, -1, 0)).to.eql(4); - expect(resolutionConstraint(1, -1, 0)).to.eql(2); + expect(resolutionConstraint(1024, -1)).to.eql(1024); + expect(resolutionConstraint(512, -1)).to.eql(512); + expect(resolutionConstraint(256, -1)).to.eql(256); + expect(resolutionConstraint(128, -1)).to.eql(128); + expect(resolutionConstraint(64, -1)).to.eql(64); + expect(resolutionConstraint(32, -1)).to.eql(32); + expect(resolutionConstraint(16, -1)).to.eql(16); + expect(resolutionConstraint(8, -1)).to.eql(8); + expect(resolutionConstraint(4, -1)).to.eql(4); + expect(resolutionConstraint(2, -1)).to.eql(2); + expect(resolutionConstraint(1, -1)).to.eql(1); }); }); }); @@ -154,87 +154,87 @@ describe('ol.resolutionconstraint', function() { beforeEach(function() { resolutionConstraint = - createSnapToPower(2, 1024, 10); + createSnapToPower(2, 1024, 1); }); - describe('delta 0, direction 0', function() { + describe('direction 0', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1050, 0, 0)).to.eql(1024); - expect(resolutionConstraint(9050, 0, 0)).to.eql(1024); - expect(resolutionConstraint(550, 0, 0)).to.eql(512); - expect(resolutionConstraint(450, 0, 0)).to.eql(512); - expect(resolutionConstraint(300, 0, 0)).to.eql(256); - expect(resolutionConstraint(250, 0, 0)).to.eql(256); - expect(resolutionConstraint(150, 0, 0)).to.eql(128); - expect(resolutionConstraint(100, 0, 0)).to.eql(128); - expect(resolutionConstraint(75, 0, 0)).to.eql(64); - expect(resolutionConstraint(50, 0, 0)).to.eql(64); - expect(resolutionConstraint(40, 0, 0)).to.eql(32); - expect(resolutionConstraint(30, 0, 0)).to.eql(32); - expect(resolutionConstraint(20, 0, 0)).to.eql(16); - expect(resolutionConstraint(12, 0, 0)).to.eql(16); - expect(resolutionConstraint(9, 0, 0)).to.eql(8); - expect(resolutionConstraint(7, 0, 0)).to.eql(8); - expect(resolutionConstraint(5, 0, 0)).to.eql(4); - expect(resolutionConstraint(3.5, 0, 0)).to.eql(4); - expect(resolutionConstraint(2.1, 0, 0)).to.eql(2); - expect(resolutionConstraint(1.9, 0, 0)).to.eql(2); - expect(resolutionConstraint(1.1, 0, 0)).to.eql(1); - expect(resolutionConstraint(0.9, 0, 0)).to.eql(1); + expect(resolutionConstraint(1050, 0)).to.eql(1024); + expect(resolutionConstraint(9050, 0)).to.eql(1024); + expect(resolutionConstraint(550, 0)).to.eql(512); + expect(resolutionConstraint(450, 0)).to.eql(512); + expect(resolutionConstraint(300, 0)).to.eql(256); + expect(resolutionConstraint(250, 0)).to.eql(256); + expect(resolutionConstraint(150, 0)).to.eql(128); + expect(resolutionConstraint(100, 0)).to.eql(128); + expect(resolutionConstraint(75, 0)).to.eql(64); + expect(resolutionConstraint(50, 0)).to.eql(64); + expect(resolutionConstraint(40, 0)).to.eql(32); + expect(resolutionConstraint(30, 0)).to.eql(32); + expect(resolutionConstraint(20, 0)).to.eql(16); + expect(resolutionConstraint(12, 0)).to.eql(16); + expect(resolutionConstraint(9, 0)).to.eql(8); + expect(resolutionConstraint(7, 0)).to.eql(8); + expect(resolutionConstraint(5, 0)).to.eql(4); + expect(resolutionConstraint(3.5, 0)).to.eql(4); + expect(resolutionConstraint(2.1, 0)).to.eql(2); + expect(resolutionConstraint(1.9, 0)).to.eql(2); + expect(resolutionConstraint(1.1, 0)).to.eql(1); + expect(resolutionConstraint(0.9, 0)).to.eql(1); }); }); - describe('delta 0, direction > 0', function() { + describe('direction 1', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1050, 0, 1)).to.eql(1024); - expect(resolutionConstraint(9050, 0, 1)).to.eql(1024); - expect(resolutionConstraint(550, 0, 1)).to.eql(1024); - expect(resolutionConstraint(450, 0, 1)).to.eql(512); - expect(resolutionConstraint(300, 0, 1)).to.eql(512); - expect(resolutionConstraint(250, 0, 1)).to.eql(256); - expect(resolutionConstraint(150, 0, 1)).to.eql(256); - expect(resolutionConstraint(100, 0, 1)).to.eql(128); - expect(resolutionConstraint(75, 0, 1)).to.eql(128); - expect(resolutionConstraint(50, 0, 1)).to.eql(64); - expect(resolutionConstraint(40, 0, 1)).to.eql(64); - expect(resolutionConstraint(30, 0, 1)).to.eql(32); - expect(resolutionConstraint(20, 0, 1)).to.eql(32); - expect(resolutionConstraint(12, 0, 1)).to.eql(16); - expect(resolutionConstraint(9, 0, 1)).to.eql(16); - expect(resolutionConstraint(7, 0, 1)).to.eql(8); - expect(resolutionConstraint(5, 0, 1)).to.eql(8); - expect(resolutionConstraint(3.5, 0, 1)).to.eql(4); - expect(resolutionConstraint(2.1, 0, 1)).to.eql(4); - expect(resolutionConstraint(1.9, 0, 1)).to.eql(2); - expect(resolutionConstraint(1.1, 0, 1)).to.eql(2); - expect(resolutionConstraint(0.9, 0, 1)).to.eql(1); + expect(resolutionConstraint(1050, 1)).to.eql(1024); + expect(resolutionConstraint(9050, 1)).to.eql(1024); + expect(resolutionConstraint(550, 1)).to.eql(1024); + expect(resolutionConstraint(450, 1)).to.eql(512); + expect(resolutionConstraint(300, 1)).to.eql(512); + expect(resolutionConstraint(250, 1)).to.eql(256); + expect(resolutionConstraint(150, 1)).to.eql(256); + expect(resolutionConstraint(100, 1)).to.eql(128); + expect(resolutionConstraint(75, 1)).to.eql(128); + expect(resolutionConstraint(50, 1)).to.eql(64); + expect(resolutionConstraint(40, 1)).to.eql(64); + expect(resolutionConstraint(30, 1)).to.eql(32); + expect(resolutionConstraint(20, 1)).to.eql(32); + expect(resolutionConstraint(12, 1)).to.eql(16); + expect(resolutionConstraint(9, 1)).to.eql(16); + expect(resolutionConstraint(7, 1)).to.eql(8); + expect(resolutionConstraint(5, 1)).to.eql(8); + expect(resolutionConstraint(3.5, 1)).to.eql(4); + expect(resolutionConstraint(2.1, 1)).to.eql(4); + expect(resolutionConstraint(1.9, 1)).to.eql(2); + expect(resolutionConstraint(1.1, 1)).to.eql(2); + expect(resolutionConstraint(0.9, 1)).to.eql(1); }); }); - describe('delta 0, direction < 0', function() { + describe('direction -1', function() { it('returns expected resolution value', function() { - expect(resolutionConstraint(1050, 0, -1)).to.eql(1024); - expect(resolutionConstraint(9050, 0, -1)).to.eql(1024); - expect(resolutionConstraint(550, 0, -1)).to.eql(512); - expect(resolutionConstraint(450, 0, -1)).to.eql(256); - expect(resolutionConstraint(300, 0, -1)).to.eql(256); - expect(resolutionConstraint(250, 0, -1)).to.eql(128); - expect(resolutionConstraint(150, 0, -1)).to.eql(128); - expect(resolutionConstraint(100, 0, -1)).to.eql(64); - expect(resolutionConstraint(75, 0, -1)).to.eql(64); - expect(resolutionConstraint(50, 0, -1)).to.eql(32); - expect(resolutionConstraint(40, 0, -1)).to.eql(32); - expect(resolutionConstraint(30, 0, -1)).to.eql(16); - expect(resolutionConstraint(20, 0, -1)).to.eql(16); - expect(resolutionConstraint(12, 0, -1)).to.eql(8); - expect(resolutionConstraint(9, 0, -1)).to.eql(8); - expect(resolutionConstraint(7, 0, -1)).to.eql(4); - expect(resolutionConstraint(5, 0, -1)).to.eql(4); - expect(resolutionConstraint(3.5, 0, -1)).to.eql(2); - expect(resolutionConstraint(2.1, 0, -1)).to.eql(2); - expect(resolutionConstraint(1.9, 0, -1)).to.eql(1); - expect(resolutionConstraint(1.1, 0, -1)).to.eql(1); - expect(resolutionConstraint(0.9, 0, -1)).to.eql(1); + expect(resolutionConstraint(1050, -1)).to.eql(1024); + expect(resolutionConstraint(9050, -1)).to.eql(1024); + expect(resolutionConstraint(550, -1)).to.eql(512); + expect(resolutionConstraint(450, -1)).to.eql(256); + expect(resolutionConstraint(300, -1)).to.eql(256); + expect(resolutionConstraint(250, -1)).to.eql(128); + expect(resolutionConstraint(150, -1)).to.eql(128); + expect(resolutionConstraint(100, -1)).to.eql(64); + expect(resolutionConstraint(75, -1)).to.eql(64); + expect(resolutionConstraint(50, -1)).to.eql(32); + expect(resolutionConstraint(40, -1)).to.eql(32); + expect(resolutionConstraint(30, -1)).to.eql(16); + expect(resolutionConstraint(20, -1)).to.eql(16); + expect(resolutionConstraint(12, -1)).to.eql(8); + expect(resolutionConstraint(9, -1)).to.eql(8); + expect(resolutionConstraint(7, -1)).to.eql(4); + expect(resolutionConstraint(5, -1)).to.eql(4); + expect(resolutionConstraint(3.5, -1)).to.eql(2); + expect(resolutionConstraint(2.1, -1)).to.eql(2); + expect(resolutionConstraint(1.9, -1)).to.eql(1); + expect(resolutionConstraint(1.1, -1)).to.eql(1); + expect(resolutionConstraint(0.9, -1)).to.eql(1); }); }); }); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 88b18e4718..b41cc34f67 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -1104,7 +1104,9 @@ describe('ol.View', function() { const min = view.getMinZoom(); expect(view.getResolutionForZoom(max)).to.be(view.getMinResolution()); + expect(view.getResolutionForZoom(max + 1)).to.be(view.getMinResolution()); expect(view.getResolutionForZoom(min)).to.be(view.getMaxResolution()); + expect(view.getResolutionForZoom(min - 1)).to.be(view.getMaxResolution()); }); it('returns correct zoom levels for specifically configured resolutions', function() { @@ -1112,11 +1114,30 @@ describe('ol.View', function() { resolutions: [10, 8, 6, 4, 2] }); + expect(view.getResolutionForZoom(-1)).to.be(10); expect(view.getResolutionForZoom(0)).to.be(10); expect(view.getResolutionForZoom(1)).to.be(8); expect(view.getResolutionForZoom(2)).to.be(6); expect(view.getResolutionForZoom(3)).to.be(4); expect(view.getResolutionForZoom(4)).to.be(2); + expect(view.getResolutionForZoom(5)).to.be(2); + }); + + it('returns correct zoom levels for resolutions with variable zoom levels', function() { + const view = new View({ + resolutions: [50, 10, 5, 2.5, 1.25, 0.625] + }); + + expect(view.getResolutionForZoom(-1)).to.be(50); + expect(view.getResolutionForZoom(0)).to.be(50); + expect(view.getResolutionForZoom(0.5)).to.be(50 / Math.pow(5, 0.5)); + expect(view.getResolutionForZoom(1)).to.be(10); + expect(view.getResolutionForZoom(2)).to.be(5); + expect(view.getResolutionForZoom(2.75)).to.be(5 / Math.pow(2, 0.75)); + expect(view.getResolutionForZoom(3)).to.be(2.5); + expect(view.getResolutionForZoom(4)).to.be(1.25); + expect(view.getResolutionForZoom(5)).to.be(0.625); + expect(view.getResolutionForZoom(6)).to.be(0.625); }); }); @@ -1470,6 +1491,41 @@ describe('ol.View', function() { expect(view.getValidZoomLevel(8)).to.be(6); }); }); + + describe('#getValidResolution()', function() { + let view; + const defaultMaxRes = 156543.03392804097; + + it('works correctly by snapping to power of 2', function() { + view = new View(); + expect(view.getValidResolution(1000000)).to.be(defaultMaxRes); + expect(view.getValidResolution(defaultMaxRes / 8)).to.be(defaultMaxRes / 8); + }); + it('works correctly by snapping to a custom zoom factor', function() { + view = new View({ + maxResolution: 2500, + zoomFactor: 5, + maxZoom: 4 + }); + expect(view.getValidResolution(90, 1)).to.be(100); + expect(view.getValidResolution(90, -1)).to.be(20); + expect(view.getValidResolution(20)).to.be(20); + expect(view.getValidResolution(5)).to.be(4); + expect(view.getValidResolution(1)).to.be(4); + }); + it('works correctly with a specific resolution set', function() { + view = new View({ + zoom: 0, + resolutions: [512, 256, 128, 64, 32, 16, 8] + }); + expect(view.getValidResolution(1000, 1)).to.be(512); + expect(view.getValidResolution(260, 1)).to.be(512); + expect(view.getValidResolution(260)).to.be(256); + expect(view.getValidResolution(30)).to.be(32); + expect(view.getValidResolution(30, -1)).to.be(16); + expect(view.getValidResolution(4, -1)).to.be(8); + }); + }); }); describe('ol.View.isNoopAnimation()', function() { From a6f65df8c49fc603bee58f9f115430e1f68aac52 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 14 Jan 2019 17:10:55 +0100 Subject: [PATCH 10/25] View / add a `resolveConstraints` method to end interactions This will help making sure that the view will come back to a "rested" state once the interactions are over. Interactions no longer need to handle the animation back to a rested state, they simply call `endInteraction` with the desired duration and direction. --- src/ol/View.js | 75 ++++++++++++++++++------- src/ol/interaction/DragPan.js | 10 ++-- src/ol/interaction/DragRotate.js | 4 +- src/ol/interaction/DragRotateAndZoom.js | 4 +- src/ol/interaction/PinchRotate.js | 6 +- src/ol/interaction/PinchZoom.js | 13 +---- src/ol/rotationconstraint.js | 21 +++---- test/spec/ol/rotationconstraint.test.js | 28 +++------ 8 files changed, 81 insertions(+), 80 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index c3645663d1..561af1cb39 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -21,6 +21,8 @@ import {clamp, modulo} from './math.js'; import {assign} from './obj.js'; import {createProjection, METERS_PER_UNIT} from './proj.js'; import Units from './proj/Units.js'; +import {equals} from './coordinate'; +import {easeOut} from './easing'; /** @@ -651,9 +653,10 @@ class View extends BaseObject { /** * @private + * @param {number|undefined} opt_rotation * @return {import("./size.js").Size} Viewport size or `[100, 100]` when no viewport is found. */ - getSizeFromViewport_() { + getSizeFromViewport_(opt_rotation) { const size = [100, 100]; const selector = '.ol-viewport[data-view="' + getUid(this) + '"]'; const element = document.querySelector(selector); @@ -662,6 +665,10 @@ class View extends BaseObject { size[0] = parseInt(metrics.width, 10); size[1] = parseInt(metrics.height, 10); } + if (opt_rotation) { + size[0] = Math.abs(size[0] * Math.cos(opt_rotation)) + Math.abs(size[1] * Math.sin(opt_rotation)); + size[1] = Math.abs(size[0] * Math.sin(opt_rotation)) + Math.abs(size[1] * Math.cos(opt_rotation)); + } return size; } @@ -1164,18 +1171,10 @@ class View extends BaseObject { const isMoving = this.getAnimating() || this.getInteracting() || opt_forceMoving; // compute rotation - const newRotation = this.constraints_.rotation(this.targetRotation_, 0, isMoving); - - // compute viewport size with rotation - const size = this.getSizeFromViewport_(); - const rotation = this.getRotation() || 0; - const rotatedSize = [ - Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), - Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) - ]; - - const newResolution = this.constraints_.resolution(this.targetResolution_, 0, rotatedSize, isMoving); - const newCenter = this.constraints_.center(this.targetCenter_, newResolution, rotatedSize, isMoving); + const newRotation = this.constraints_.rotation(this.targetRotation_, isMoving); + const size = this.getSizeFromViewport_(newRotation); + const newResolution = this.constraints_.resolution(this.targetResolution_, 0, size, isMoving); + const newCenter = this.constraints_.center(this.targetCenter_, newResolution, size, isMoving); this.set(ViewProperty.ROTATION, newRotation); this.set(ViewProperty.RESOLUTION, newResolution); @@ -1186,6 +1185,41 @@ class View extends BaseObject { } } + /** + * If any constraints need to be applied, an animation will be triggered. + * This is typically done on interaction end. + * @param {number=} opt_duration The animation duration in ms. + * @param {number=} opt_resolutionDirection Which direction to zoom. + * @observable + * @private + */ + resolveConstraints_(opt_duration, opt_resolutionDirection) { + const duration = opt_duration || 250; + const direction = opt_resolutionDirection || 0; + + const newRotation = this.constraints_.rotation(this.targetRotation_); + const size = this.getSizeFromViewport_(newRotation); + const newResolution = this.constraints_.resolution(this.targetResolution_, direction, size); + const newCenter = this.constraints_.center(this.targetCenter_, newResolution, size); + + if (this.getResolution() !== newResolution || + this.getRotation() !== newRotation || + !equals(this.getCenter(), newCenter)) { + + if (this.getAnimating()) { + this.cancelAnimations(); + } + + this.animate({ + rotation: newRotation, + center: newCenter, + resolution: newResolution, + duration: duration, + easing: easeOut + }); + } + } + /** * Notify the View that an interaction has started. * @api @@ -1196,10 +1230,14 @@ class View extends BaseObject { /** * Notify the View that an interaction has ended. + * @param {number=} opt_duration Animation duration in ms. + * @param {number=} opt_resolutionDirection Which direction to zoom. * @api */ - endInteraction() { + endInteraction(opt_duration, opt_resolutionDirection) { this.setHint(ViewHint.INTERACTING, -1); + + this.resolveConstraints_(opt_duration, opt_resolutionDirection); } /** @@ -1227,14 +1265,9 @@ class View extends BaseObject { */ getValidResolution(targetResolution, opt_direction) { const direction = opt_direction || 0; - const size = this.getSizeFromViewport_(); - const rotation = this.getRotation() || 0; - const rotatedSize = [ - Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), - Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) - ]; + const size = this.getSizeFromViewport_(this.getRotation()); - return(this.constraints_.resolution(targetResolution, direction, rotatedSize)); + return(this.constraints_.resolution(targetResolution, direction, size)); } } diff --git a/src/ol/interaction/DragPan.js b/src/ol/interaction/DragPan.js index 020eb509ef..6f18683a40 100644 --- a/src/ol/interaction/DragPan.js +++ b/src/ol/interaction/DragPan.js @@ -74,10 +74,6 @@ class DragPan extends PointerInteraction { * @inheritDoc */ handleDragEvent(mapBrowserEvent) { - if (!this.panning_) { - this.panning_ = true; - this.getMap().getView().beginInteraction(); - } const targetPointers = this.targetPointers; const centroid = centroidFromPointers(targetPointers); if (targetPointers.length == this.lastPointersCount_) { @@ -152,7 +148,11 @@ class DragPan extends PointerInteraction { this.lastCentroid = null; // stop any current animation if (view.getAnimating()) { - view.setCenter(mapBrowserEvent.frameState.viewState.center); + view.cancelAnimations(); + } + if (!this.panning_) { + this.panning_ = true; + this.getMap().getView().beginInteraction(); } if (this.kinetic_) { this.kinetic_.begin(); diff --git a/src/ol/interaction/DragRotate.js b/src/ol/interaction/DragRotate.js index 09143c0d79..7d4d9a4be2 100644 --- a/src/ol/interaction/DragRotate.js +++ b/src/ol/interaction/DragRotate.js @@ -97,9 +97,7 @@ class DragRotate extends PointerInteraction { const map = mapBrowserEvent.map; const view = map.getView(); - view.endInteraction(); - const rotation = view.getRotation(); - rotate(view, rotation, undefined, this.duration_); + view.endInteraction(this.duration_); return false; } diff --git a/src/ol/interaction/DragRotateAndZoom.js b/src/ol/interaction/DragRotateAndZoom.js index 0bd024ed67..3f3d4d62c3 100644 --- a/src/ol/interaction/DragRotateAndZoom.js +++ b/src/ol/interaction/DragRotateAndZoom.js @@ -113,10 +113,8 @@ class DragRotateAndZoom extends PointerInteraction { const map = mapBrowserEvent.map; const view = map.getView(); - view.endInteraction(); const direction = this.lastScaleDelta_ - 1; - rotate(view, view.getRotation()); - zoom(view, view.getResolution(), undefined, this.duration_, direction); + view.endInteraction(this.duration_, direction); this.lastScaleDelta_ = 0; return false; } diff --git a/src/ol/interaction/PinchRotate.js b/src/ol/interaction/PinchRotate.js index fc40d7f851..fba8c0ff96 100644 --- a/src/ol/interaction/PinchRotate.js +++ b/src/ol/interaction/PinchRotate.js @@ -131,11 +131,7 @@ class PinchRotate extends PointerInteraction { if (this.targetPointers.length < 2) { const map = mapBrowserEvent.map; const view = map.getView(); - view.endInteraction(); - if (this.rotating_) { - const rotation = view.getRotation(); - rotate(view, rotation, this.anchor_, this.duration_); - } + view.endInteraction(this.duration_); return false; } else { return true; diff --git a/src/ol/interaction/PinchZoom.js b/src/ol/interaction/PinchZoom.js index 885e69b492..282d543138 100644 --- a/src/ol/interaction/PinchZoom.js +++ b/src/ol/interaction/PinchZoom.js @@ -126,17 +126,8 @@ class PinchZoom extends PointerInteraction { if (this.targetPointers.length < 2) { const map = mapBrowserEvent.map; const view = map.getView(); - view.endInteraction(); - const resolution = view.getResolution(); - if (this.constrainResolution_ || - resolution < view.getMinResolution() || - resolution > view.getMaxResolution()) { - // Zoom to final resolution, with an animation, and provide a - // direction not to zoom out/in if user was pinching in/out. - // Direction is > 0 if pinching out, and < 0 if pinching in. - const direction = this.lastScaleDelta_ - 1; - zoom(view, resolution, this.anchor_, this.duration_, direction); - } + const direction = this.lastScaleDelta_ - 1; + view.endInteraction(this.duration_, direction); return false; } else { return true; diff --git a/src/ol/rotationconstraint.js b/src/ol/rotationconstraint.js index 1efcc7f734..67c5183869 100644 --- a/src/ol/rotationconstraint.js +++ b/src/ol/rotationconstraint.js @@ -5,16 +5,15 @@ import {toRadians} from './math.js'; /** - * @typedef {function((number|undefined), number, boolean=): (number|undefined)} Type + * @typedef {function((number|undefined), boolean=): (number|undefined)} Type */ /** * @param {number|undefined} rotation Rotation. - * @param {number} delta Delta. * @return {number|undefined} Rotation. */ -export function disable(rotation, delta) { +export function disable(rotation) { if (rotation !== undefined) { return 0; } else { @@ -25,12 +24,11 @@ export function disable(rotation, delta) { /** * @param {number|undefined} rotation Rotation. - * @param {number} delta Delta. * @return {number|undefined} Rotation. */ -export function none(rotation, delta) { +export function none(rotation) { if (rotation !== undefined) { - return rotation + delta; + return rotation; } else { return undefined; } @@ -46,17 +44,16 @@ export function createSnapToN(n) { return ( /** * @param {number|undefined} rotation Rotation. - * @param {number} delta Delta. * @param {boolean=} opt_isMoving True if an interaction or animation is in progress. * @return {number|undefined} Rotation. */ - function(rotation, delta, opt_isMoving) { + function(rotation, opt_isMoving) { if (opt_isMoving) { return rotation; } if (rotation !== undefined) { - rotation = Math.floor((rotation + delta) / theta + 0.5) * theta; + rotation = Math.floor(rotation / theta + 0.5) * theta; return rotation; } else { return undefined; @@ -77,16 +74,16 @@ export function createSnapToZero(opt_tolerance) { * @param {number} delta Delta. * @return {number|undefined} Rotation. */ - function(rotation, delta, opt_isMoving) { + function(rotation, opt_isMoving) { if (opt_isMoving) { return rotation; } if (rotation !== undefined) { - if (Math.abs(rotation + delta) <= tolerance) { + if (Math.abs(rotation) <= tolerance) { return 0; } else { - return rotation + delta; + return rotation; } } else { return undefined; diff --git a/test/spec/ol/rotationconstraint.test.js b/test/spec/ol/rotationconstraint.test.js index f3e19a6119..c042ad4307 100644 --- a/test/spec/ol/rotationconstraint.test.js +++ b/test/spec/ol/rotationconstraint.test.js @@ -8,27 +8,15 @@ describe('ol.rotationconstraint', function() { it('returns expected rotation value', function() { const rotationConstraint = createSnapToZero(0.3); - expect(rotationConstraint(0.1, 0)).to.eql(0); - expect(rotationConstraint(0.2, 0)).to.eql(0); - expect(rotationConstraint(0.3, 0)).to.eql(0); - expect(rotationConstraint(0.4, 0)).to.eql(0.4); + expect(rotationConstraint(0.1)).to.eql(0); + expect(rotationConstraint(0.2)).to.eql(0); + expect(rotationConstraint(0.3)).to.eql(0); + expect(rotationConstraint(0.4)).to.eql(0.4); - expect(rotationConstraint(-0.1, 0)).to.eql(0); - expect(rotationConstraint(-0.2, 0)).to.eql(0); - expect(rotationConstraint(-0.3, 0)).to.eql(0); - expect(rotationConstraint(-0.4, 0)).to.eql(-0.4); - - expect(rotationConstraint(1, -0.9)).to.eql(0); - expect(rotationConstraint(1, -0.8)).to.eql(0); - // floating-point arithmetic - expect(rotationConstraint(1, -0.7)).not.to.eql(0); - expect(rotationConstraint(1, -0.6)).to.eql(0.4); - - expect(rotationConstraint(-1, 0.9)).to.eql(0); - expect(rotationConstraint(-1, 0.8)).to.eql(0); - // floating-point arithmetic - expect(rotationConstraint(-1, 0.7)).not.to.eql(0); - expect(rotationConstraint(-1, 0.6)).to.eql(-0.4); + expect(rotationConstraint(-0.1)).to.eql(0); + expect(rotationConstraint(-0.2)).to.eql(0); + expect(rotationConstraint(-0.3)).to.eql(0); + expect(rotationConstraint(-0.4)).to.eql(-0.4); }); }); From cd186ada7fdeadd6e9a15ec369a9d236834d296a Mon Sep 17 00:00:00 2001 From: jahow Date: Mon, 14 Jan 2019 22:11:54 +0100 Subject: [PATCH 11/25] Add new tests for View & Interaction w/ fixes --- src/ol/View.js | 8 ++-- src/ol/interaction/Interaction.js | 17 -------- test/spec/ol/interaction/interaction.test.js | 46 +++++++++++++++++++- test/spec/ol/view.test.js | 8 +++- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 561af1cb39..67c63fa475 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -653,7 +653,7 @@ class View extends BaseObject { /** * @private - * @param {number|undefined} opt_rotation + * @param {number=} opt_rotation Take into account the rotation of the viewport when giving the size * @return {import("./size.js").Size} Viewport size or `[100, 100]` when no viewport is found. */ getSizeFromViewport_(opt_rotation) { @@ -666,8 +666,10 @@ class View extends BaseObject { size[1] = parseInt(metrics.height, 10); } if (opt_rotation) { - size[0] = Math.abs(size[0] * Math.cos(opt_rotation)) + Math.abs(size[1] * Math.sin(opt_rotation)); - size[1] = Math.abs(size[0] * Math.sin(opt_rotation)) + Math.abs(size[1] * Math.cos(opt_rotation)); + const w = size[0]; + const h = size[1]; + size[0] = Math.abs(w * Math.cos(opt_rotation)) + Math.abs(h * Math.sin(opt_rotation)); + size[1] = Math.abs(w * Math.sin(opt_rotation)) + Math.abs(h * Math.cos(opt_rotation)); } return size; } diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index 405e986891..b2897c8658 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -4,7 +4,6 @@ import BaseObject from '../Object.js'; import {easeOut, linear} from '../easing.js'; import InteractionProperty from './Property.js'; -import {clamp} from '../math.js'; /** @@ -194,7 +193,6 @@ export function zoom(view, resolution, opt_anchor, opt_duration, opt_direction) */ export function zoomByDelta(view, delta, opt_anchor, opt_duration) { const currentZoom = view.getZoom(); - const currentResolution = view.getResolution(); if (currentZoom === undefined) { return; @@ -203,21 +201,6 @@ export function zoomByDelta(view, delta, opt_anchor, opt_duration) { const newZoom = view.getValidZoomLevel(currentZoom + delta); const newResolution = view.getResolutionForZoom(newZoom); - // If we have a constraint on center, we need to change the anchor so that the - // new center is within the extent. We first calculate the new center, apply - // the constraint to it, and then calculate back the anchor - if (opt_anchor) { - const currentCenter = view.getCenter(); - const center = view.calculateCenterZoom(newResolution, opt_anchor); - - opt_anchor = [ - (newResolution * currentCenter[0] - currentResolution * center[0]) / - (newResolution - currentResolution), - (newResolution * currentCenter[1] - currentResolution * center[1]) / - (newResolution - currentResolution) - ]; - } - if (opt_duration > 0) { if (view.getAnimating()) { view.cancelAnimations(); diff --git a/test/spec/ol/interaction/interaction.test.js b/test/spec/ol/interaction/interaction.test.js index 3d2d04e91a..5a6f14a703 100644 --- a/test/spec/ol/interaction/interaction.test.js +++ b/test/spec/ol/interaction/interaction.test.js @@ -128,7 +128,7 @@ describe('ol.interaction.Interaction', function() { expect(view.getCenter()).to.eql([10, 10]); }); - it('changes view resolution and center relative to the anchor, while respecting the extent', function() { + it('changes view resolution and center relative to the anchor, while respecting the extent (center only)', function() { const view = new View({ center: [0, 0], extent: [-2.5, -2.5, 2.5, 2.5], @@ -149,6 +149,50 @@ describe('ol.interaction.Interaction', function() { zoomByDelta(view, -2, [0, 0]); expect(view.getCenter()).to.eql([2.5, 2.5]); }); + + it('changes view resolution and center relative to the anchor, while respecting the extent', function() { + const map = new Map({}); + const view = new View({ + center: [50, 50], + extent: [0, 0, 100, 100], + resolution: 1, + resolutions: [4, 2, 1, 0.5, 0.25, 0.125] + }); + map.setView(view); + + zoomByDelta(view, 1, [100, 100]); + expect(view.getCenter()).to.eql([75, 75]); + + zoomByDelta(view, -1, [75, 75]); + expect(view.getCenter()).to.eql([50, 50]); + + zoomByDelta(view, 2, [100, 100]); + expect(view.getCenter()).to.eql([87.5, 87.5]); + + zoomByDelta(view, -3, [0, 0]); + expect(view.getCenter()).to.eql([50, 50]); + expect(view.getResolution()).to.eql(2); + }); + + it('changes view resolution and center relative to the anchor, while respecting the extent (rotated)', function() { + const map = new Map({}); + const view = new View({ + center: [50, 50], + extent: [-100, -100, 100, 100], + resolution: 1, + resolutions: [2, 1, 0.5, 0.25, 0.125], + rotation: Math.PI / 4 + }); + map.setView(view); + const halfSize = 100 * Math.SQRT1_2; + + zoomByDelta(view, 1, [100, 100]); + expect(view.getCenter()).to.eql([100 - halfSize / 2, 100 - halfSize / 2]); + + view.setCenter([0, 50]); + zoomByDelta(view, -1, [0, 0]); + expect(view.getCenter()).to.eql([0, 100 - halfSize]); + }); }); }); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index b41cc34f67..fcc5cd64d8 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -1269,8 +1269,14 @@ describe('ol.View', function() { document.body.removeChild(target); }); it('calculates the size correctly', function() { - const size = map.getView().getSizeFromViewport_(); + let size = map.getView().getSizeFromViewport_(); expect(size).to.eql([200, 150]); + size = map.getView().getSizeFromViewport_(Math.PI / 2); + expect(size[0]).to.roughlyEqual(150,1e-9); + expect(size[1]).to.roughlyEqual(200, 1e-9); + size = map.getView().getSizeFromViewport_(Math.PI); + expect(size[0]).to.roughlyEqual(200,1e-9); + expect(size[1]).to.roughlyEqual(150, 1e-9); }); }); From 48ad1ffcbf0e2f401936c6533b4322779348623d Mon Sep 17 00:00:00 2001 From: jahow Date: Mon, 14 Jan 2019 22:30:35 +0100 Subject: [PATCH 12/25] View / implement a smooth rebound effect when a max extent is given This is done by applying the center constraint differently when we're in the middle of an interaction/animation or not. When the view is moving, the center constraint will restrain the given value in an "elastic" way, using a logarithmic function. This can be disabled using the `smoothCenterConstrain` view parameter. --- src/ol/View.js | 25 ++++++++++++++++++++++--- src/ol/centerconstraint.js | 24 +++++++++++++++++++----- src/ol/interaction/DragPan.js | 14 +++++++------- test/spec/ol/view.test.js | 22 +++++++++++++++++++++- 4 files changed, 69 insertions(+), 16 deletions(-) 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() { From 49662b019cea818065d53c428fac5710dbf95a12 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 20 Jan 2019 16:05:10 +0100 Subject: [PATCH 13/25] View / add a `constrainResolution` option This introduces a breaking change. This options replaces the various `constrainResolution` options on interactions and the `fit` method. Since constraints are now the responsibility of the View, the fact that intermediary zoom levels are allowed or not is now set in the view options. By default, the view resolution is unconstrained. --- examples/center.html | 4 +- examples/center.js | 21 ++------- examples/interaction-options.js | 2 +- examples/pinch-zoom.js | 9 ++-- src/ol/View.js | 47 +++++++++++++------ src/ol/interaction.js | 4 -- src/ol/interaction/MouseWheelZoom.js | 11 +---- src/ol/interaction/PinchZoom.js | 8 ---- src/ol/resolutionconstraint.js | 57 ++++++++++++++++------- test/spec/ol/map.test.js | 16 ------- test/spec/ol/view.test.js | 67 +++++++++++++++------------- 11 files changed, 120 insertions(+), 126 deletions(-) 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); From b5273babb50b6b9699655f2701aac7bf841ffe1b Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 20 Jan 2019 16:32:15 +0100 Subject: [PATCH 14/25] View / handle resolutions array with length=1 --- src/ol/View.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ol/View.js b/src/ol/View.js index caed07efbb..44a8570620 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -971,6 +971,9 @@ class View extends BaseObject { */ getResolutionForZoom(zoom) { if (this.resolutions_) { + if (this.resolutions_.length <= 1) { + return 0; + } const baseLevel = clamp(Math.floor(zoom), 0, this.resolutions_.length - 2); const zoomFactor = this.resolutions_[baseLevel] / this.resolutions_[baseLevel + 1]; return this.resolutions_[baseLevel] / Math.pow(zoomFactor, clamp(zoom - baseLevel, 0, 1)); From e023c144bb4d43ed517b711d82a62ddb3466f6ab Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Tue, 29 Jan 2019 12:13:27 +0100 Subject: [PATCH 15/25] View / add adjust* methods to manipulate the view more easily API changes: * (breaking) the `rotate` method is gone * the `adjustRotation`, `adjustResolution` and `adjustZoom` methods are now available and allow using an anchor. This means interactions do not have to do the anchor computation themselves and this also fix anchor computation when constraints must be applied. --- src/ol/View.js | 67 ++++++-- src/ol/interaction/DragRotate.js | 3 +- src/ol/interaction/DragRotateAndZoom.js | 10 +- src/ol/interaction/Interaction.js | 100 ++---------- src/ol/interaction/MouseWheelZoom.js | 44 +----- src/ol/interaction/PinchRotate.js | 3 +- src/ol/interaction/PinchZoom.js | 15 +- .../ol/interaction/dragrotateandzoom.test.js | 24 +-- test/spec/ol/interaction/interaction.test.js | 108 ------------- test/spec/ol/view.test.js | 147 ++++++++++++++++++ 10 files changed, 237 insertions(+), 284 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 44a8570620..380196154d 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -635,10 +635,10 @@ class View extends BaseObject { */ calculateCenterRotate(rotation, anchor) { let center; - const currentCenter = this.targetCenter_; + const currentCenter = this.getCenter(); if (currentCenter !== undefined) { center = [currentCenter[0] - anchor[0], currentCenter[1] - anchor[1]]; - rotateCoordinate(center, rotation - this.targetRotation_); + rotateCoordinate(center, rotation - this.getRotation()); addCoordinate(center, anchor); } return center; @@ -651,8 +651,8 @@ class View extends BaseObject { */ calculateCenterZoom(resolution, anchor) { let center; - const currentCenter = this.targetCenter_; - const currentResolution = this.targetResolution_; + const currentCenter = this.getCenter(); + const currentResolution = this.getResolution(); if (currentCenter !== undefined && currentResolution !== undefined) { const x = anchor[0] - resolution * (anchor[0] - currentCenter[0]) / currentResolution; const y = anchor[1] - resolution * (anchor[1] - currentCenter[1]) / currentResolution; @@ -1123,17 +1123,50 @@ class View extends BaseObject { } /** - * Rotate the view around a given coordinate. - * @param {number} rotation New rotation value for the view. - * @param {import("./coordinate.js").Coordinate=} opt_anchor The rotation center. + * Multiply the view resolution by a ratio, optionally using an anchor. + * @param {number} ratio The ratio to apply on the view resolution. + * @param {import("./coordinate.js").Coordinate=} opt_anchor The origin of the transformation. + * @observable * @api */ - rotate(rotation, opt_anchor) { + adjustResolution(ratio, opt_anchor) { + const isMoving = this.getAnimating() || this.getInteracting(); + const size = this.getSizeFromViewport_(this.getRotation()); + const newResolution = this.constraints_.resolution(this.targetResolution_ * ratio, 0, size, isMoving); + if (opt_anchor !== undefined) { - const center = this.calculateCenterRotate(rotation, opt_anchor); - this.setCenter(center); + this.targetCenter_ = this.calculateCenterZoom(newResolution, opt_anchor); } - this.setRotation(rotation); + + this.targetResolution_ *= ratio; + this.applyParameters_(); + } + + /** + * Adds a value to the view zoom level, optionally using an anchor. + * @param {number} delta Relative value to add to the zoom level. + * @param {import("./coordinate.js").Coordinate=} opt_anchor The origin of the transformation. + * @api + */ + adjustZoom(delta, opt_anchor) { + this.adjustResolution(Math.pow(this.zoomFactor_, -delta), opt_anchor); + } + + /** + * Adds a value to the view rotation, optionally using an anchor. + * @param {number} delta Relative value to add to the zoom rotation, in radians. + * @param {import("./coordinate.js").Coordinate=} opt_anchor The rotation center. + * @observable + * @api + */ + adjustRotation(delta, opt_anchor) { + const isMoving = this.getAnimating() || this.getInteracting(); + const newRotation = this.constraints_.rotation(this.targetRotation_ + delta, isMoving); + if (opt_anchor !== undefined) { + this.targetCenter_ = this.calculateCenterRotate(newRotation, opt_anchor); + } + this.targetRotation_ += delta; + this.applyParameters_(); } /** @@ -1206,9 +1239,15 @@ class View extends BaseObject { const newResolution = this.constraints_.resolution(this.targetResolution_, 0, size, isMoving); const newCenter = this.constraints_.center(this.targetCenter_, newResolution, size, isMoving); - this.set(ViewProperty.ROTATION, newRotation); - this.set(ViewProperty.RESOLUTION, newResolution); - this.set(ViewProperty.CENTER, newCenter); + if (this.get(ViewProperty.ROTATION) !== newRotation) { + this.set(ViewProperty.ROTATION, newRotation); + } + if (this.get(ViewProperty.RESOLUTION) !== newResolution) { + this.set(ViewProperty.RESOLUTION, newResolution); + } + if (!this.get(ViewProperty.CENTER) || !equals(this.get(ViewProperty.CENTER), newCenter)) { + this.set(ViewProperty.CENTER, newCenter); + } if (this.getAnimating() && !opt_doNotCancelAnims) { this.cancelAnimations(); diff --git a/src/ol/interaction/DragRotate.js b/src/ol/interaction/DragRotate.js index 7d4d9a4be2..c7c57fead9 100644 --- a/src/ol/interaction/DragRotate.js +++ b/src/ol/interaction/DragRotate.js @@ -80,8 +80,7 @@ class DragRotate extends PointerInteraction { Math.atan2(size[1] / 2 - offset[1], offset[0] - size[0] / 2); if (this.lastAngle_ !== undefined) { const delta = theta - this.lastAngle_; - const rotation = view.getRotation(); - rotate(view, rotation - delta); + view.adjustRotation(-delta); } this.lastAngle_ = theta; } diff --git a/src/ol/interaction/DragRotateAndZoom.js b/src/ol/interaction/DragRotateAndZoom.js index 3f3d4d62c3..ab229393fe 100644 --- a/src/ol/interaction/DragRotateAndZoom.js +++ b/src/ol/interaction/DragRotateAndZoom.js @@ -4,7 +4,6 @@ import {disable} from '../rotationconstraint.js'; import ViewHint from '../ViewHint.js'; import {shiftKeyOnly, mouseOnly} from '../events/condition.js'; -import {rotate, zoom} from './Interaction.js'; import PointerInteraction from './Pointer.js'; @@ -88,14 +87,13 @@ class DragRotateAndZoom extends PointerInteraction { const theta = Math.atan2(deltaY, deltaX); const magnitude = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const view = map.getView(); - if (view.getConstraints().rotation !== disable && this.lastAngle_ !== undefined) { - const angleDelta = theta - this.lastAngle_; - rotate(view, view.getRotation() - angleDelta); + if (this.lastAngle_ !== undefined) { + const angleDelta = this.lastAngle_ - theta; + view.adjustRotation(angleDelta); } this.lastAngle_ = theta; if (this.lastMagnitude_ !== undefined) { - const resolution = this.lastMagnitude_ * (view.getResolution() / magnitude); - zoom(view, resolution); + view.adjustResolution(this.lastMagnitude_ / magnitude); } if (this.lastMagnitude_ !== undefined) { this.lastScaleDelta_ = this.lastMagnitude_ / magnitude; diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index b2897c8658..c48921d7ca 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -111,80 +111,14 @@ export function pan(view, delta, opt_duration) { const currentCenter = view.getCenter(); if (currentCenter) { const center = [currentCenter[0] + delta[0], currentCenter[1] + delta[1]]; - if (opt_duration) { - view.animate({ - duration: opt_duration, - easing: linear, - center: center - }); - } else { - view.setCenter(center); - } + view.animate({ + duration: opt_duration !== undefined ? opt_duration : 250, + easing: linear, + center: center + }); } } - -/** - * @param {import("../View.js").default} view View. - * @param {number|undefined} rotation Rotation. - * @param {import("../coordinate.js").Coordinate=} opt_anchor Anchor coordinate. - * @param {number=} opt_duration Duration. - */ -export function rotate(view, rotation, opt_anchor, opt_duration) { - if (rotation !== undefined) { - const currentRotation = view.getRotation(); - const currentCenter = view.getCenter(); - if (currentRotation !== undefined && currentCenter && opt_duration > 0) { - view.animate({ - rotation: rotation, - anchor: opt_anchor, - duration: opt_duration, - easing: easeOut - }); - } else { - view.rotate(rotation, opt_anchor); - } - } -} - - -/** - * @param {import("../View.js").default} view View. - * @param {number|undefined} resolution Resolution to go to. - * @param {import("../coordinate.js").Coordinate=} opt_anchor Anchor coordinate. - * @param {number=} opt_duration Duration. - * @param {number=} opt_direction Zooming direction; > 0 indicates - * zooming out, in which case the constraints system will select - * the largest nearest resolution; < 0 indicates zooming in, in - * which case the constraints system will select the smallest - * nearest resolution; == 0 indicates that the zooming direction - * is unknown/not relevant, in which case the constraints system - * will select the nearest resolution. If not defined 0 is - * assumed. - */ -export function zoom(view, resolution, opt_anchor, opt_duration, opt_direction) { - if (resolution) { - const currentResolution = view.getResolution(); - const currentCenter = view.getCenter(); - if (currentResolution !== undefined && currentCenter && - resolution !== currentResolution && opt_duration) { - view.animate({ - resolution: resolution, - anchor: opt_anchor, - duration: opt_duration, - easing: easeOut - }); - } else { - if (opt_anchor) { - const center = view.calculateCenterZoom(resolution, opt_anchor); - view.setCenter(center); - } - view.setResolution(resolution); - } - } -} - - /** * @param {import("../View.js").default} view View. * @param {number} delta Delta from previous zoom level. @@ -201,23 +135,15 @@ export function zoomByDelta(view, delta, opt_anchor, opt_duration) { const newZoom = view.getValidZoomLevel(currentZoom + delta); const newResolution = view.getResolutionForZoom(newZoom); - if (opt_duration > 0) { - if (view.getAnimating()) { - view.cancelAnimations(); - } - view.animate({ - resolution: newResolution, - anchor: opt_anchor, - duration: opt_duration, - easing: easeOut - }); - } else { - if (opt_anchor) { - const center = view.calculateCenterZoom(newResolution, opt_anchor); - view.setCenter(center); - } - view.setResolution(newResolution); + if (view.getAnimating()) { + view.cancelAnimations(); } + view.animate({ + resolution: newResolution, + anchor: opt_anchor, + duration: opt_duration !== undefined ? opt_duration : 250, + easing: easeOut + }); } export default Interaction; diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 8ba3c68fd0..47c638464f 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -212,49 +212,7 @@ class MouseWheelZoom extends Interaction { view.beginInteraction(); } this.trackpadTimeoutId_ = setTimeout(this.decrementInteractingHint_.bind(this), this.trackpadEventGap_); - let resolution = view.getResolution() * Math.pow(2, delta / this.trackpadDeltaPerZoom_); - const minResolution = view.getMinResolution(); - const maxResolution = view.getMaxResolution(); - let rebound = 0; - if (resolution < minResolution) { - resolution = Math.max(resolution, minResolution / this.trackpadZoomBuffer_); - rebound = 1; - } else if (resolution > maxResolution) { - resolution = Math.min(resolution, maxResolution * this.trackpadZoomBuffer_); - rebound = -1; - } - if (this.lastAnchor_) { - const center = view.calculateCenterZoom(resolution, this.lastAnchor_); - view.setCenter(center); - } - view.setResolution(resolution); - - if (rebound === 0) { - const zoomDelta = delta > 0 ? -1 : 1; - const newZoom = view.getValidZoomLevel(view.getZoom() + zoomDelta); - view.animate({ - resolution: view.getResolutionForZoom(newZoom), - easing: easeOut, - anchor: this.lastAnchor_, - duration: this.duration_ - }); - } - - if (rebound > 0) { - view.animate({ - resolution: minResolution, - easing: easeOut, - anchor: this.lastAnchor_, - duration: 500 - }); - } else if (rebound < 0) { - view.animate({ - resolution: maxResolution, - easing: easeOut, - anchor: this.lastAnchor_, - duration: 500 - }); - } + view.adjustZoom(-delta / this.trackpadDeltaPerZoom_, this.lastAnchor_); this.startTime_ = now; return false; } diff --git a/src/ol/interaction/PinchRotate.js b/src/ol/interaction/PinchRotate.js index fba8c0ff96..f18652557a 100644 --- a/src/ol/interaction/PinchRotate.js +++ b/src/ol/interaction/PinchRotate.js @@ -118,9 +118,8 @@ class PinchRotate extends PointerInteraction { // rotate if (this.rotating_) { - const rotation = view.getRotation(); map.render(); - rotate(view, rotation + rotationDelta, this.anchor_); + view.adjustRotation(rotationDelta, this.anchor_); } } diff --git a/src/ol/interaction/PinchZoom.js b/src/ol/interaction/PinchZoom.js index b3c1e16c90..d3e0e9f6bd 100644 --- a/src/ol/interaction/PinchZoom.js +++ b/src/ol/interaction/PinchZoom.js @@ -83,17 +83,6 @@ class PinchZoom extends PointerInteraction { const map = mapBrowserEvent.map; const view = map.getView(); - const resolution = view.getResolution(); - const maxResolution = view.getMaxResolution(); - const minResolution = view.getMinResolution(); - let newResolution = resolution * scaleDelta; - if (newResolution > maxResolution) { - scaleDelta = maxResolution / resolution; - newResolution = maxResolution; - } else if (newResolution < minResolution) { - scaleDelta = minResolution / resolution; - newResolution = minResolution; - } if (scaleDelta != 1.0) { this.lastScaleDelta_ = scaleDelta; @@ -108,7 +97,7 @@ class PinchZoom extends PointerInteraction { // scale, bypass the resolution constraint map.render(); - zoom(view, newResolution, this.anchor_); + view.adjustResolution(scaleDelta, this.anchor_); } /** @@ -118,7 +107,7 @@ class PinchZoom extends PointerInteraction { if (this.targetPointers.length < 2) { const map = mapBrowserEvent.map; const view = map.getView(); - const direction = this.lastScaleDelta_ - 1; + const direction = this.lastScaleDelta_ > 1 ? 1 : -1; view.endInteraction(this.duration_, direction); return false; } else { diff --git a/test/spec/ol/interaction/dragrotateandzoom.test.js b/test/spec/ol/interaction/dragrotateandzoom.test.js index 7a9249182e..47bc94cab4 100644 --- a/test/spec/ol/interaction/dragrotateandzoom.test.js +++ b/test/spec/ol/interaction/dragrotateandzoom.test.js @@ -62,13 +62,18 @@ describe('ol.interaction.DragRotateAndZoom', function() { true); interaction.lastAngle_ = Math.PI; - let view = map.getView(); - let spy = sinon.spy(view, 'rotate'); - interaction.handleDragEvent(event); - expect(spy.callCount).to.be(1); - expect(interaction.lastAngle_).to.be(-0.8308214428190254); - view.rotate.restore(); + let callCount = 0; + let view = map.getView(); + view.on('change:rotation', function() { + callCount++; + }); + + interaction.handleDragEvent(event); + expect(callCount).to.be(1); + expect(interaction.lastAngle_).to.be(-0.8308214428190254); + + callCount = 0; view = new View({ projection: 'EPSG:4326', center: [0, 0], @@ -76,15 +81,16 @@ describe('ol.interaction.DragRotateAndZoom', function() { enableRotation: false }); map.setView(view); + view.on('change:rotation', function() { + callCount++; + }); event = new MapBrowserPointerEvent('pointermove', map, new PointerEvent('pointermove', {clientX: 24, clientY: 16}, {pointerType: 'mouse'}), true); - spy = sinon.spy(view, 'rotate'); interaction.handleDragEvent(event); - expect(spy.callCount).to.be(0); - view.rotate.restore(); + expect(callCount).to.be(0); }); }); diff --git a/test/spec/ol/interaction/interaction.test.js b/test/spec/ol/interaction/interaction.test.js index 5a6f14a703..7d752ab0c9 100644 --- a/test/spec/ol/interaction/interaction.test.js +++ b/test/spec/ol/interaction/interaction.test.js @@ -87,112 +87,4 @@ describe('ol.interaction.Interaction', function() { }); - describe('zoomByDelta()', function() { - - it('changes view resolution', function() { - const view = new View({ - resolution: 1, - resolutions: [4, 2, 1, 0.5, 0.25] - }); - - zoomByDelta(view, 1); - expect(view.getResolution()).to.be(0.5); - - zoomByDelta(view, -1); - expect(view.getResolution()).to.be(1); - - zoomByDelta(view, 2); - expect(view.getResolution()).to.be(0.25); - - zoomByDelta(view, -2); - expect(view.getResolution()).to.be(1); - }); - - it('changes view resolution and center relative to the anchor', function() { - const view = new View({ - center: [0, 0], - resolution: 1, - resolutions: [4, 2, 1, 0.5, 0.25] - }); - - zoomByDelta(view, 1, [10, 10]); - expect(view.getCenter()).to.eql([5, 5]); - - zoomByDelta(view, -1, [0, 0]); - expect(view.getCenter()).to.eql([10, 10]); - - zoomByDelta(view, 2, [0, 0]); - expect(view.getCenter()).to.eql([2.5, 2.5]); - - zoomByDelta(view, -2, [0, 0]); - expect(view.getCenter()).to.eql([10, 10]); - }); - - it('changes view resolution and center relative to the anchor, while respecting the extent (center only)', function() { - const view = new View({ - center: [0, 0], - extent: [-2.5, -2.5, 2.5, 2.5], - constrainOnlyCenter: true, - resolution: 1, - resolutions: [4, 2, 1, 0.5, 0.25] - }); - - zoomByDelta(view, 1, [10, 10]); - expect(view.getCenter()).to.eql([2.5, 2.5]); - - zoomByDelta(view, -1, [0, 0]); - expect(view.getCenter()).to.eql([2.5, 2.5]); - - zoomByDelta(view, 2, [10, 10]); - expect(view.getCenter()).to.eql([2.5, 2.5]); - - zoomByDelta(view, -2, [0, 0]); - expect(view.getCenter()).to.eql([2.5, 2.5]); - }); - - it('changes view resolution and center relative to the anchor, while respecting the extent', function() { - const map = new Map({}); - const view = new View({ - center: [50, 50], - extent: [0, 0, 100, 100], - resolution: 1, - resolutions: [4, 2, 1, 0.5, 0.25, 0.125] - }); - map.setView(view); - - zoomByDelta(view, 1, [100, 100]); - expect(view.getCenter()).to.eql([75, 75]); - - zoomByDelta(view, -1, [75, 75]); - expect(view.getCenter()).to.eql([50, 50]); - - zoomByDelta(view, 2, [100, 100]); - expect(view.getCenter()).to.eql([87.5, 87.5]); - - zoomByDelta(view, -3, [0, 0]); - expect(view.getCenter()).to.eql([50, 50]); - expect(view.getResolution()).to.eql(2); - }); - - it('changes view resolution and center relative to the anchor, while respecting the extent (rotated)', function() { - const map = new Map({}); - const view = new View({ - center: [50, 50], - extent: [-100, -100, 100, 100], - resolution: 1, - resolutions: [2, 1, 0.5, 0.25, 0.125], - rotation: Math.PI / 4 - }); - map.setView(view); - const halfSize = 100 * Math.SQRT1_2; - - zoomByDelta(view, 1, [100, 100]); - expect(view.getCenter()).to.eql([100 - halfSize / 2, 100 - halfSize / 2]); - - view.setCenter([0, 50]); - zoomByDelta(view, -1, [0, 0]); - expect(view.getCenter()).to.eql([0, 100 - halfSize]); - }); - }); - }); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 669a12ab7c..3b52a5ba89 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -8,6 +8,7 @@ import {createEmpty} from '../../../src/ol/extent.js'; import Circle from '../../../src/ol/geom/Circle.js'; import LineString from '../../../src/ol/geom/LineString.js'; import Point from '../../../src/ol/geom/Point.js'; +import {zoomByDelta} from '../../../src/ol/interaction/Interaction'; describe('ol.View', function() { @@ -1559,6 +1560,152 @@ describe('ol.View', function() { expect(view.getValidResolution(4, -1)).to.be(8); }); }); + + describe('#adjustRotation()', function() { + it('changes view rotation with anchor', function() { + const view = new View({ + resolution: 1, + center: [0, 0] + }); + + view.adjustRotation(Math.PI / 2); + expect(view.getRotation()).to.be(Math.PI / 2); + expect(view.getCenter()).to.eql([0, 0]); + + view.adjustRotation(-Math.PI); + expect(view.getRotation()).to.be(-Math.PI / 2); + expect(view.getCenter()).to.eql([0, 0]); + + view.adjustRotation(Math.PI / 3, [50, 0]); + expect(view.getRotation()).to.roughlyEqual(-Math.PI / 6, 1e-9); + expect(view.getCenter()[0]).to.roughlyEqual(50 * (1 - Math.cos(Math.PI / 3)), 1e-9); + expect(view.getCenter()[1]).to.roughlyEqual(-50 * Math.sin(Math.PI / 3), 1e-9); + }); + + it('does not change view parameters if rotation is disabled', function() { + const view = new View({ + resolution: 1, + enableRotation: false, + center: [0, 0] + }); + + view.adjustRotation(Math.PI / 2); + expect(view.getRotation()).to.be(0); + expect(view.getCenter()).to.eql([0, 0]); + + view.adjustRotation(-Math.PI * 3, [-50, 0]); + expect(view.getRotation()).to.be(0); + expect(view.getCenter()).to.eql([0, 0]); + }); + }); + + describe('#adjustZoom()', function() { + + it('changes view resolution', function() { + const view = new View({ + resolution: 1, + resolutions: [4, 2, 1, 0.5, 0.25] + }); + + view.adjustZoom(1); + expect(view.getResolution()).to.be(0.5); + + view.adjustZoom(-1); + expect(view.getResolution()).to.be(1); + + view.adjustZoom(2); + expect(view.getResolution()).to.be(0.25); + + view.adjustZoom(-2); + expect(view.getResolution()).to.be(1); + }); + + it('changes view resolution and center relative to the anchor', function() { + const view = new View({ + center: [0, 0], + resolution: 1, + resolutions: [4, 2, 1, 0.5, 0.25] + }); + + view.adjustZoom(1, [10, 10]); + expect(view.getCenter()).to.eql([5, 5]); + + view.adjustZoom(-1, [0, 0]); + expect(view.getCenter()).to.eql([10, 10]); + + view.adjustZoom(2, [0, 0]); + expect(view.getCenter()).to.eql([2.5, 2.5]); + + view.adjustZoom(-2, [0, 0]); + expect(view.getCenter()).to.eql([10, 10]); + }); + + it('changes view resolution and center relative to the anchor, while respecting the extent (center only)', function() { + const view = new View({ + center: [0, 0], + extent: [-2.5, -2.5, 2.5, 2.5], + constrainOnlyCenter: true, + resolution: 1, + resolutions: [4, 2, 1, 0.5, 0.25] + }); + + view.adjustZoom(1, [10, 10]); + expect(view.getCenter()).to.eql([2.5, 2.5]); + + view.adjustZoom(-1, [0, 0]); + expect(view.getCenter()).to.eql([2.5, 2.5]); + + view.adjustZoom(2, [10, 10]); + expect(view.getCenter()).to.eql([2.5, 2.5]); + + view.adjustZoom(-2, [0, 0]); + expect(view.getCenter()).to.eql([2.5, 2.5]); + }); + + it('changes view resolution and center relative to the anchor, while respecting the extent', function() { + const map = new Map({}); + const view = new View({ + center: [50, 50], + extent: [0, 0, 100, 100], + resolution: 1, + resolutions: [4, 2, 1, 0.5, 0.25, 0.125] + }); + map.setView(view); + + view.adjustZoom(1, [100, 100]); + expect(view.getCenter()).to.eql([75, 75]); + + view.adjustZoom(-1, [75, 75]); + expect(view.getCenter()).to.eql([50, 50]); + + view.adjustZoom(2, [100, 100]); + expect(view.getCenter()).to.eql([87.5, 87.5]); + + view.adjustZoom(-3, [0, 0]); + expect(view.getCenter()).to.eql([50, 50]); + expect(view.getResolution()).to.eql(1); + }); + + it('changes view resolution and center relative to the anchor, while respecting the extent (rotated)', function() { + const map = new Map({}); + const view = new View({ + center: [50, 50], + extent: [-100, -100, 100, 100], + resolution: 1, + resolutions: [2, 1, 0.5, 0.25, 0.125], + rotation: Math.PI / 4 + }); + map.setView(view); + const halfSize = 100 * Math.SQRT1_2; + + view.adjustZoom(1, [100, 100]); + expect(view.getCenter()).to.eql([100 - halfSize / 2, 100 - halfSize / 2]); + + view.setCenter([0, 50]); + view.adjustZoom(-1, [0, 0]); + expect(view.getCenter()).to.eql([0, 100 - halfSize]); + }); + }); }); describe('ol.View.isNoopAnimation()', function() { From ef6d17d8170cabef3aa9c29636fd2631978dfc59 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 20 Jan 2019 20:45:49 +0100 Subject: [PATCH 16/25] View / add a 'smoothResolutionConstraint' options When enabled (true by default), the resolution min/max values will be applied with a smoothing effect for a better user experience. --- src/ol/View.js | 25 +++--- src/ol/centerconstraint.js | 6 +- src/ol/control/ZoomSlider.js | 38 +++------ src/ol/interaction/MouseWheelZoom.js | 11 +-- src/ol/resolutionconstraint.js | 77 ++++++++++++++---- test/spec/ol/resolutionconstraint.test.js | 95 +++++++++++++++++++++++ 6 files changed, 187 insertions(+), 65 deletions(-) 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); + }); + }); + }); + }); From 433bccd207f6acba0ac5fdcb0eb6729178f9f520 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Tue, 15 Jan 2019 17:45:21 +0100 Subject: [PATCH 17/25] Linting and fixes for unit tests --- src/ol/View.js | 4 +-- src/ol/centerconstraint.js | 4 +-- src/ol/control/ZoomSlider.js | 1 - src/ol/interaction/DragPan.js | 3 +- src/ol/interaction/DragRotate.js | 2 -- src/ol/interaction/DragRotateAndZoom.js | 2 -- src/ol/interaction/MouseWheelZoom.js | 2 -- src/ol/interaction/PinchRotate.js | 2 -- src/ol/interaction/PinchZoom.js | 2 -- src/ol/resolutionconstraint.js | 4 +-- src/ol/rotationconstraint.js | 2 +- test/spec/ol/interaction/interaction.test.js | 3 +- test/spec/ol/resolutionconstraint.test.js | 12 +++---- test/spec/ol/view.test.js | 38 +++++++++++--------- 14 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index bdfffe3a7b..8c82d91ba7 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -934,7 +934,7 @@ class View extends BaseObject { */ getZoom() { let zoom; - const resolution = this.targetResolution_; + const resolution = this.getResolution(); if (resolution !== undefined) { zoom = this.getZoomForResolution(resolution); } @@ -1339,7 +1339,7 @@ class View extends BaseObject { const direction = opt_direction || 0; const size = this.getSizeFromViewport_(this.getRotation()); - return(this.constraints_.resolution(targetResolution, direction, size)); + return this.constraints_.resolution(targetResolution, direction, size); } } diff --git a/src/ol/centerconstraint.js b/src/ol/centerconstraint.js index 0aee0057d0..3eed6c6216 100644 --- a/src/ol/centerconstraint.js +++ b/src/ol/centerconstraint.js @@ -27,8 +27,8 @@ export function createExtent(extent, onlyCenter, smooth) { */ function(center, resolution, size, opt_isMoving) { if (center) { - let viewWidth = onlyCenter ? 0 : size[0] * resolution; - let viewHeight = onlyCenter ? 0 : size[1] * resolution; + const viewWidth = onlyCenter ? 0 : size[0] * resolution; + const viewHeight = onlyCenter ? 0 : size[1] * resolution; const minX = extent[0] + viewWidth / 2; const maxX = extent[2] - viewWidth / 2; const minY = extent[1] + viewHeight / 2; diff --git a/src/ol/control/ZoomSlider.js b/src/ol/control/ZoomSlider.js index 78d65fc07b..b9629b0cca 100644 --- a/src/ol/control/ZoomSlider.js +++ b/src/ol/control/ZoomSlider.js @@ -1,7 +1,6 @@ /** * @module ol/control/ZoomSlider */ -import ViewHint from '../ViewHint.js'; import Control from './Control.js'; import {CLASS_CONTROL, CLASS_UNSELECTABLE} from '../css.js'; import {easeOut} from '../easing.js'; diff --git a/src/ol/interaction/DragPan.js b/src/ol/interaction/DragPan.js index 6bf2fb820f..1058867947 100644 --- a/src/ol/interaction/DragPan.js +++ b/src/ol/interaction/DragPan.js @@ -1,8 +1,7 @@ /** * @module ol/interaction/DragPan */ -import ViewHint from '../ViewHint.js'; -import {scale as scaleCoordinate, rotate as rotateCoordinate, add as addCoordinate} from '../coordinate.js'; +import {scale as scaleCoordinate, rotate as rotateCoordinate} from '../coordinate.js'; import {easeOut} from '../easing.js'; import {noModifierKeys} from '../events/condition.js'; import {FALSE} from '../functions.js'; diff --git a/src/ol/interaction/DragRotate.js b/src/ol/interaction/DragRotate.js index c7c57fead9..d7fc11cfd6 100644 --- a/src/ol/interaction/DragRotate.js +++ b/src/ol/interaction/DragRotate.js @@ -2,10 +2,8 @@ * @module ol/interaction/DragRotate */ import {disable} from '../rotationconstraint.js'; -import ViewHint from '../ViewHint.js'; import {altShiftKeysOnly, mouseOnly, mouseActionButton} from '../events/condition.js'; import {FALSE} from '../functions.js'; -import {rotate} from './Interaction.js'; import PointerInteraction from './Pointer.js'; diff --git a/src/ol/interaction/DragRotateAndZoom.js b/src/ol/interaction/DragRotateAndZoom.js index ab229393fe..2a43740c03 100644 --- a/src/ol/interaction/DragRotateAndZoom.js +++ b/src/ol/interaction/DragRotateAndZoom.js @@ -1,8 +1,6 @@ /** * @module ol/interaction/DragRotateAndZoom */ -import {disable} from '../rotationconstraint.js'; -import ViewHint from '../ViewHint.js'; import {shiftKeyOnly, mouseOnly} from '../events/condition.js'; import PointerInteraction from './Pointer.js'; diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 33f5ac4290..a08e657956 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -1,9 +1,7 @@ /** * @module ol/interaction/MouseWheelZoom */ -import ViewHint from '../ViewHint.js'; import {always} from '../events/condition.js'; -import {easeOut} from '../easing.js'; import EventType from '../events/EventType.js'; import {DEVICE_PIXEL_RATIO, FIREFOX, SAFARI} from '../has.js'; import Interaction, {zoomByDelta} from './Interaction.js'; diff --git a/src/ol/interaction/PinchRotate.js b/src/ol/interaction/PinchRotate.js index f18652557a..c1143ba312 100644 --- a/src/ol/interaction/PinchRotate.js +++ b/src/ol/interaction/PinchRotate.js @@ -1,9 +1,7 @@ /** * @module ol/interaction/PinchRotate */ -import ViewHint from '../ViewHint.js'; import {FALSE} from '../functions.js'; -import {rotate} from './Interaction.js'; import PointerInteraction, {centroid as centroidFromPointers} from './Pointer.js'; import {disable} from '../rotationconstraint.js'; diff --git a/src/ol/interaction/PinchZoom.js b/src/ol/interaction/PinchZoom.js index d3e0e9f6bd..9d352bafe6 100644 --- a/src/ol/interaction/PinchZoom.js +++ b/src/ol/interaction/PinchZoom.js @@ -1,9 +1,7 @@ /** * @module ol/interaction/PinchZoom */ -import ViewHint from '../ViewHint.js'; import {FALSE} from '../functions.js'; -import {zoom} from './Interaction.js'; import PointerInteraction, {centroid as centroidFromPointers} from './Pointer.js'; diff --git a/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index 16384a3770..9b053da031 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -2,8 +2,8 @@ * @module ol/resolutionconstraint */ import {linearFindNearest} from './array.js'; -import {clamp} from './math.js'; import {getHeight, getWidth} from './extent'; +import {clamp} from './math'; /** @@ -123,7 +123,7 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_s const capped = Math.min(cappedMaxRes, resolution); const zoomLevel = Math.floor( Math.log(maxResolution / capped) / Math.log(power) + offset); - let newResolution = maxResolution / Math.pow(power, zoomLevel); + const newResolution = maxResolution / Math.pow(power, zoomLevel); return clamp(newResolution, minResolution, cappedMaxRes); } else { return undefined; diff --git a/src/ol/rotationconstraint.js b/src/ol/rotationconstraint.js index 67c5183869..3da582e459 100644 --- a/src/ol/rotationconstraint.js +++ b/src/ol/rotationconstraint.js @@ -71,7 +71,7 @@ export function createSnapToZero(opt_tolerance) { return ( /** * @param {number|undefined} rotation Rotation. - * @param {number} delta Delta. + * @param {boolean} opt_isMoving True if an interaction or animation is in progress. * @return {number|undefined} Rotation. */ function(rotation, opt_isMoving) { diff --git a/test/spec/ol/interaction/interaction.test.js b/test/spec/ol/interaction/interaction.test.js index 7d752ab0c9..b83f949edb 100644 --- a/test/spec/ol/interaction/interaction.test.js +++ b/test/spec/ol/interaction/interaction.test.js @@ -1,7 +1,6 @@ import Map from '../../../../src/ol/Map.js'; -import View from '../../../../src/ol/View.js'; import EventTarget from '../../../../src/ol/events/Target.js'; -import Interaction, {zoomByDelta} from '../../../../src/ol/interaction/Interaction.js'; +import Interaction from '../../../../src/ol/interaction/Interaction.js'; import {FALSE} from '../../../../src/ol/functions.js'; describe('ol.interaction.Interaction', function() { diff --git a/test/spec/ol/resolutionconstraint.test.js b/test/spec/ol/resolutionconstraint.test.js index 2ac0d8fb43..2fc2649d77 100644 --- a/test/spec/ol/resolutionconstraint.test.js +++ b/test/spec/ol/resolutionconstraint.test.js @@ -244,7 +244,7 @@ describe('ol.resolutionconstraint', function() { describe('snap to power, smooth constraint on', function() { it('returns expected resolution value', function() { - let resolutionConstraint = createSnapToPower(2, 128, 16, true); + const 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); @@ -261,7 +261,7 @@ describe('ol.resolutionconstraint', function() { describe('snap to power, smooth constraint off', function() { it('returns expected resolution value', function() { - let resolutionConstraint = createSnapToPower(2, 128, 16, false); + const 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); @@ -274,7 +274,7 @@ describe('ol.resolutionconstraint', function() { describe('snap to resolutions, smooth constraint on', function() { it('returns expected resolution value', function() { - let resolutionConstraint = createSnapToResolutions([128, 64, 32, 16], true); + const 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); @@ -291,7 +291,7 @@ describe('ol.resolutionconstraint', function() { describe('snap to resolutions, smooth constraint off', function() { it('returns expected resolution value', function() { - let resolutionConstraint = createSnapToResolutions([128, 64, 32, 16], false); + const 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); @@ -304,7 +304,7 @@ describe('ol.resolutionconstraint', function() { describe('min/max, smooth constraint on', function() { it('returns expected resolution value', function() { - let resolutionConstraint = createMinMaxResolution(128, 16, true); + const 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); @@ -321,7 +321,7 @@ describe('ol.resolutionconstraint', function() { describe('min/max, smooth constraint off', function() { it('returns expected resolution value', function() { - let resolutionConstraint = createMinMaxResolution(128, 16, false); + const 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); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 3b52a5ba89..6fe4ec1e01 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -8,7 +8,6 @@ import {createEmpty} from '../../../src/ol/extent.js'; import Circle from '../../../src/ol/geom/Circle.js'; import LineString from '../../../src/ol/geom/LineString.js'; import Point from '../../../src/ol/geom/Point.js'; -import {zoomByDelta} from '../../../src/ol/interaction/Interaction'; describe('ol.View', function() { @@ -989,7 +988,8 @@ describe('ol.View', function() { let view; beforeEach(function() { view = new View({ - resolutions: [512, 256, 128, 64, 32, 16] + resolutions: [1024, 512, 256, 128, 64, 32, 16, 8], + smoothResolutionConstraint: false }); }); @@ -998,30 +998,31 @@ describe('ol.View', function() { expect(view.getZoom()).to.be(undefined); view.setResolution(513); - expect(view.getZoom()).to.roughlyEqual(Math.log(512 / 513) / Math.LN2, 1e-9); + expect(view.getZoom()).to.roughlyEqual(Math.log(1024 / 513) / Math.LN2, 1e-9); view.setResolution(512); - expect(view.getZoom()).to.be(0); + expect(view.getZoom()).to.be(1); view.setResolution(100); - expect(view.getZoom()).to.roughlyEqual(2.35614, 1e-5); + expect(view.getZoom()).to.roughlyEqual(3.35614, 1e-5); view.setResolution(65); - expect(view.getZoom()).to.roughlyEqual(2.97763, 1e-5); + expect(view.getZoom()).to.roughlyEqual(3.97763, 1e-5); view.setResolution(64); - expect(view.getZoom()).to.be(3); + expect(view.getZoom()).to.be(4); view.setResolution(16); - expect(view.getZoom()).to.be(5); + expect(view.getZoom()).to.be(6); view.setResolution(15); - expect(view.getZoom()).to.roughlyEqual(Math.log(512 / 15) / Math.LN2, 1e-9); + expect(view.getZoom()).to.roughlyEqual(Math.log(1024 / 15) / Math.LN2, 1e-9); }); it('works for resolution arrays with variable zoom factors', function() { const view = new View({ - resolutions: [10, 5, 2, 1] + resolutions: [10, 5, 2, 1], + smoothResolutionConstraint: false }); view.setZoom(1); @@ -1046,7 +1047,8 @@ describe('ol.View', function() { it('returns correct zoom levels', function() { const view = new View({ minZoom: 10, - maxZoom: 20 + maxZoom: 20, + smoothResolutionConstraint: false }); view.setZoom(5); @@ -1122,14 +1124,16 @@ describe('ol.View', function() { describe('#getResolutionForZoom', function() { it('returns correct zoom resolution', function() { - const view = new View(); + const view = new View({ + smoothResolutionConstraint: false + }); const max = view.getMaxZoom(); const min = view.getMinZoom(); expect(view.getResolutionForZoom(max)).to.be(view.getMinResolution()); - expect(view.getResolutionForZoom(max + 1)).to.be(view.getMinResolution()); + expect(view.getResolutionForZoom(max + 1)).to.be(view.getMinResolution() / 2); expect(view.getResolutionForZoom(min)).to.be(view.getMaxResolution()); - expect(view.getResolutionForZoom(min - 1)).to.be(view.getMaxResolution()); + expect(view.getResolutionForZoom(min - 1)).to.be(view.getMaxResolution() * 2); }); it('returns correct zoom levels for specifically configured resolutions', function() { @@ -1295,10 +1299,10 @@ describe('ol.View', function() { let size = map.getView().getSizeFromViewport_(); expect(size).to.eql([200, 150]); size = map.getView().getSizeFromViewport_(Math.PI / 2); - expect(size[0]).to.roughlyEqual(150,1e-9); + expect(size[0]).to.roughlyEqual(150, 1e-9); expect(size[1]).to.roughlyEqual(200, 1e-9); size = map.getView().getSizeFromViewport_(Math.PI); - expect(size[0]).to.roughlyEqual(200,1e-9); + expect(size[0]).to.roughlyEqual(200, 1e-9); expect(size[1]).to.roughlyEqual(150, 1e-9); }); }); @@ -1481,7 +1485,7 @@ describe('ol.View', function() { describe('#beginInteraction() and endInteraction()', function() { let view; beforeEach(function() { - view = new View() + view = new View(); }); it('correctly changes the view hint', function() { From 78e8f23df5c48733aa0650a9aaa0b1ce341cfc52 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Tue, 29 Jan 2019 17:10:49 +0100 Subject: [PATCH 18/25] View / add getValidCenter method to improve interactions The DragPan, KeyboardPan and DragZoom interactions now make sure to animate to a valid center/resolution target to avoid a chained "resolve" animation which looks weird. The `View.fit` method was also fixed to use this. --- src/ol/View.js | 15 ++++++++++++++- src/ol/interaction/DragPan.js | 2 +- src/ol/interaction/DragZoom.js | 8 +++----- src/ol/interaction/Interaction.js | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 8c82d91ba7..2bfe5d9592 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -1069,7 +1069,7 @@ class View extends BaseObject { if (options.duration !== undefined) { this.animate({ resolution: resolution, - center: center, + center: this.getValidCenter(center, resolution), duration: options.duration, easing: options.easing }, callback); @@ -1312,6 +1312,19 @@ class View extends BaseObject { this.resolveConstraints_(opt_duration, opt_resolutionDirection); } + /** + * Get a valid position for the view center according to the current constraints. + * @param {import("./coordinate.js").Coordinate|undefined} targetCenter Target center position. + * @param {number=} opt_targetResolution Target resolution. If not supplied, the current one will be used. + * This is useful to guess a valid center position at a different zoom level. + * @return {import("./coordinate.js").Coordinate|undefined} Valid center position. + * @api + */ + getValidCenter(targetCenter, opt_targetResolution) { + const size = this.getSizeFromViewport_(this.getRotation()); + return this.constraints_.center(targetCenter, opt_targetResolution || this.getResolution(), size); + } + /** * Get a valid zoom level according to the current view constraints. * @param {number|undefined} targetZoom Target zoom. diff --git a/src/ol/interaction/DragPan.js b/src/ol/interaction/DragPan.js index 1058867947..ae31b38003 100644 --- a/src/ol/interaction/DragPan.js +++ b/src/ol/interaction/DragPan.js @@ -116,7 +116,7 @@ class DragPan extends PointerInteraction { centerpx[1] - distance * Math.sin(angle) ]); view.animate({ - center: dest, + center: view.getValidCenter(dest), duration: 500, easing: easeOut }); diff --git a/src/ol/interaction/DragZoom.js b/src/ol/interaction/DragZoom.js index 393db36660..2dcd12bdd5 100644 --- a/src/ol/interaction/DragZoom.js +++ b/src/ol/interaction/DragZoom.js @@ -80,13 +80,11 @@ function onBoxEnd() { extent = mapExtent; } - const resolution = view.getResolutionForExtent(extent, size); - const zoom = view.getValidZoomLevel(view.getZoomForResolution(resolution)); - - const center = getCenter(extent); + const resolution = view.getValidResolution(view.getResolutionForExtent(extent, size)); + const center = view.getValidCenter(getCenter(extent), resolution); view.animate({ - zoom: zoom, + resolution: resolution, center: center, duration: this.duration_, easing: easeOut diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index c48921d7ca..5834c5900b 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -114,7 +114,7 @@ export function pan(view, delta, opt_duration) { view.animate({ duration: opt_duration !== undefined ? opt_duration : 250, easing: linear, - center: center + center: view.getValidCenter(center) }); } } From 7835869582f290513cb27db6a8005f0d87acae50 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 30 Jan 2019 19:13:27 +0100 Subject: [PATCH 19/25] Add an extent restriction on the mapbox layer example This fixes a bug where the OL map would allow much larger resolution than mapbox. --- examples/mapbox-layer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/mapbox-layer.js b/examples/mapbox-layer.js index aa2fbadcf4..e717328572 100644 --- a/examples/mapbox-layer.js +++ b/examples/mapbox-layer.js @@ -203,7 +203,11 @@ const map = new Map({ target: 'map', view: new View({ center: [-10997148, 4569099], - zoom: 4 + zoom: 4, + minZoom: 1, + extent: [-Infinity, -20048966.10, Infinity, 20048966.10], + smoothExtentConstraint: false, + smoothResolutionConstraint: false }) }); From c2af03f1526d901cde1480e9c7abccb5e40f3079 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 31 Jan 2019 17:43:52 +0100 Subject: [PATCH 20/25] Update the View documentation & document breaking changes --- changelog/upgrade-notes.md | 18 +++++ examples/interaction-options.html | 15 ++-- examples/pinch-zoom.html | 2 +- src/ol/View.js | 122 +++++++++++++++++------------- 4 files changed, 95 insertions(+), 62 deletions(-) diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index b03d2f346a..b6efd1b675 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -4,6 +4,24 @@ #### Backwards incompatible changes +##### The `setCenter`, `setZoom`, `setResolution` and `setRotation` methods on `ol/View` do not bypass constraints anymore + +Previously, these methods allowed setting values that were inconsistent with the given view constraints. +This is no longer the case and all changes to the view state now follow the same logic: +target values are provided and constraints are applied on these to determine the actual values to be used. + +##### Removal of the `constrainResolution` option on `View.fit`, `PinchZoom`, `MouseWheelZoom` and `ol/interaction.js` + +The `constrainResolution` option is now only supported by the `View` class. A `View.setResolutionConstrained` method was added as well. + +Generally, the responsibility of applying center/rotation/resolutions constraints was moved from interactions and controls to the `View` class. + +##### The view `extent` option now applies to the whole viewport + +Previously, this options only constrained the view *center*. This behaviour can still be obtained by specifying `constrainCenterOnly` in the view options. + +As a side effect, the view `rotate` method is gone and has been replaced with `adjustRotation` which takes a delta as input. + ##### Removal of deprecated methods The `inherits` function that was used to inherit the prototype methods from one constructor into another has been removed. diff --git a/examples/interaction-options.html b/examples/interaction-options.html index 8c07ee8505..b0be6f0d4b 100644 --- a/examples/interaction-options.html +++ b/examples/interaction-options.html @@ -4,16 +4,11 @@ title: Interaction Options shortdesc: Shows interaction options for custom scroll and zoom behavior. docs: > This example uses a custom `ol/interaction/defaults` configuration: - - * By default, wheel/trackpad zoom and drag panning is always active, which - can be unexpected on pages with a lot of scrollable content and an embedded - map. To perform wheel/trackpad zoom and drag-pan actions only when the map - has the focus, set `onFocusOnly: true` as option. This requires a map div - with a `tabindex` attribute set. - * By default, pinch-zoom and wheel/trackpad zoom interactions can leave the - map at fractional zoom levels. If instead you want to constrain - wheel/trackpad zooming to integer zoom levels, set - `constrainResolution: true`. + by default, wheel/trackpad zoom and drag panning is always active, which + can be unexpected on pages with a lot of scrollable content and an embedded + map. To perform wheel/trackpad zoom and drag-pan actions only when the map + has the focus, set `onFocusOnly: true` as option. This requires a map div + with a `tabindex` attribute set. tags: "trackpad, mousewheel, zoom, scroll, interaction, fractional" ---
diff --git a/examples/pinch-zoom.html b/examples/pinch-zoom.html index ab61eef6fc..e955ad7dac 100644 --- a/examples/pinch-zoom.html +++ b/examples/pinch-zoom.html @@ -5,7 +5,7 @@ shortdesc: Restrict pinch zooming to integer zoom levels. docs: > By default, the `ol/interaction/PinchZoom` can leave the map at fractional zoom levels. If instead you want to constrain pinch zooming to integer zoom levels, set - constrainResolution: true when constructing the interaction. + constrainResolution: true when constructing the view. tags: "pinch, zoom, interaction" ---
diff --git a/src/ol/View.js b/src/ol/View.js index 2bfe5d9592..6a73338a3d 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -94,12 +94,12 @@ import {createMinMaxResolution} from './resolutionconstraint'; * used. The `constrainRotation` option has no effect if `enableRotation` is * `false`. * @property {import("./extent.js").Extent} [extent] The extent that constrains the - * 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. + * view, in other words, nothing outside of this extent can be visible on the map. + * @property {boolean} [constrainOnlyCenter=false] If true, the extent + * constraint will only apply to the view center and not the whole extent. + * @property {boolean} [smoothExtentConstraint=true] If true, the extent + * constraint will be applied smoothly, i.e. allow the view to go slightly outside + * of the given `extent`. * @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 @@ -120,12 +120,12 @@ import {createMinMaxResolution} from './resolutionconstraint'; * 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 + * @property {boolean} [constrainResolution=false] 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 + * intermediary zoom levels are allowed. + * @property {boolean} [smoothResolutionConstraint=true] 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. + * the given resolution or zoom bounds. * @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 @@ -139,10 +139,9 @@ import {createMinMaxResolution} from './resolutionconstraint'; * @property {number} [rotation=0] The initial rotation for the view in radians * (positive rotation clockwise, 0 means North). * @property {number} [zoom] Only used if `resolution` is not defined. Zoom - * level used to calculate the initial resolution for the view. The initial - * resolution is determined using the {@link #constrainResolution} method. - * @property {number} [zoomFactor=2] The zoom factor used to determine the - * resolution constraint. + * level used to calculate the initial resolution for the view. + * @property {number} [zoomFactor=2] The zoom factor used to compute the + * corresponding resolution. */ @@ -156,7 +155,7 @@ import {createMinMaxResolution} from './resolutionconstraint'; * of the animation. If `zoom` is also provided, this option will be ignored. * @property {number} [rotation] The rotation of the view at the end of * the animation. - * @property {import("./coordinate.js").Coordinate} [anchor] Optional anchor to remained fixed + * @property {import("./coordinate.js").Coordinate} [anchor] Optional anchor to remain fixed * during a rotation or resolution animation. * @property {number} [duration=1000] The duration of the animation in milliseconds. * @property {function(number):number} [easing] The easing function used @@ -197,7 +196,12 @@ const DEFAULT_MIN_ZOOM = 0; * and `rotation`. Each state has a corresponding getter and setter, e.g. * `getCenter` and `setCenter` for the `center` state. * - * An View has a `projection`. The projection determines the + * The `zoom` state is actually not saved on the view: all computations + * internally use the `resolution` state. Still, the `setZoom` and `getZoom` + * methods are available, as well as `getResolutionForZoom` and + * `getZoomForResolution` to switch from one system to the other. + * + * A View has a `projection`. The projection determines the * coordinate system of the center, and its units determine the units of the * resolution (projection units per pixel). The default projection is * Spherical Mercator (EPSG:3857). @@ -205,28 +209,19 @@ const DEFAULT_MIN_ZOOM = 0; * ### The constraints * * `setCenter`, `setResolution` and `setRotation` can be used to change the - * states of the view. Any value can be passed to the setters. And the value - * that is passed to a setter will effectively be the value set in the view, - * and returned by the corresponding getter. + * states of the view, but any constraint defined in the constructor will + * be applied along the way. * - * But a View object also has a *resolution constraint*, a - * *rotation constraint* and a *center constraint*. + * A View object can have a *resolution constraint*, a *rotation constraint* + * and a *center constraint*. * - * As said above, no constraints are applied when the setters are used to set - * new states for the view. Applying constraints is done explicitly through - * the use of the `constrain*` functions (`constrainResolution` and - * `constrainRotation` and `constrainCenter`). - * - * The main users of the constraints are the interactions and the - * controls. For example, double-clicking on the map changes the view to - * the "next" resolution. And releasing the fingers after pinch-zooming - * snaps to the closest resolution (with an animation). - * - * The *resolution constraint* snaps to specific resolutions. It is - * determined by the following options: `resolutions`, `maxResolution`, - * `maxZoom`, and `zoomFactor`. If `resolutions` is set, the other three - * options are ignored. See documentation for each option for more - * information. + * The *resolution constraint* typically restricts min/max values and + * snaps to specific resolutions. It is determined by the following + * options: `resolutions`, `maxResolution`, `maxZoom`, and `zoomFactor`. + * If `resolutions` is set, the other three options are ignored. See + * documentation for each option for more information. By default, the view + * only has a min/max restriction and allow intermediary zoom levels when + * pinch-zooming for example. * * The *rotation constraint* snaps to specific angles. It is determined * by the following options: `enableRotation` and `constrainRotation`. @@ -234,9 +229,31 @@ const DEFAULT_MIN_ZOOM = 0; * horizontal. * * The *center constraint* is determined by the `extent` option. By - * default the center is not constrained at all. + * default the view center is not constrained at all. * - * @api + * ### Changing the view state + * + * It is important to note that `setZoom`, `setResolution`, `setCenter` and + * `setRotation` are subject to the above mentioned constraints. As such, it + * may sometimes not be possible to know in advance the resulting state of the + * View. For example, calling `setResolution(10)` does not guarantee that + * `getResolution()` will return `10`. + * + * A consequence of this is that, when applying a delta on the view state, one + * should use `adjustCenter`, `adjustRotation`, `adjustZoom` and `adjustResolution` + * rather than the corresponding setters. This will let view do its internal + * computations. Besides, the `adjust*` methods also take an `opt_anchor` + * argument which allows specifying an origin for the transformation. + * + * ### Interacting with the view + * + * View constraints are usually only applied when the view is *at rest*, meaning that + * no interaction or animation is ongoing. As such, if the user puts the view in a + * state that is not equivalent to a constrained one (e.g. rotating the view when + * the snap angle is 0), an animation will be triggered at the interaction end to + * put back the view to a stable state; + * + * @api */ class View extends BaseObject { @@ -926,9 +943,9 @@ class View extends BaseObject { } /** - * Get the current zoom level. If you configured your view with a resolutions - * array (this is rare), this method may return non-integer zoom levels (so - * the zoom level is not safe to use as an index into a resolutions array). + * Get the current zoom level. This method may return non-integer zoom levels + * if the view does not constrain the resolution, or if an interaction or + * animation is underway. * @return {number|undefined} Zoom. * @api */ @@ -1115,7 +1132,7 @@ class View extends BaseObject { } /** - * Adds relative coordinates to the center of the view. + * Adds relative coordinates to the center of the view. Any extent constraint will apply. * @param {import("./coordinate.js").Coordinate} deltaCoordinates Relative value to add. * @api */ @@ -1125,7 +1142,8 @@ class View extends BaseObject { } /** - * Multiply the view resolution by a ratio, optionally using an anchor. + * Multiply the view resolution by a ratio, optionally using an anchor. Any resolution + * constraint will apply. * @param {number} ratio The ratio to apply on the view resolution. * @param {import("./coordinate.js").Coordinate=} opt_anchor The origin of the transformation. * @observable @@ -1145,7 +1163,8 @@ class View extends BaseObject { } /** - * Adds a value to the view zoom level, optionally using an anchor. + * Adds a value to the view zoom level, optionally using an anchor. Any resolution + * constraint will apply. * @param {number} delta Relative value to add to the zoom level. * @param {import("./coordinate.js").Coordinate=} opt_anchor The origin of the transformation. * @api @@ -1155,7 +1174,8 @@ class View extends BaseObject { } /** - * Adds a value to the view rotation, optionally using an anchor. + * Adds a value to the view rotation, optionally using an anchor. Any rotation + * constraint will apply. * @param {number} delta Relative value to add to the zoom rotation, in radians. * @param {import("./coordinate.js").Coordinate=} opt_anchor The rotation center. * @observable @@ -1172,7 +1192,7 @@ class View extends BaseObject { } /** - * Set the center of the current view. + * Set the center of the current view. Any extent constraint will apply. * @param {import("./coordinate.js").Coordinate|undefined} center The center of the view. * @observable * @api @@ -1194,7 +1214,7 @@ class View extends BaseObject { } /** - * Set the resolution for this view. + * Set the resolution for this view. Any resolution constraint will apply. * @param {number|undefined} resolution The resolution of the view. * @observable * @api @@ -1205,7 +1225,7 @@ class View extends BaseObject { } /** - * Set the rotation for this view using an anchor. + * Set the rotation for this view. Any rotation constraint will apply. * @param {number} rotation The rotation of the view in radians. * @observable * @api @@ -1216,7 +1236,7 @@ class View extends BaseObject { } /** - * Zoom to a specific zoom level using an anchor + * Zoom to a specific zoom level. Any resolution constrain will apply. * @param {number} zoom Zoom level. * @api */ @@ -1261,7 +1281,6 @@ class View extends BaseObject { * This is typically done on interaction end. * @param {number=} opt_duration The animation duration in ms. * @param {number=} opt_resolutionDirection Which direction to zoom. - * @observable * @private */ resolveConstraints_(opt_duration, opt_resolutionDirection) { @@ -1301,7 +1320,8 @@ class View extends BaseObject { } /** - * Notify the View that an interaction has ended. + * Notify the View that an interaction has ended. The view state will be resolved + * to a stable one if needed (depending on its constraints). * @param {number=} opt_duration Animation duration in ms. * @param {number=} opt_resolutionDirection Which direction to zoom. * @api From 405e206717292ae5c6d6550079d3d8e1582bbd7d Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Fri, 15 Feb 2019 13:43:50 +0100 Subject: [PATCH 21/25] View / better names for getValid* and applyParameters_ methods --- src/ol/View.js | 31 ++++++++--------- src/ol/control/Zoom.js | 2 +- src/ol/control/ZoomSlider.js | 2 +- src/ol/interaction/DragPan.js | 2 +- src/ol/interaction/DragZoom.js | 4 +-- src/ol/interaction/Interaction.js | 4 +-- test/spec/ol/interaction/dragzoom.test.js | 2 +- test/spec/ol/view.test.js | 42 +++++++++++------------ 8 files changed, 43 insertions(+), 46 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index 6a73338a3d..c5ae377bcd 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -622,7 +622,7 @@ class View extends BaseObject { } this.targetRotation_ = rotation; } - this.applyParameters_(true); + this.applyTargetState_(true); more = true; if (!animation.complete) { break; @@ -1070,7 +1070,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); - resolution = this.getValidResolution(resolution, nearest ? 0 : 1); + resolution = this.getConstrainedResolution(resolution, nearest ? 0 : 1); // calculate center sinAngle = -sinAngle; // go back to original rotation @@ -1086,14 +1086,14 @@ class View extends BaseObject { if (options.duration !== undefined) { this.animate({ resolution: resolution, - center: this.getValidCenter(center, resolution), + center: this.getConstrainedCenter(center, resolution), duration: options.duration, easing: options.easing }, callback); } else { this.targetResolution_ = resolution; this.targetCenter_ = center; - this.applyParameters_(false, true); + this.applyTargetState_(false, true); animationCallback(callback, true); } } @@ -1159,7 +1159,7 @@ class View extends BaseObject { } this.targetResolution_ *= ratio; - this.applyParameters_(); + this.applyTargetState_(); } /** @@ -1188,7 +1188,7 @@ class View extends BaseObject { this.targetCenter_ = this.calculateCenterRotate(newRotation, opt_anchor); } this.targetRotation_ += delta; - this.applyParameters_(); + this.applyTargetState_(); } /** @@ -1199,7 +1199,7 @@ class View extends BaseObject { */ setCenter(center) { this.targetCenter_ = center; - this.applyParameters_(); + this.applyTargetState_(); } /** @@ -1221,7 +1221,7 @@ class View extends BaseObject { */ setResolution(resolution) { this.targetResolution_ = resolution; - this.applyParameters_(); + this.applyTargetState_(); } /** @@ -1232,7 +1232,7 @@ class View extends BaseObject { */ setRotation(rotation) { this.targetRotation_ = rotation; - this.applyParameters_(); + this.applyTargetState_(); } /** @@ -1252,7 +1252,7 @@ class View extends BaseObject { * @param {boolean=} opt_forceMoving Apply constraints as if the view is moving. * @private */ - applyParameters_(opt_doNotCancelAnims, opt_forceMoving) { + applyTargetState_(opt_doNotCancelAnims, opt_forceMoving) { const isMoving = this.getAnimating() || this.getInteracting() || opt_forceMoving; // compute rotation @@ -1338,9 +1338,8 @@ class View extends BaseObject { * @param {number=} opt_targetResolution Target resolution. If not supplied, the current one will be used. * This is useful to guess a valid center position at a different zoom level. * @return {import("./coordinate.js").Coordinate|undefined} Valid center position. - * @api */ - getValidCenter(targetCenter, opt_targetResolution) { + getConstrainedCenter(targetCenter, opt_targetResolution) { const size = this.getSizeFromViewport_(this.getRotation()); return this.constraints_.center(targetCenter, opt_targetResolution || this.getResolution(), size); } @@ -1352,11 +1351,10 @@ class View extends BaseObject { * the available value respectively lower or greater than the target one. Leaving `0` will simply choose * the nearest available value. * @return {number|undefined} Valid zoom level. - * @api */ - getValidZoomLevel(targetZoom, opt_direction) { + getConstrainedZoom(targetZoom, opt_direction) { const targetRes = this.getResolutionForZoom(targetZoom); - return this.getZoomForResolution(this.getValidResolution(targetRes)); + return this.getZoomForResolution(this.getConstrainedResolution(targetRes)); } /** @@ -1366,9 +1364,8 @@ class View extends BaseObject { * the available value respectively lower or greater than the target one. Leaving `0` will simply choose * the nearest available value. * @return {number|undefined} Valid resolution. - * @api */ - getValidResolution(targetResolution, opt_direction) { + getConstrainedResolution(targetResolution, opt_direction) { const direction = opt_direction || 0; const size = this.getSizeFromViewport_(this.getRotation()); diff --git a/src/ol/control/Zoom.js b/src/ol/control/Zoom.js index f84254f904..379e703fc4 100644 --- a/src/ol/control/Zoom.js +++ b/src/ol/control/Zoom.js @@ -116,7 +116,7 @@ class Zoom extends Control { } const currentZoom = view.getZoom(); if (currentZoom !== undefined) { - const newZoom = view.getValidZoomLevel(currentZoom + delta); + const newZoom = view.getConstrainedZoom(currentZoom + delta); if (this.duration_ > 0) { if (view.getAnimating()) { view.cancelAnimations(); diff --git a/src/ol/control/ZoomSlider.js b/src/ol/control/ZoomSlider.js index b9629b0cca..050cb0d021 100644 --- a/src/ol/control/ZoomSlider.js +++ b/src/ol/control/ZoomSlider.js @@ -217,7 +217,7 @@ class ZoomSlider extends Control { event.offsetY - this.thumbSize_[1] / 2); const resolution = this.getResolutionForPosition_(relativePosition); - const zoom = view.getValidZoomLevel(view.getZoomForResolution(resolution)); + const zoom = view.getConstrainedZoom(view.getZoomForResolution(resolution)); view.animate({ zoom: zoom, diff --git a/src/ol/interaction/DragPan.js b/src/ol/interaction/DragPan.js index ae31b38003..cfcb9c1530 100644 --- a/src/ol/interaction/DragPan.js +++ b/src/ol/interaction/DragPan.js @@ -116,7 +116,7 @@ class DragPan extends PointerInteraction { centerpx[1] - distance * Math.sin(angle) ]); view.animate({ - center: view.getValidCenter(dest), + center: view.getConstrainedCenter(dest), duration: 500, easing: easeOut }); diff --git a/src/ol/interaction/DragZoom.js b/src/ol/interaction/DragZoom.js index 2dcd12bdd5..4050193eb1 100644 --- a/src/ol/interaction/DragZoom.js +++ b/src/ol/interaction/DragZoom.js @@ -80,8 +80,8 @@ function onBoxEnd() { extent = mapExtent; } - const resolution = view.getValidResolution(view.getResolutionForExtent(extent, size)); - const center = view.getValidCenter(getCenter(extent), resolution); + const resolution = view.getConstrainedResolution(view.getResolutionForExtent(extent, size)); + const center = view.getConstrainedCenter(getCenter(extent), resolution); view.animate({ resolution: resolution, diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index 5834c5900b..80a9644758 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -114,7 +114,7 @@ export function pan(view, delta, opt_duration) { view.animate({ duration: opt_duration !== undefined ? opt_duration : 250, easing: linear, - center: view.getValidCenter(center) + center: view.getConstrainedCenter(center) }); } } @@ -132,7 +132,7 @@ export function zoomByDelta(view, delta, opt_anchor, opt_duration) { return; } - const newZoom = view.getValidZoomLevel(currentZoom + delta); + const newZoom = view.getConstrainedZoom(currentZoom + delta); const newResolution = view.getResolutionForZoom(newZoom); if (view.getAnimating()) { diff --git a/test/spec/ol/interaction/dragzoom.test.js b/test/spec/ol/interaction/dragzoom.test.js index 77201d253e..9746ee25ca 100644 --- a/test/spec/ol/interaction/dragzoom.test.js +++ b/test/spec/ol/interaction/dragzoom.test.js @@ -103,7 +103,7 @@ describe('ol.interaction.DragZoom', function() { setTimeout(function() { const view = map.getView(); const resolution = view.getResolution(); - expect(resolution).to.eql(view.getValidResolution(0.5)); + expect(resolution).to.eql(view.getConstrainedResolution(0.5)); done(); }, 50); }, 50); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 6fe4ec1e01..3fc8c4e9ce 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -1499,14 +1499,14 @@ describe('ol.View', function() { }); }); - describe('#getValidZoomLevel()', function() { + describe('#getConstrainedZoom()', function() { let view; it('works correctly without constraint', function() { view = new View({ zoom: 0 }); - expect(view.getValidZoomLevel(3)).to.be(3); + expect(view.getConstrainedZoom(3)).to.be(3); }); it('works correctly with resolution constraints', function() { view = new View({ @@ -1514,28 +1514,28 @@ describe('ol.View', function() { minZoom: 4, maxZoom: 8 }); - expect(view.getValidZoomLevel(3)).to.be(4); - expect(view.getValidZoomLevel(10)).to.be(8); + expect(view.getConstrainedZoom(3)).to.be(4); + expect(view.getConstrainedZoom(10)).to.be(8); }); it('works correctly with a specific resolution set', function() { view = new View({ zoom: 0, resolutions: [512, 256, 128, 64, 32, 16, 8] }); - expect(view.getValidZoomLevel(0)).to.be(0); - expect(view.getValidZoomLevel(4)).to.be(4); - expect(view.getValidZoomLevel(8)).to.be(6); + expect(view.getConstrainedZoom(0)).to.be(0); + expect(view.getConstrainedZoom(4)).to.be(4); + expect(view.getConstrainedZoom(8)).to.be(6); }); }); - describe('#getValidResolution()', function() { + describe('#getConstrainedResolution()', function() { let view; const defaultMaxRes = 156543.03392804097; it('works correctly by snapping to power of 2', function() { view = new View(); - expect(view.getValidResolution(1000000)).to.be(defaultMaxRes); - expect(view.getValidResolution(defaultMaxRes / 8)).to.be(defaultMaxRes / 8); + expect(view.getConstrainedResolution(1000000)).to.be(defaultMaxRes); + expect(view.getConstrainedResolution(defaultMaxRes / 8)).to.be(defaultMaxRes / 8); }); it('works correctly by snapping to a custom zoom factor', function() { view = new View({ @@ -1544,11 +1544,11 @@ describe('ol.View', function() { maxZoom: 4, constrainResolution: true }); - expect(view.getValidResolution(90, 1)).to.be(100); - expect(view.getValidResolution(90, -1)).to.be(20); - expect(view.getValidResolution(20)).to.be(20); - expect(view.getValidResolution(5)).to.be(4); - expect(view.getValidResolution(1)).to.be(4); + expect(view.getConstrainedResolution(90, 1)).to.be(100); + expect(view.getConstrainedResolution(90, -1)).to.be(20); + expect(view.getConstrainedResolution(20)).to.be(20); + expect(view.getConstrainedResolution(5)).to.be(4); + expect(view.getConstrainedResolution(1)).to.be(4); }); it('works correctly with a specific resolution set', function() { view = new View({ @@ -1556,12 +1556,12 @@ describe('ol.View', function() { 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); - expect(view.getValidResolution(260)).to.be(256); - expect(view.getValidResolution(30)).to.be(32); - expect(view.getValidResolution(30, -1)).to.be(16); - expect(view.getValidResolution(4, -1)).to.be(8); + expect(view.getConstrainedResolution(1000, 1)).to.be(512); + expect(view.getConstrainedResolution(260, 1)).to.be(512); + expect(view.getConstrainedResolution(260)).to.be(256); + expect(view.getConstrainedResolution(30)).to.be(32); + expect(view.getConstrainedResolution(30, -1)).to.be(16); + expect(view.getConstrainedResolution(4, -1)).to.be(8); }); }); From 75c379beeb25d39a297ebb42ad77c1606718a33a Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Fri, 22 Feb 2019 14:54:18 +0100 Subject: [PATCH 22/25] View / allow specifying an anchor on interaction end This also fixes a bug where the animation anchor would be lost when outside the allowed resolution. --- src/ol/View.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index c5ae377bcd..6342888e37 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -497,7 +497,7 @@ class View extends BaseObject { if (options.center) { animation.sourceCenter = center; - animation.targetCenter = options.center; + animation.targetCenter = options.center.slice(); center = animation.targetCenter; } @@ -609,16 +609,20 @@ class View extends BaseObject { animation.targetResolution : animation.sourceResolution + progress * (animation.targetResolution - animation.sourceResolution); if (animation.anchor) { - this.targetCenter_ = this.calculateCenterZoom(resolution, animation.anchor); + const size = this.getSizeFromViewport_(this.getRotation()); + const constrainedResolution = this.constraints_.resolution(resolution, 0, size, true); + this.targetCenter_ = this.calculateCenterZoom(constrainedResolution, animation.anchor); } this.targetResolution_ = resolution; + this.applyTargetState_(true); } if (animation.sourceRotation !== undefined && animation.targetRotation !== undefined) { const rotation = progress === 1 ? modulo(animation.targetRotation + Math.PI, 2 * Math.PI) - Math.PI : animation.sourceRotation + progress * (animation.targetRotation - animation.sourceRotation); if (animation.anchor) { - this.targetCenter_ = this.calculateCenterRotate(rotation, animation.anchor); + const constrainedRotation = this.constraints_.rotation(rotation, true); + this.targetCenter_ = this.calculateCenterRotate(constrainedRotation, animation.anchor); } this.targetRotation_ = rotation; } @@ -1281,9 +1285,10 @@ class View extends BaseObject { * This is typically done on interaction end. * @param {number=} opt_duration The animation duration in ms. * @param {number=} opt_resolutionDirection Which direction to zoom. + * @param {import("./coordinate.js").Coordinate=} opt_anchor The origin of the transformation. * @private */ - resolveConstraints_(opt_duration, opt_resolutionDirection) { + resolveConstraints_(opt_duration, opt_resolutionDirection, opt_anchor) { const duration = opt_duration !== undefined ? opt_duration : 200; const direction = opt_resolutionDirection || 0; @@ -1306,7 +1311,8 @@ class View extends BaseObject { center: newCenter, resolution: newResolution, duration: duration, - easing: easeOut + easing: easeOut, + anchor: opt_anchor }); } } @@ -1324,12 +1330,13 @@ class View extends BaseObject { * to a stable one if needed (depending on its constraints). * @param {number=} opt_duration Animation duration in ms. * @param {number=} opt_resolutionDirection Which direction to zoom. + * @param {import("./coordinate.js").Coordinate=} opt_anchor The origin of the transformation. * @api */ - endInteraction(opt_duration, opt_resolutionDirection) { + endInteraction(opt_duration, opt_resolutionDirection, opt_anchor) { this.setHint(ViewHint.INTERACTING, -1); - this.resolveConstraints_(opt_duration, opt_resolutionDirection); + this.resolveConstraints_(opt_duration, opt_resolutionDirection, opt_anchor); } /** From f67baa0dc0b784337881a37c2b580c8f55465ddb Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Fri, 22 Feb 2019 15:00:57 +0100 Subject: [PATCH 23/25] Interactions / fix zoom level when a zoom interaction ends --- src/ol/interaction/DragRotateAndZoom.js | 2 +- src/ol/interaction/MouseWheelZoom.js | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/ol/interaction/DragRotateAndZoom.js b/src/ol/interaction/DragRotateAndZoom.js index 2a43740c03..3e750df327 100644 --- a/src/ol/interaction/DragRotateAndZoom.js +++ b/src/ol/interaction/DragRotateAndZoom.js @@ -109,7 +109,7 @@ class DragRotateAndZoom extends PointerInteraction { const map = mapBrowserEvent.map; const view = map.getView(); - const direction = this.lastScaleDelta_ - 1; + const direction = this.lastScaleDelta_ > 1 ? 1 : -1; view.endInteraction(this.duration_, direction); this.lastScaleDelta_ = 0; return false; diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index a08e657956..30548d2587 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -57,7 +57,13 @@ class MouseWheelZoom extends Interaction { * @private * @type {number} */ - this.delta_ = 0; + this.totalDelta_ = 0; + + /** + * @private + * @type {number} + */ + this.lastDelta_ = 0; /** * @private @@ -134,7 +140,7 @@ class MouseWheelZoom extends Interaction { endInteraction_() { this.trackpadTimeoutId_ = undefined; const view = this.getMap().getView(); - view.endInteraction(); + view.endInteraction(undefined, Math.sign(this.lastDelta_), this.lastAnchor_); } /** @@ -181,6 +187,8 @@ class MouseWheelZoom extends Interaction { if (delta === 0) { return false; + } else { + this.lastDelta_ = delta; } const now = Date.now(); @@ -208,7 +216,7 @@ class MouseWheelZoom extends Interaction { return false; } - this.delta_ += delta; + this.totalDelta_ += delta; const timeLeft = Math.max(this.timeout_ - (now - this.startTime_), 0); @@ -228,10 +236,10 @@ class MouseWheelZoom extends Interaction { view.cancelAnimations(); } const maxDelta = MAX_DELTA; - const delta = clamp(this.delta_, -maxDelta, maxDelta); + const delta = clamp(this.totalDelta_, -maxDelta, maxDelta); zoomByDelta(view, -delta, this.lastAnchor_, this.duration_); this.mode_ = undefined; - this.delta_ = 0; + this.totalDelta_ = 0; this.lastAnchor_ = null; this.startTime_ = undefined; this.timeoutId_ = undefined; From 447266cbe8914828edb21708ec977821bd4dd7f2 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 25 Feb 2019 23:12:39 +0100 Subject: [PATCH 24/25] Better smoothing of resolution and center constraints Previously the formula for the resolution constraint allowed going way past the minimum zoom. Also adjusted the center constraint to avoid a zigzag effect when going out of resolution bounds. --- src/ol/centerconstraint.js | 22 ++++++++++++++++------ src/ol/resolutionconstraint.js | 11 ++++++++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/ol/centerconstraint.js b/src/ol/centerconstraint.js index 3eed6c6216..1bf5480c6f 100644 --- a/src/ol/centerconstraint.js +++ b/src/ol/centerconstraint.js @@ -29,12 +29,22 @@ export function createExtent(extent, onlyCenter, smooth) { if (center) { const viewWidth = onlyCenter ? 0 : size[0] * resolution; const 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 = minX > maxX ? (maxX + minX) / 2 : clamp(center[0], minX, maxX); - let y = minY > maxY ? (maxY + minY) / 2 : clamp(center[1], minY, maxY); + let minX = extent[0] + viewWidth / 2; + let maxX = extent[2] - viewWidth / 2; + let minY = extent[1] + viewHeight / 2; + let maxY = extent[3] - viewHeight / 2; + + // note: when zooming out of bounds, min and max values for x and y may + // end up inverted (min > max); this has to be accounted for + if (minX > maxX) { + minX = maxX = (maxX + minX) / 2; + } + if (minY > maxY) { + minY = maxY = (maxY + minY) / 2; + } + + let x = clamp(center[0], minX, maxX); + let y = clamp(center[1], minY, maxY); const ratio = 30 * resolution; // during an interaction, allow some overscroll diff --git a/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index 9b053da031..2e12a1a69d 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -27,6 +27,10 @@ function getViewportClampedResolution(resolution, maxExtent, viewportSize) { /** * Returns a modified resolution to be between maxResolution and minResolution while * still allowing the value to be slightly out of bounds. + * Note: the computation is based on the logarithm function (ln): + * - at 1, ln(x) is 0 + * - above 1, ln(x) keeps increasing but at a much slower pace than x + * The final result is clamped to prevent getting too far away from bounds. * @param {number} resolution Resolution. * @param {number} maxResolution Max resolution. * @param {number} minResolution Min resolution. @@ -34,13 +38,14 @@ function getViewportClampedResolution(resolution, maxExtent, viewportSize) { */ function getSmoothClampedResolution(resolution, maxResolution, minResolution) { let result = Math.min(resolution, maxResolution); + const ratio = 50; - result *= Math.log(Math.max(1, resolution / maxResolution)) * 0.1 + 1; + result *= Math.log(1 + ratio * Math.max(0, resolution / maxResolution - 1)) / ratio + 1; if (minResolution) { result = Math.max(result, minResolution); - result /= Math.log(Math.max(1, minResolution / resolution)) * 0.1 + 1; + result /= Math.log(1 + ratio * Math.max(0, minResolution / resolution - 1)) / ratio + 1; } - return result; + return clamp(result, minResolution / 2, maxResolution * 2); } /** From 0995f95ef12a186ba270a1957ef137aba705fe52 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 4 Mar 2019 18:11:15 +0100 Subject: [PATCH 25/25] Fix typo in extent-constrained example Co-Authored-By: jahow --- examples/extent-constrained.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/extent-constrained.html b/examples/extent-constrained.html index 7c228d6c1b..c28d8d5120 100644 --- a/examples/extent-constrained.html +++ b/examples/extent-constrained.html @@ -3,7 +3,7 @@ layout: example.html title: Constrained Extent shortdesc: Example of a view with a constrained extent. docs: > - This map has a view that is constrained in an extent. This is done using the `extent` view option. Please note that specifying `restrictOnlyCenter: true` would only apply the extent restriction to the view center. + This map has a view that is constrained in an extent. This is done using the `extent` view option. Please note that specifying `constrainOnlyCenter: true` would only apply the extent restriction to the view center. tags: "view, extent, constrain, restrict" ---