diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index 1ba1139f5a..04559b5ef2 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/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/extent-constrained.html b/examples/extent-constrained.html new file mode 100644 index 0000000000..c28d8d5120 --- /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 `constrainOnlyCenter: 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/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/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/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 }) }); 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/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 0b33692e0c..6342888e37 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -21,6 +21,9 @@ 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'; +import {createMinMaxResolution} from './resolutionconstraint'; /** @@ -58,9 +61,8 @@ import Units from './proj/Units.js'; * @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. @@ -92,7 +94,12 @@ 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=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 @@ -113,6 +120,12 @@ import Units from './proj/Units.js'; * 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=false] If true, the view will always + * animate to the closest zoom level after an interaction; false means + * 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. * @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 @@ -126,10 +139,9 @@ import Units from './proj/Units.js'; * @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. */ @@ -143,7 +155,7 @@ import Units from './proj/Units.js'; * 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 @@ -184,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). @@ -192,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`. @@ -221,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 { @@ -262,6 +292,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 +323,6 @@ class View extends BaseObject { * @type {Object} */ const properties = {}; - properties[ViewProperty.CENTER] = options.center !== undefined ? - options.center : null; const resolutionConstraintInfo = createResolutionConstraint(options); @@ -324,19 +370,20 @@ 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_); - if (this.resolutions_) { // in case map zoom is out of min/max zoom range - properties[ViewProperty.RESOLUTION] = clamp( - Number(this.getResolution() || properties[ViewProperty.RESOLUTION]), - this.minResolution_, this.maxResolution_); + const resolution = this.getResolutionForZoom(options.zoom); + this.setResolution(clamp(resolution, + this.minResolution_, this.maxResolution_)); + } else { + this.setZoom(options.zoom); } } - properties[ViewProperty.ROTATION] = options.rotation !== undefined ? options.rotation : 0; + this.setProperties(properties); /** @@ -432,9 +479,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]); @@ -450,14 +497,13 @@ class View extends BaseObject { if (options.center) { animation.sourceCenter = center; - animation.targetCenter = options.center; + animation.targetCenter = options.center.slice(); center = animation.targetCenter; } 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; @@ -556,28 +602,31 @@ 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)); + const size = this.getSizeFromViewport_(this.getRotation()); + const constrainedResolution = this.constraints_.resolution(resolution, 0, size, true); + this.targetCenter_ = this.calculateCenterZoom(constrainedResolution, animation.anchor); } - this.set(ViewProperty.RESOLUTION, resolution); + 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.set(ViewProperty.CENTER, - this.calculateCenterRotate(rotation, animation.anchor)); + const constrainedRotation = this.constraints_.rotation(rotation, true); + this.targetCenter_ = this.calculateCenterRotate(constrainedRotation, animation.anchor); } - this.set(ViewProperty.ROTATION, rotation); + this.targetRotation_ = rotation; } + this.applyTargetState_(true); more = true; if (!animation.complete) { break; @@ -597,6 +646,10 @@ class View extends BaseObject { if (more && this.updateAnimationKey_ === undefined) { this.updateAnimationKey_ = requestAnimationFrame(this.updateAnimations_); } + + if (!this.getAnimating()) { + setTimeout(this.resolveConstraints_.bind(this), 0); + } } /** @@ -634,9 +687,10 @@ class View extends BaseObject { /** * @private + * @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_() { + getSizeFromViewport_(opt_rotation) { const size = [100, 100]; const selector = '.ol-viewport[data-view="' + getUid(this) + '"]'; const element = document.querySelector(selector); @@ -645,45 +699,15 @@ class View extends BaseObject { size[0] = parseInt(metrics.width, 10); size[1] = parseInt(metrics.height, 10); } + if (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; } - /** - * 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. - * @param {number=} opt_delta Delta. Default is `0`. - * @param {number=} opt_direction Direction. Default is `0`. - * @return {number|undefined} Constrained resolution. - * @api - */ - constrainResolution(resolution, opt_delta, opt_direction) { - const delta = opt_delta || 0; - const direction = opt_direction || 0; - 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. @@ -793,6 +817,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. @@ -914,9 +947,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 */ @@ -961,8 +994,16 @@ class View extends BaseObject { * @api */ getResolutionForZoom(zoom) { - return /** @type {number} */ (this.constrainResolution( - this.maxResolution_, zoom - this.minZoom_, 0)); + 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)); + } else { + return this.maxResolution_ / Math.pow(this.zoomFactor_, zoom - this.minZoom_); + } } /** @@ -998,15 +1039,12 @@ 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) { 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; } @@ -1036,14 +1074,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) { - let constrainedResolution = this.constrainResolution(resolution, 0, 0); - if (!nearest && constrainedResolution < resolution) { - constrainedResolution = this.constrainResolution( - constrainedResolution, -1, 0); - } - resolution = constrainedResolution; - } + resolution = this.getConstrainedResolution(resolution, nearest ? 0 : 1); // calculate center sinAngle = -sinAngle; // go back to original rotation @@ -1059,13 +1090,14 @@ class View extends BaseObject { if (options.duration !== undefined) { this.animate({ resolution: resolution, - center: center, + center: this.getConstrainedCenter(center, resolution), duration: options.duration, easing: options.easing }, callback); } else { - this.setResolution(resolution); - this.setCenter(center); + this.targetResolution_ = resolution; + this.targetCenter_ = center; + this.applyTargetState_(false, true); animationCallback(callback, true); } } @@ -1104,30 +1136,74 @@ 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. + * 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 */ - rotate(rotation, opt_anchor) { - if (opt_anchor !== undefined) { - const center = this.calculateCenterRotate(rotation, opt_anchor); - this.setCenter(center); - } - this.setRotation(rotation); + adjustCenter(deltaCoordinates) { + const center = this.targetCenter_; + this.setCenter([center[0] + deltaCoordinates[0], center[1] + deltaCoordinates[1]]); } /** - * Set the center of the current view. + * 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 + * @api + */ + 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) { + this.targetCenter_ = this.calculateCenterZoom(newResolution, opt_anchor); + } + + this.targetResolution_ *= ratio; + this.applyTargetState_(); + } + + /** + * 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 + */ + adjustZoom(delta, opt_anchor) { + this.adjustResolution(Math.pow(this.zoomFactor_, -delta), opt_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 + * @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.applyTargetState_(); + } + + /** + * 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 */ setCenter(center) { - this.set(ViewProperty.CENTER, center); - if (this.getAnimating()) { - this.cancelAnimations(); - } + this.targetCenter_ = center; + this.applyTargetState_(); } /** @@ -1142,39 +1218,166 @@ 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 */ setResolution(resolution) { - this.set(ViewProperty.RESOLUTION, resolution); - if (this.getAnimating()) { - this.cancelAnimations(); - } + this.targetResolution_ = resolution; + this.applyTargetState_(); } /** - * Set the rotation for this view. + * Set the rotation for this view. Any rotation constraint will apply. * @param {number} rotation The rotation of the view in radians. * @observable * @api */ setRotation(rotation) { - this.set(ViewProperty.ROTATION, rotation); - if (this.getAnimating()) { - this.cancelAnimations(); - } + this.targetRotation_ = rotation; + this.applyTargetState_(); } /** - * Zoom to a specific zoom level. + * Zoom to a specific zoom level. Any resolution constrain will apply. * @param {number} zoom Zoom level. * @api */ setZoom(zoom) { this.setResolution(this.getResolutionForZoom(zoom)); } + + /** + * 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 + */ + applyTargetState_(opt_doNotCancelAnims, opt_forceMoving) { + const isMoving = this.getAnimating() || this.getInteracting() || opt_forceMoving; + + // compute rotation + 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); + + 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(); + } + } + + /** + * 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. + * @param {import("./coordinate.js").Coordinate=} opt_anchor The origin of the transformation. + * @private + */ + resolveConstraints_(opt_duration, opt_resolutionDirection, opt_anchor) { + const duration = opt_duration !== undefined ? opt_duration : 200; + 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 || + !this.getCenter() || + !equals(this.getCenter(), newCenter)) { + + if (this.getAnimating()) { + this.cancelAnimations(); + } + + this.animate({ + rotation: newRotation, + center: newCenter, + resolution: newResolution, + duration: duration, + easing: easeOut, + anchor: opt_anchor + }); + } + } + + /** + * Notify the View that an interaction has started. + * @api + */ + beginInteraction() { + this.setHint(ViewHint.INTERACTING, 1); + } + + /** + * 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. + * @param {import("./coordinate.js").Coordinate=} opt_anchor The origin of the transformation. + * @api + */ + endInteraction(opt_duration, opt_resolutionDirection, opt_anchor) { + this.setHint(ViewHint.INTERACTING, -1); + + this.resolveConstraints_(opt_duration, opt_resolutionDirection, opt_anchor); + } + + /** + * 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. + */ + getConstrainedCenter(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. + * @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. + */ + getConstrainedZoom(targetZoom, opt_direction) { + const targetRes = this.getResolutionForZoom(targetZoom); + return this.getZoomForResolution(this.getConstrainedResolution(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. + */ + getConstrainedResolution(targetResolution, opt_direction) { + const direction = opt_direction || 0; + const size = this.getSizeFromViewport_(this.getRotation()); + + return this.constraints_.resolution(targetResolution, direction, size); + } } @@ -1195,7 +1398,8 @@ function animationCallback(callback, returnValue) { */ export function createCenterConstraint(options) { if (options.extent !== undefined) { - return createExtent(options.extent); + return createExtent(options.extent, options.constrainOnlyCenter, + options.smoothExtentConstraint !== undefined ? options.smoothExtentConstraint : true); } else { return centerNone; } @@ -1226,13 +1430,22 @@ 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]; minResolution = resolutions[maxZoom] !== undefined ? resolutions[maxZoom] : resolutions[resolutions.length - 1]; - resolutionConstraint = createSnapToResolutions( - resolutions); + + if (options.constrainResolution) { + resolutionConstraint = createSnapToResolutions(resolutions, smooth, + !options.constrainOnlyCenter && options.extent); + } else { + resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, smooth, + !options.constrainOnlyCenter && options.extent); + } } else { // calculate the default min and max resolution const projection = createProjection(options.projection, 'EPSG:3857'); @@ -1276,8 +1489,14 @@ export function createResolutionConstraint(options) { Math.log(maxResolution / minResolution) / Math.log(zoomFactor)); minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom); - resolutionConstraint = createSnapToPower( - zoomFactor, maxResolution, maxZoom - minZoom); + if (options.constrainResolution) { + resolutionConstraint = createSnapToPower( + zoomFactor, maxResolution, minResolution, smooth, + !options.constrainOnlyCenter && options.extent); + } else { + resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, smooth, + !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 3d24463c8b..1bf5480c6f 100644 --- a/src/ol/centerconstraint.js +++ b/src/ol/centerconstraint.js @@ -5,26 +5,57 @@ 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. + * @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) { +export function createExtent(extent, onlyCenter, smooth) { 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) { + function(center, resolution, size, opt_isMoving) { if (center) { - return [ - clamp(center[0], extent[0], extent[2]), - clamp(center[1], extent[1], extent[3]) - ]; + const viewWidth = onlyCenter ? 0 : size[0] * resolution; + const viewHeight = onlyCenter ? 0 : size[1] * resolution; + 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 + 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/control/Zoom.js b/src/ol/control/Zoom.js index 5e016950a9..379e703fc4 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.getConstrainedZoom(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 fa0b3560cb..050cb0d021 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'; @@ -102,13 +101,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_ @@ -218,9 +217,10 @@ class ZoomSlider extends Control { event.offsetY - this.thumbSize_[1] / 2); const resolution = this.getResolutionForPosition_(relativePosition); + const zoom = view.getConstrainedZoom(view.getZoomForResolution(resolution)); view.animate({ - resolution: view.constrainResolution(resolution), + zoom: zoom, duration: this.duration_, easing: easeOut }); @@ -233,9 +233,10 @@ class ZoomSlider extends Control { */ handleDraggerStart_(event) { if (!this.dragging_ && event.originalEvent.target === this.element.firstElementChild) { - this.getMap().getView().setHint(ViewHint.INTERACTING, 1); - this.previousX_ = event.clientX; - this.previousY_ = event.clientY; + const element = /** @type {HTMLElement} */ (this.element.firstElementChild); + this.getMap().getView().beginInteraction(); + this.startX_ = event.clientX - parseFloat(element.style.left); + this.startY_ = event.clientY - parseFloat(element.style.top); this.dragging_ = true; if (this.dragListenerKeys_.length === 0) { @@ -259,15 +260,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; } } @@ -279,17 +276,11 @@ class ZoomSlider extends Control { handleDraggerEnd_(event) { if (this.dragging_) { const view = this.getMap().getView(); - view.setHint(ViewHint.INTERACTING, -1); - - view.animate({ - resolution: view.constrainResolution(this.currentResolution_), - duration: this.duration_, - easing: easeOut - }); + view.endInteraction(); this.dragging_ = false; - this.previousX_ = undefined; - this.previousY_ = undefined; + this.startX_ = undefined; + this.startY_ = undefined; this.dragListenerKeys_.forEach(unlistenByKey); this.dragListenerKeys_.length = 0; } @@ -356,7 +347,7 @@ class ZoomSlider extends Control { */ getPositionForResolution_(res) { const fn = this.getMap().getView().getValueForResolutionFunction(); - return 1 - fn(res); + return clamp(1 - fn(res), 0, 1); } } @@ -375,10 +366,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.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/DragPan.js b/src/ol/interaction/DragPan.js index d889830f33..cfcb9c1530 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'; @@ -74,10 +73,6 @@ class DragPan extends PointerInteraction { * @inheritDoc */ handleDragEvent(mapBrowserEvent) { - if (!this.panning_) { - this.panning_ = true; - this.getMap().getView().setHint(ViewHint.INTERACTING, 1); - } const targetPointers = this.targetPointers; const centroid = centroidFromPointers(targetPointers); if (targetPointers.length == this.lastPointersCount_) { @@ -85,16 +80,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()); - center = view.constrainCenter(center); - 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 @@ -122,14 +116,14 @@ class DragPan extends PointerInteraction { centerpx[1] - distance * Math.sin(angle) ]); view.animate({ - center: view.constrainCenter(dest), + center: view.getConstrainedCenter(dest), duration: 500, easing: easeOut }); } if (this.panning_) { this.panning_ = false; - view.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(); } return false; } else { @@ -153,7 +147,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 68995ccf08..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, rotateWithoutConstraints} from './Interaction.js'; import PointerInteraction from './Pointer.js'; @@ -80,8 +78,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(); - rotateWithoutConstraints(view, rotation - delta); + view.adjustRotation(-delta); } this.lastAngle_ = theta; } @@ -97,9 +94,7 @@ class DragRotate extends PointerInteraction { const map = mapBrowserEvent.map; const view = map.getView(); - view.setHint(ViewHint.INTERACTING, -1); - const rotation = view.getRotation(); - rotate(view, rotation, undefined, this.duration_); + view.endInteraction(this.duration_); return false; } @@ -114,7 +109,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..3e750df327 100644 --- a/src/ol/interaction/DragRotateAndZoom.js +++ b/src/ol/interaction/DragRotateAndZoom.js @@ -1,10 +1,7 @@ /** * @module ol/interaction/DragRotateAndZoom */ -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 PointerInteraction from './Pointer.js'; @@ -88,14 +85,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_; - rotateWithoutConstraints(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); - zoomWithoutConstraints(view, resolution); + view.adjustResolution(this.lastMagnitude_ / magnitude); } if (this.lastMagnitude_ !== undefined) { this.lastScaleDelta_ = this.lastMagnitude_ / magnitude; @@ -113,10 +109,8 @@ class DragRotateAndZoom extends PointerInteraction { const map = mapBrowserEvent.map; const view = map.getView(); - view.setHint(ViewHint.INTERACTING, -1); - const direction = this.lastScaleDelta_ - 1; - rotate(view, view.getRotation()); - zoom(view, view.getResolution(), undefined, this.duration_, direction); + const direction = this.lastScaleDelta_ > 1 ? 1 : -1; + view.endInteraction(this.duration_, direction); this.lastScaleDelta_ = 0; return false; } @@ -130,7 +124,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/DragZoom.js b/src/ol/interaction/DragZoom.js index 83b70c5b47..4050193eb1 100644 --- a/src/ol/interaction/DragZoom.js +++ b/src/ol/interaction/DragZoom.js @@ -80,11 +80,8 @@ function onBoxEnd() { extent = mapExtent; } - const resolution = view.constrainResolution( - view.getResolutionForExtent(extent, size)); - - let center = getCenter(extent); - center = view.constrainCenter(center); + 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 6a13a03857..80a9644758 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'; /** @@ -111,77 +110,15 @@ 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]]); - if (opt_duration) { - view.animate({ - duration: opt_duration, - easing: linear, - center: center - }); - } else { - view.setCenter(center); - } + const center = [currentCenter[0] + delta[0], currentCenter[1] + delta[1]]; + view.animate({ + duration: opt_duration !== undefined ? opt_duration : 250, + easing: linear, + center: view.getConstrainedCenter(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) { - 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(); - 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) { - resolution = view.constrainResolution(resolution, 0, opt_direction); - zoomWithoutConstraints(view, resolution, opt_anchor, opt_duration); -} - - /** * @param {import("../View.js").default} view View. * @param {number} delta Delta from previous zoom level. @@ -189,63 +126,24 @@ 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 currentResolution = view.getResolution(); - let resolution = view.constrainResolution(currentResolution, delta, 0); + const currentZoom = view.getZoom(); - if (resolution !== undefined) { - const resolutions = view.getResolutions(); - resolution = clamp( - resolution, - view.getMinResolution() || resolutions[resolutions.length - 1], - view.getMaxResolution() || resolutions[0]); + if (currentZoom === undefined) { + return; } - // 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) { - const currentCenter = view.getCenter(); - let center = view.calculateCenterZoom(resolution, opt_anchor); - center = view.constrainCenter(center); + const newZoom = view.getConstrainedZoom(currentZoom + delta); + const newResolution = view.getResolutionForZoom(newZoom); - opt_anchor = [ - (resolution * currentCenter[0] - currentResolution * center[0]) / - (resolution - currentResolution), - (resolution * currentCenter[1] - currentResolution * center[1]) / - (resolution - currentResolution) - ]; - } - - zoomWithoutConstraints(view, resolution, 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); - } + 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 726ee1ba62..30548d2587 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'; @@ -34,9 +32,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. @@ -62,7 +57,13 @@ class MouseWheelZoom extends Interaction { * @private * @type {number} */ - this.delta_ = 0; + this.totalDelta_ = 0; + + /** + * @private + * @type {number} + */ + this.lastDelta_ = 0; /** * @private @@ -82,12 +83,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} @@ -137,22 +132,15 @@ 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.setHint(ViewHint.INTERACTING, -1); + view.endInteraction(undefined, Math.sign(this.lastDelta_), this.lastAnchor_); } /** @@ -199,6 +187,8 @@ class MouseWheelZoom extends Interaction { if (delta === 0) { return false; + } else { + this.lastDelta_ = delta; } const now = Date.now(); @@ -218,55 +208,15 @@ class MouseWheelZoom extends Interaction { if (this.trackpadTimeoutId_) { clearTimeout(this.trackpadTimeoutId_); } else { - view.setHint(ViewHint.INTERACTING, 1); - } - 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(view.constrainCenter(center)); - } - view.setResolution(resolution); - - if (rebound === 0 && this.constrainResolution_) { - view.animate({ - resolution: view.constrainResolution(resolution, delta > 0 ? -1 : 1), - 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.beginInteraction(); } + this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.trackpadEventGap_); + view.adjustZoom(-delta / this.trackpadDeltaPerZoom_, this.lastAnchor_); this.startTime_ = now; return false; } - this.delta_ += delta; + this.totalDelta_ += delta; const timeLeft = Math.max(this.timeout_ - (now - this.startTime_), 0); @@ -286,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; diff --git a/src/ol/interaction/PinchRotate.js b/src/ol/interaction/PinchRotate.js index 82f8c254e5..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, rotateWithoutConstraints} from './Interaction.js'; import PointerInteraction, {centroid as centroidFromPointers} from './Pointer.js'; import {disable} from '../rotationconstraint.js'; @@ -118,9 +116,8 @@ class PinchRotate extends PointerInteraction { // rotate if (this.rotating_) { - const rotation = view.getRotation(); map.render(); - rotateWithoutConstraints(view, rotation + rotationDelta, this.anchor_); + view.adjustRotation(rotationDelta, this.anchor_); } } @@ -131,11 +128,7 @@ class PinchRotate extends PointerInteraction { if (this.targetPointers.length < 2) { const map = mapBrowserEvent.map; const view = map.getView(); - view.setHint(ViewHint.INTERACTING, -1); - if (this.rotating_) { - const rotation = view.getRotation(); - rotate(view, rotation, this.anchor_, this.duration_); - } + view.endInteraction(this.duration_); return false; } else { return true; @@ -153,7 +146,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..9d352bafe6 100644 --- a/src/ol/interaction/PinchZoom.js +++ b/src/ol/interaction/PinchZoom.js @@ -1,17 +1,13 @@ /** * @module ol/interaction/PinchZoom */ -import ViewHint from '../ViewHint.js'; import {FALSE} from '../functions.js'; -import {zoom, zoomWithoutConstraints} from './Interaction.js'; 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 +33,6 @@ class PinchZoom extends PointerInteraction { super(pointerOptions); - /** - * @private - * @type {boolean} - */ - this.constrainResolution_ = options.constrainResolution || false; - /** * @private * @type {import("../coordinate.js").Coordinate} @@ -91,17 +81,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; @@ -116,7 +95,7 @@ class PinchZoom extends PointerInteraction { // scale, bypass the resolution constraint map.render(); - zoomWithoutConstraints(view, newResolution, this.anchor_); + view.adjustResolution(scaleDelta, this.anchor_); } /** @@ -126,17 +105,8 @@ class PinchZoom extends PointerInteraction { if (this.targetPointers.length < 2) { const map = mapBrowserEvent.map; const view = map.getView(); - view.setHint(ViewHint.INTERACTING, -1); - 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 ? 1 : -1; + view.endInteraction(this.duration_, direction); return false; } else { return true; @@ -153,7 +123,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/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index 8e85618832..2e12a1a69d 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -2,37 +2,87 @@ * @module ol/resolutionconstraint */ 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): (number|undefined)} Type + * @typedef {function((number|undefined), number, import("./size.js").Size, boolean=): (number|undefined)} Type */ +/** + * 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 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. + * 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. + * @return {number} Smoothed resolution. + */ +function getSmoothClampedResolution(resolution, maxResolution, minResolution) { + let result = Math.min(resolution, maxResolution); + const ratio = 50; + + result *= Math.log(1 + ratio * Math.max(0, resolution / maxResolution - 1)) / ratio + 1; + if (minResolution) { + result = Math.max(result, minResolution); + result /= Math.log(1 + ratio * Math.max(0, minResolution / resolution - 1)) / ratio + 1; + } + return clamp(result, minResolution / 2, maxResolution * 2); +} /** * @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) { +export function createSnapToResolutions(resolutions, opt_smooth, 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) { + function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { - let z = linearFindNearest(resolutions, resolution, 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]; + 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 smooth = opt_smooth !== undefined ? opt_smooth : true; + if (!smooth) { + return clamp(resolution, minResolution, cappedMaxRes); + } + return getSmoothClampedResolution(resolution, cappedMaxRes, minResolution); } + + const capped = Math.min(cappedMaxRes, resolution); + const z = Math.floor(linearFindNearest(resolutions, capped, direction)); + return resolutions[z]; } else { return undefined; } @@ -44,29 +94,78 @@ export function createSnapToResolutions(resolutions) { /** * @param {number} power Power. * @param {number} maxResolution Maximum resolution. - * @param {number=} opt_maxLevel Maximum level. + * @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_maxLevel) { +export function createSnapToPower(power, maxResolution, opt_minResolution, opt_smooth, 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) { + function(resolution, direction, size, opt_isMoving) { if (resolution !== undefined) { - const offset = -direction / 2 + 0.5; - const oldLevel = Math.floor( - Math.log(maxResolution / resolution) / Math.log(power) + offset); - let newLevel = Math.max(oldLevel + delta, 0); - if (opt_maxLevel !== undefined) { - newLevel = Math.min(newLevel, opt_maxLevel); + 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) { + const smooth = opt_smooth !== undefined ? opt_smooth : true; + if (!smooth) { + return clamp(resolution, minResolution, cappedMaxRes); + } + return getSmoothClampedResolution(resolution, cappedMaxRes, minResolution); } - return maxResolution / Math.pow(power, newLevel); + + const offset = -direction * (0.5 - 1e-9) + 0.5; + const capped = Math.min(cappedMaxRes, resolution); + const zoomLevel = Math.floor( + Math.log(maxResolution / capped) / Math.log(power) + offset); + const newResolution = maxResolution / Math.pow(power, zoomLevel); + return clamp(newResolution, minResolution, cappedMaxRes); } else { return undefined; } }); } + +/** + * @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_smooth, 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 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/src/ol/rotationconstraint.js b/src/ol/rotationconstraint.js index 6abc294e42..3da582e459 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): (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,12 +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) { + 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; @@ -69,15 +71,19 @@ 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, delta) { + 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/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/dragzoom.test.js b/test/spec/ol/interaction/dragzoom.test.js index de9f3ec6af..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.constrainResolution(0.5)); + expect(resolution).to.eql(view.getConstrainedResolution(0.5)); done(); }, 50); }, 50); diff --git a/test/spec/ol/interaction/interaction.test.js b/test/spec/ol/interaction/interaction.test.js index 000a575eda..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() { @@ -87,67 +86,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', function() { - const view = new View({ - center: [0, 0], - extent: [-2.5, -2.5, 2.5, 2.5], - 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]); - }); - }); - }); 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/resolutionconstraint.test.js b/test/spec/ol/resolutionconstraint.test.js index c48ea0fe10..2fc2649d77 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() { @@ -12,30 +13,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 +51,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 +97,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,88 +155,182 @@ 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); }); }); }); + + describe('SnapToPower smooth constraint', function() { + + describe('snap to power, smooth constraint on', function() { + it('returns expected resolution value', function() { + 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); + 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() { + 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); + 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() { + 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); + 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() { + 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); + 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() { + 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); + 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() { + 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); + 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); + }); + }); + }); + }); 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); }); }); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 321c69667c..3fc8c4e9ce 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -42,15 +42,36 @@ describe('ol.View', function() { }); }); + describe('with extent option and center only', function() { + it('gives a correct center constraint function', function() { + const options = { + extent: [0, 0, 1, 1], + constrainOnlyCenter: true + }; + const fn = createCenterConstraint(options); + expect(fn([0, 0])).to.eql([0, 0]); + expect(fn([-10, 0])).to.eql([0, 0]); + expect(fn([100, 100])).to.eql([1, 1]); + }); + }); + describe('with extent option', function() { it('gives a correct center constraint function', function() { const options = { extent: [0, 0, 1, 1] }; const fn = createCenterConstraint(options); - expect(fn([0, 0])).to.eql([0, 0]); - expect(fn([-10, 0])).to.eql([0, 0]); - expect(fn([100, 100])).to.eql([1, 1]); + 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); }); }); @@ -218,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); @@ -234,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); @@ -365,6 +388,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); @@ -964,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 }); }); @@ -973,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); @@ -1021,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); @@ -1097,12 +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() / 2); expect(view.getResolutionForZoom(min)).to.be(view.getMaxResolution()); + expect(view.getResolutionForZoom(min - 1)).to.be(view.getMaxResolution() * 2); }); it('returns correct zoom levels for specifically configured resolutions', function() { @@ -1110,11 +1141,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); }); }); @@ -1246,8 +1296,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); }); }); @@ -1278,14 +1334,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]}); @@ -1314,30 +1396,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); @@ -1360,7 +1420,6 @@ describe('ol.View', function() { { size: [200, 200], padding: [100, 0, 0, 100], - constrainResolution: false, duration: 25 }); @@ -1422,6 +1481,235 @@ 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('#getConstrainedZoom()', function() { + let view; + + it('works correctly without constraint', function() { + view = new View({ + zoom: 0 + }); + expect(view.getConstrainedZoom(3)).to.be(3); + }); + it('works correctly with resolution constraints', function() { + view = new View({ + zoom: 4, + minZoom: 4, + maxZoom: 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.getConstrainedZoom(0)).to.be(0); + expect(view.getConstrainedZoom(4)).to.be(4); + expect(view.getConstrainedZoom(8)).to.be(6); + }); + }); + + describe('#getConstrainedResolution()', function() { + let view; + const defaultMaxRes = 156543.03392804097; + + it('works correctly by snapping to power of 2', function() { + view = new View(); + 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({ + maxResolution: 2500, + zoomFactor: 5, + maxZoom: 4, + constrainResolution: true + }); + 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({ + zoom: 0, + resolutions: [512, 256, 128, 64, 32, 16, 8], + constrainResolution: true + }); + 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); + }); + }); + + 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() {