diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md
index b0ec2e0590..17e80b9a10 100644
--- a/changelog/upgrade-notes.md
+++ b/changelog/upgrade-notes.md
@@ -1,5 +1,40 @@
## Upgrade notes
+### Next release
+
+#### Use `view.animate()` instead of `map.beforeRender()` and `ol.animation` functions
+
+The `map.beforeRender()` and `ol.animation` functions have been deprecated in favor of a new `view.animate()` function. Use of the deprecated functions will result in a warning during development. These functions are subject to removal in an upcoming release.
+
+For details on the `view.animate()` method, see the API docs and the view animation example. Upgrading should be relatively straightforward. For example, if you wanted to have an animated pan, zoom, and rotation previously, you might have done this:
+
+```js
+var zoom = ol.animation.zoom({
+ resolution: view.getResolution()
+});
+var pan = ol.animation.pan({
+ source: view.getCenter()
+});
+var rotate = ol.animation.rotate({
+ rotation: view.getRotation()
+});
+
+map.beforeRender(zoom, pan, rotate);
+
+map.setZoom(1);
+map.setCenter([0, 0]);
+map.setRotation(Math.PI);
+```
+
+Now, the same can be accomplished with this:
+```js
+view.animate({
+ zoom: 1,
+ center: [0, 0],
+ rotation: Math.PI
+});
+```
+
### v3.19.1
#### `ol.style.Fill` with `CanvasGradient` or `CanvasPattern`
diff --git a/doc/tutorials/introduction.md b/doc/tutorials/introduction.md
index 05c7a4d474..6858efb4cd 100644
--- a/doc/tutorials/introduction.md
+++ b/doc/tutorials/introduction.md
@@ -34,7 +34,7 @@ The library is intended for use on both desktop/laptop and mobile devices.
OL3 uses a similar object hierarchy to the Closure library. There is a top-level `ol` namespace (basically, `var ol = {};`). Subdivisions of this are:
* further namespaces, such as `ol.layer`; these have a lower-case initial
-* simple objects containing static properties and methods, such as `ol.animation`; these also have a lower-case initial
+* simple objects containing static properties and methods, such as `ol.easing`; these also have a lower-case initial
* types, which have an upper-case initial. These are mainly 'classes', which here means a constructor function with prototypal inheritance, such as `ol.Map` or `ol.layer.Vector` (the Vector class within the layer namespace). There are however other, simpler, types, such as `ol.Extent`, which is an array.
Class namespaces, such as `ol.layer` have a base class type with the same name, such as `ol.layer.Layer`. These are mainly abstract classes, from which the other subclasses inherit.
diff --git a/examples/animation.html b/examples/animation.html
index e0de6e10a9..54fa0cd014 100644
--- a/examples/animation.html
+++ b/examples/animation.html
@@ -3,17 +3,17 @@ layout: example.html
title: View Animation
shortdesc: Demonstrates animated pan, zoom, and rotation.
docs: >
- This example shows how to use the beforeRender function on the Map to run one
- or more animations.
+ This example shows how to use the view.animate() method to run
+ one or more animations.
tags: "animation"
---
- This map requires a browser that supports
WebGL.
+
+
+
WebGL
+
+
+ This map requires a browser that supports
WebGL.
+
diff --git a/externs/olx.js b/externs/olx.js
index ee532d33a5..7808ae5948 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -312,7 +312,7 @@ olx.MapOptions.prototype.view;
* stopEvent: (boolean|undefined),
* insertFirst: (boolean|undefined),
* autoPan: (boolean|undefined),
- * autoPanAnimation: (olx.animation.PanOptions|undefined),
+ * autoPanAnimation: (olx.OverlayPanOptions|undefined),
* autoPanMargin: (number|undefined)}}
*/
olx.OverlayOptions;
@@ -398,10 +398,10 @@ olx.OverlayOptions.prototype.autoPan;
/**
- * The options used to create a `ol.animation.pan` animation. This animation
- * is only used when `autoPan` is enabled. By default the default options for
- * `ol.animation.pan` are used. If set to `null` the panning is not animated.
- * @type {olx.animation.PanOptions|undefined}
+ * The animation options used to pan the overlay into view. This animation
+ * is only used when `autoPan` is enabled. A `duration` and `easing` may be
+ * provided to customize the animation.
+ * @type {olx.OverlayPanOptions|undefined}
* @api
*/
olx.OverlayOptions.prototype.autoPanAnimation;
@@ -416,6 +416,32 @@ olx.OverlayOptions.prototype.autoPanAnimation;
olx.OverlayOptions.prototype.autoPanMargin;
+/**
+ * @typedef {{
+ * duration: (number|undefined),
+ * easing: (function(number):number|undefined)
+ * }}
+ */
+olx.OverlayPanOptions;
+
+
+/**
+ * The duration of the animation in milliseconds. Default is `1000`.
+ * @type {number|undefined}
+ * @api
+ */
+olx.OverlayPanOptions.prototype.duration;
+
+
+/**
+ * The easing function to use. Can be an {@link ol.easing} or a custom function.
+ * Default is {@link ol.easing.inAndOut}.
+ * @type {function(number):number|undefined}
+ * @api
+ */
+olx.OverlayPanOptions.prototype.easing;
+
+
/**
* Object literal with config options for the projection.
* @typedef {{code: string,
@@ -661,6 +687,81 @@ olx.ViewOptions.prototype.zoom;
olx.ViewOptions.prototype.zoomFactor;
+/**
+ * @typedef {{
+ * center: (ol.Coordinate|undefined),
+ * zoom: (number|undefined),
+ * resolution: (number|undefined),
+ * rotation: (number|undefined),
+ * anchor: (ol.Coordinate|undefined),
+ * duration: (number|undefined),
+ * easing: (function(number):number|undefined)
+ * }}
+ */
+olx.AnimationOptions;
+
+
+/**
+ * The center of the view at the end of the animation.
+ * @type {ol.Coordinate|undefined}
+ * @api
+ */
+olx.AnimationOptions.prototype.center;
+
+
+/**
+ * The zoom level of the view at the end of the animation. This takes
+ * precedence over `resolution`.
+ * @type {number|undefined}
+ * @api
+ */
+olx.AnimationOptions.prototype.zoom;
+
+
+/**
+ * The resolution of the view at the end of the animation. If `zoom` is also
+ * provided, this option will be ignored.
+ * @type {number|undefined}
+ * @api
+ */
+olx.AnimationOptions.prototype.resolution;
+
+
+/**
+ * The rotation of the view at the end of the animation.
+ * @type {number|undefined}
+ * @api
+ */
+olx.AnimationOptions.prototype.rotation;
+
+
+/**
+ * Optional anchor to remained fixed during a rotation or resolution animation.
+ * @type {ol.Coordinate|undefined}
+ * @api
+ */
+olx.AnimationOptions.prototype.anchor;
+
+
+/**
+ * The duration of the animation in milliseconds (defaults to `1000`).
+ * @type {number|undefined}
+ * @api
+ */
+olx.AnimationOptions.prototype.duration;
+
+
+/**
+ * The easing function used during the animation (defaults to {@link ol.easing.inAndOut}).
+ * The function will be called for each frame with a number representing a
+ * fraction of the animation's duration. The function should return a number
+ * between 0 and 1 representing the progress toward the destination state.
+ * @type {function(number):number|undefined}
+ * @api
+ */
+olx.AnimationOptions.prototype.easing;
+
+
/**
* Namespace.
* @type {Object}
diff --git a/package.json b/package.json
index 8c8bbeb0e3..b4915bf03f 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,8 @@
2,
{
"allow": [
- "assert"
+ "assert",
+ "warn"
]
}
],
diff --git a/src/ol/animation.js b/src/ol/animation.js
index b32d4de422..ec555c1fef 100644
--- a/src/ol/animation.js
+++ b/src/ol/animation.js
@@ -7,6 +7,7 @@ goog.require('ol.easing');
/**
+ * Deprecated (use {@link ol.View#animate} instead).
* Generate an animated transition that will "bounce" the resolution as it
* approaches the final value.
* @param {olx.animation.BounceOptions} options Bounce options.
@@ -14,6 +15,7 @@ goog.require('ol.easing');
* @api
*/
ol.animation.bounce = function(options) {
+ ol.DEBUG && console.warn('ol.animation.bounce() is deprecated. Use view.animate() instead.');
var resolution = options.resolution;
var start = options.start ? options.start : Date.now();
var duration = options.duration !== undefined ? options.duration : 1000;
@@ -45,12 +47,14 @@ ol.animation.bounce = function(options) {
/**
+ * Deprecated (use {@link ol.View#animate} instead).
* Generate an animated transition while updating the view center.
* @param {olx.animation.PanOptions} options Pan options.
* @return {ol.PreRenderFunction} Pre-render function.
* @api
*/
ol.animation.pan = function(options) {
+ ol.DEBUG && console.warn('ol.animation.pan() is deprecated. Use view.animate() instead.');
var source = options.source;
var start = options.start ? options.start : Date.now();
var sourceX = source[0];
@@ -86,12 +90,14 @@ ol.animation.pan = function(options) {
/**
+ * Deprecated (use {@link ol.View#animate} instead).
* Generate an animated transition while updating the view rotation.
* @param {olx.animation.RotateOptions} options Rotate options.
* @return {ol.PreRenderFunction} Pre-render function.
* @api
*/
ol.animation.rotate = function(options) {
+ ol.DEBUG && console.warn('ol.animation.rotate() is deprecated. Use view.animate() instead.');
var sourceRotation = options.rotation ? options.rotation : 0;
var start = options.start ? options.start : Date.now();
var duration = options.duration !== undefined ? options.duration : 1000;
@@ -133,12 +139,14 @@ ol.animation.rotate = function(options) {
/**
+ * Deprecated (use {@link ol.View#animate} instead).
* Generate an animated transition while updating the view resolution.
* @param {olx.animation.ZoomOptions} options Zoom options.
* @return {ol.PreRenderFunction} Pre-render function.
* @api
*/
ol.animation.zoom = function(options) {
+ ol.DEBUG && console.warn('ol.animation.zoom() is deprecated. Use view.animate() instead.');
var sourceResolution = options.resolution;
var start = options.start ? options.start : Date.now();
var duration = options.duration !== undefined ? options.duration : 1000;
diff --git a/src/ol/control/rotate.js b/src/ol/control/rotate.js
index 54d5f420c9..bc6cede392 100644
--- a/src/ol/control/rotate.js
+++ b/src/ol/control/rotate.js
@@ -3,7 +3,6 @@ goog.provide('ol.control.Rotate');
goog.require('ol.events');
goog.require('ol.events.EventType');
goog.require('ol');
-goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');
@@ -131,13 +130,14 @@ ol.control.Rotate.prototype.resetNorth_ = function() {
if (currentRotation > Math.PI) {
currentRotation -= 2 * Math.PI;
}
- map.beforeRender(ol.animation.rotate({
- rotation: currentRotation,
+ view.animate({
+ rotation: 0,
duration: this.duration_,
easing: ol.easing.easeOut
- }));
+ });
+ } else {
+ view.setRotation(0);
}
- view.setRotation(0);
}
};
diff --git a/src/ol/control/zoom.js b/src/ol/control/zoom.js
index 8b61d8c92d..390578d332 100644
--- a/src/ol/control/zoom.js
+++ b/src/ol/control/zoom.js
@@ -3,7 +3,6 @@ goog.provide('ol.control.Zoom');
goog.require('ol');
goog.require('ol.events');
goog.require('ol.events.EventType');
-goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');
@@ -105,14 +104,15 @@ ol.control.Zoom.prototype.zoomByDelta_ = function(delta) {
}
var currentResolution = view.getResolution();
if (currentResolution) {
+ var newResolution = view.constrainResolution(currentResolution, delta);
if (this.duration_ > 0) {
- map.beforeRender(ol.animation.zoom({
- resolution: currentResolution,
+ view.animate({
+ resolution: newResolution,
duration: this.duration_,
easing: ol.easing.easeOut
- }));
+ });
+ } else {
+ view.setResolution(newResolution);
}
- var newResolution = view.constrainResolution(currentResolution, delta);
- view.setResolution(newResolution);
}
};
diff --git a/src/ol/control/zoomslider.js b/src/ol/control/zoomslider.js
index 25d99d4442..35c2995895 100644
--- a/src/ol/control/zoomslider.js
+++ b/src/ol/control/zoomslider.js
@@ -4,7 +4,6 @@ goog.provide('ol.control.ZoomSlider');
goog.require('ol');
goog.require('ol.View');
-goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');
@@ -233,19 +232,19 @@ ol.control.ZoomSlider.render = function(mapEvent) {
* @private
*/
ol.control.ZoomSlider.prototype.handleContainerClick_ = function(event) {
- var map = this.getMap();
- var view = map.getView();
- var currentResolution = view.getResolution();
- map.beforeRender(ol.animation.zoom({
- resolution: /** @type {number} */ (currentResolution),
- duration: this.duration_,
- easing: ol.easing.easeOut
- }));
+ var view = this.getMap().getView();
+
var relativePosition = this.getRelativePosition_(
event.offsetX - this.thumbSize_[0] / 2,
event.offsetY - this.thumbSize_[1] / 2);
+
var resolution = this.getResolutionForPosition_(relativePosition);
- view.setResolution(view.constrainResolution(resolution));
+
+ view.animate({
+ resolution: view.constrainResolution(resolution),
+ duration: this.duration_,
+ easing: ol.easing.easeOut
+ });
};
@@ -306,16 +305,15 @@ ol.control.ZoomSlider.prototype.handleDraggerDrag_ = function(event) {
*/
ol.control.ZoomSlider.prototype.handleDraggerEnd_ = function(event) {
if (this.dragging_) {
- var map = this.getMap();
- var view = map.getView();
+ var view = this.getMap().getView();
view.setHint(ol.View.Hint.INTERACTING, -1);
- map.beforeRender(ol.animation.zoom({
- resolution: /** @type {number} */ (this.currentResolution_),
+
+ view.animate({
+ resolution: view.constrainResolution(this.currentResolution_),
duration: this.duration_,
easing: ol.easing.easeOut
- }));
- var resolution = view.constrainResolution(this.currentResolution_);
- view.setResolution(resolution);
+ });
+
this.dragging_ = false;
this.previousX_ = undefined;
this.previousY_ = undefined;
diff --git a/src/ol/easing.jsdoc b/src/ol/easing.jsdoc
index 2d63bec167..1a3dad223e 100644
--- a/src/ol/easing.jsdoc
+++ b/src/ol/easing.jsdoc
@@ -1,4 +1,4 @@
/**
- * Easing functions for {@link ol.animation}.
+ * Easing functions for {@link ol.View#animate}.
* @namespace ol.easing
*/
diff --git a/src/ol/interaction/dragpan.js b/src/ol/interaction/dragpan.js
index feef87caa5..a8f51e7e82 100644
--- a/src/ol/interaction/dragpan.js
+++ b/src/ol/interaction/dragpan.js
@@ -3,6 +3,7 @@ goog.provide('ol.interaction.DragPan');
goog.require('ol');
goog.require('ol.View');
goog.require('ol.coordinate');
+goog.require('ol.easing');
goog.require('ol.events.condition');
goog.require('ol.functions');
goog.require('ol.interaction.Pointer');
@@ -33,12 +34,6 @@ ol.interaction.DragPan = function(opt_options) {
*/
this.kinetic_ = options.kinetic;
- /**
- * @private
- * @type {?ol.PreRenderFunction}
- */
- this.kineticPreRenderFn_ = null;
-
/**
* @type {ol.Pixel}
*/
@@ -105,15 +100,16 @@ ol.interaction.DragPan.handleUpEvent_ = function(mapBrowserEvent) {
var distance = this.kinetic_.getDistance();
var angle = this.kinetic_.getAngle();
var center = /** @type {!ol.Coordinate} */ (view.getCenter());
- this.kineticPreRenderFn_ = this.kinetic_.pan(center);
- map.beforeRender(this.kineticPreRenderFn_);
var centerpx = map.getPixelFromCoordinate(center);
var dest = map.getCoordinateFromPixel([
centerpx[0] - distance * Math.cos(angle),
centerpx[1] - distance * Math.sin(angle)
]);
- dest = view.constrainCenter(dest);
- view.setCenter(dest);
+ view.animate({
+ center: view.constrainCenter(dest),
+ duration: 500,
+ easing: ol.easing.easeOut
+ });
} else {
// the view is not updated, force a render
map.render();
@@ -141,11 +137,8 @@ ol.interaction.DragPan.handleDownEvent_ = function(mapBrowserEvent) {
if (!this.handlingDownUpSequence) {
view.setHint(ol.View.Hint.INTERACTING, 1);
}
- if (this.kineticPreRenderFn_ &&
- map.removePreRenderFunction(this.kineticPreRenderFn_)) {
- view.setCenter(mapBrowserEvent.frameState.viewState.center);
- this.kineticPreRenderFn_ = null;
- }
+ // stop any current animation
+ view.setCenter(mapBrowserEvent.frameState.viewState.center);
if (this.kinetic_) {
this.kinetic_.begin();
}
diff --git a/src/ol/interaction/dragzoom.js b/src/ol/interaction/dragzoom.js
index fa9652b492..ab54c2e467 100644
--- a/src/ol/interaction/dragzoom.js
+++ b/src/ol/interaction/dragzoom.js
@@ -1,7 +1,6 @@
goog.provide('ol.interaction.DragZoom');
goog.require('ol');
-goog.require('ol.animation');
goog.require('ol.easing');
goog.require('ol.events.condition');
goog.require('ol.extent');
@@ -75,21 +74,11 @@ ol.interaction.DragZoom.prototype.onBoxEnd = function() {
var resolution = view.constrainResolution(
view.getResolutionForExtent(extent, size));
- var currentResolution = /** @type {number} */ (view.getResolution());
-
- var currentCenter = /** @type {!ol.Coordinate} */ (view.getCenter());
-
- map.beforeRender(ol.animation.zoom({
- resolution: currentResolution,
+ view.animate({
+ resolution: resolution,
+ center: ol.extent.getCenter(extent),
duration: this.duration_,
easing: ol.easing.easeOut
- }));
- map.beforeRender(ol.animation.pan({
- source: currentCenter,
- duration: this.duration_,
- easing: ol.easing.easeOut
- }));
+ });
- view.setCenter(ol.extent.getCenter(extent));
- view.setResolution(resolution);
};
diff --git a/src/ol/interaction/interaction.js b/src/ol/interaction/interaction.js
index 5d13ac1947..67647dcb0f 100644
--- a/src/ol/interaction/interaction.js
+++ b/src/ol/interaction/interaction.js
@@ -4,7 +4,6 @@ goog.provide('ol.interaction.Interaction');
goog.require('ol');
goog.require('ol.Object');
-goog.require('ol.animation');
goog.require('ol.easing');
@@ -99,16 +98,17 @@ ol.interaction.Interaction.prototype.setMap = function(map) {
ol.interaction.Interaction.pan = function(map, view, delta, opt_duration) {
var currentCenter = view.getCenter();
if (currentCenter) {
- if (opt_duration && opt_duration > 0) {
- map.beforeRender(ol.animation.pan({
- source: currentCenter,
- duration: opt_duration,
- easing: ol.easing.linear
- }));
- }
var center = view.constrainCenter(
[currentCenter[0] + delta[0], currentCenter[1] + delta[1]]);
- view.setCenter(center);
+ if (opt_duration) {
+ view.animate({
+ duration: opt_duration,
+ easing: ol.easing.linear,
+ center: center
+ });
+ } else {
+ view.setCenter(center);
+ }
}
};
@@ -138,22 +138,16 @@ ol.interaction.Interaction.rotateWithoutConstraints = function(map, view, rotati
if (rotation !== undefined) {
var currentRotation = view.getRotation();
var currentCenter = view.getCenter();
- if (currentRotation !== undefined && currentCenter &&
- opt_duration && opt_duration > 0) {
- map.beforeRender(ol.animation.rotate({
- rotation: currentRotation,
+ if (currentRotation !== undefined && currentCenter && opt_duration > 0) {
+ view.animate({
+ rotation: rotation,
+ anchor: opt_anchor,
duration: opt_duration,
easing: ol.easing.easeOut
- }));
- if (opt_anchor) {
- map.beforeRender(ol.animation.pan({
- source: currentCenter,
- duration: opt_duration,
- easing: ol.easing.easeOut
- }));
- }
+ });
+ } else {
+ view.rotate(rotation, opt_anchor);
}
- view.rotate(rotation, opt_anchor);
}
};
@@ -207,26 +201,20 @@ ol.interaction.Interaction.zoomWithoutConstraints = function(map, view, resoluti
var currentResolution = view.getResolution();
var currentCenter = view.getCenter();
if (currentResolution !== undefined && currentCenter &&
- resolution !== currentResolution &&
- opt_duration && opt_duration > 0) {
- map.beforeRender(ol.animation.zoom({
- resolution: currentResolution,
+ resolution !== currentResolution && opt_duration) {
+ view.animate({
+ resolution: resolution,
+ anchor: opt_anchor,
duration: opt_duration,
easing: ol.easing.easeOut
- }));
+ });
+ } else {
if (opt_anchor) {
- map.beforeRender(ol.animation.pan({
- source: currentCenter,
- duration: opt_duration,
- easing: ol.easing.easeOut
- }));
+ var center = view.calculateCenterZoom(resolution, opt_anchor);
+ view.setCenter(center);
}
+ view.setResolution(resolution);
}
- if (opt_anchor) {
- var center = view.calculateCenterZoom(resolution, opt_anchor);
- view.setCenter(center);
- }
- view.setResolution(resolution);
}
};
diff --git a/src/ol/kinetic.js b/src/ol/kinetic.js
index e5cbdca658..65adb7a583 100644
--- a/src/ol/kinetic.js
+++ b/src/ol/kinetic.js
@@ -1,7 +1,5 @@
goog.provide('ol.Kinetic');
-goog.require('ol.animation');
-
/**
* @classdesc
@@ -105,32 +103,6 @@ ol.Kinetic.prototype.end = function() {
};
-/**
- * @param {ol.Coordinate} source Source coordinate for the animation.
- * @return {ol.PreRenderFunction} Pre-render function for kinetic animation.
- */
-ol.Kinetic.prototype.pan = function(source) {
- var decay = this.decay_;
- var initialVelocity = this.initialVelocity_;
- var velocity = this.minVelocity_ - initialVelocity;
- var duration = this.getDuration_();
- var easingFunction = (
- /**
- * @param {number} t T.
- * @return {number} Easing.
- */
- function(t) {
- return initialVelocity * (Math.exp((decay * t) * duration) - 1) /
- velocity;
- });
- return ol.animation.pan({
- source: source,
- duration: duration,
- easing: easingFunction
- });
-};
-
-
/**
* @private
* @return {number} Duration of animation (milliseconds).
diff --git a/src/ol/map.js b/src/ol/map.js
index 78c989b7b3..91ef5674ee 100644
--- a/src/ol/map.js
+++ b/src/ol/map.js
@@ -512,6 +512,7 @@ ol.Map.prototype.addOverlayInternal_ = function(overlay) {
/**
+ * Deprecated (use {@link ol.View#animate} instead).
* Add functions to be called before rendering. This can be used for attaching
* animations before updating the map's view. The {@link ol.animation}
* namespace provides several static methods for creating prerender functions.
@@ -519,6 +520,7 @@ ol.Map.prototype.addOverlayInternal_ = function(overlay) {
* @api
*/
ol.Map.prototype.beforeRender = function(var_args) {
+ ol.DEBUG && console.warn('map.beforeRender() is deprecated. Use view.animate() instead.');
this.render();
Array.prototype.push.apply(this.preRenderFunctions_, arguments);
};
diff --git a/src/ol/overlay.js b/src/ol/overlay.js
index 635fae8aa5..506fe92800 100644
--- a/src/ol/overlay.js
+++ b/src/ol/overlay.js
@@ -3,7 +3,6 @@ goog.provide('ol.Overlay');
goog.require('ol');
goog.require('ol.MapEvent');
goog.require('ol.Object');
-goog.require('ol.animation');
goog.require('ol.dom');
goog.require('ol.events');
goog.require('ol.extent');
@@ -69,10 +68,10 @@ ol.Overlay = function(options) {
/**
* @private
- * @type {olx.animation.PanOptions}
+ * @type {olx.OverlayPanOptions}
*/
- this.autoPanAnimation_ = options.autoPanAnimation !== undefined ?
- options.autoPanAnimation : /** @type {olx.animation.PanOptions} */ ({});
+ this.autoPanAnimation_ = options.autoPanAnimation ||
+ /** @type {olx.OverlayPanOptions} */ ({});
/**
* @private
@@ -380,11 +379,11 @@ ol.Overlay.prototype.panIntoView_ = function() {
centerPx[1] + delta[1]
];
- if (this.autoPanAnimation_) {
- this.autoPanAnimation_.source = center;
- map.beforeRender(ol.animation.pan(this.autoPanAnimation_));
- }
- map.getView().setCenter(map.getCoordinateFromPixel(newCenterPx));
+ map.getView().animate({
+ center: map.getCoordinateFromPixel(newCenterPx),
+ duration: this.autoPanAnimation_.duration,
+ easing: this.autoPanAnimation_.easing
+ });
}
}
};
diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js
index 7901c923f0..fdbbd0fd48 100644
--- a/src/ol/typedefs.js
+++ b/src/ol/typedefs.js
@@ -652,6 +652,27 @@ ol.TileUrlFunctionType;
ol.TransformFunction;
+/**
+ * An animation configuration
+ *
+ * @typedef {{
+ * sourceCenter: (ol.Coordinate|undefined),
+ * targetCenter: (ol.Coordinate|undefined),
+ * sourceResolution: (number|undefined),
+ * targetResolution: (number|undefined),
+ * sourceRotation: (number|undefined),
+ * targetRotation: (number|undefined),
+ * anchor: (ol.Coordinate|undefined),
+ * start: number,
+ * duration: number,
+ * complete: boolean,
+ * easing: function(number):number,
+ * callback: (function(boolean)|undefined)
+ * }}
+ */
+ol.ViewAnimation;
+
+
/**
* @typedef {{buf: ol.webgl.Buffer,
* buffer: WebGLBuffer}}
diff --git a/src/ol/view.js b/src/ol/view.js
index a9f1a94732..61b4640ee9 100644
--- a/src/ol/view.js
+++ b/src/ol/view.js
@@ -9,6 +9,7 @@ goog.require('ol.RotationConstraint');
goog.require('ol.array');
goog.require('ol.asserts');
goog.require('ol.coordinate');
+goog.require('ol.easing');
goog.require('ol.extent');
goog.require('ol.geom.Polygon');
goog.require('ol.geom.SimpleGeometry');
@@ -84,6 +85,20 @@ ol.View = function(opt_options) {
*/
this.hints_ = [0, 0];
+ /**
+ * @private
+ * @type {Array.
>}
+ */
+ this.animations_ = [];
+
+ /**
+ * @private
+ * @type {number|undefined}
+ */
+ this.updateAnimationKey_;
+
+ this.updateAnimations_ = this.updateAnimations_.bind(this);
+
/**
* @type {Object.}
*/
@@ -155,6 +170,194 @@ ol.View = function(opt_options) {
ol.inherits(ol.View, ol.Object);
+/**
+ * Animate the view. The view's center, zoom (or resolution), and rotation
+ * can be animated for smooth transitions between view states. For example,
+ * to animate the view to a new zoom level:
+ *
+ * view.animate({zoom: view.getZoom() + 1});
+ *
+ * By default, the animation lasts one second and uses in-and-out easing. You
+ * can customize this behavior by including `duration` (in milliseconds) and
+ * `easing` options (see {@link ol.easing}).
+ *
+ * To chain together multiple animations, call the method with multiple
+ * animation objects. For example, to first zoom and then pan:
+ *
+ * view.animate({zoom: 10}, {center: [0, 0]});
+ *
+ * If you provide a function as the last argument to the animate method, it
+ * will get called at the end of an animation series. The callback will be
+ * called with `true` if the animation series completed on its own or `false`
+ * if it was cancelled.
+ *
+ * Animations are cancelled by user interactions (e.g. dragging the map) or by
+ * calling `view.setCenter()`, `view.setResolution()`, or `view.setRotation()`
+ * (or another method that calls one of these).
+ *
+ * @param {...(olx.AnimationOptions|function(boolean))} var_args Animation
+ * options. Multiple animations can be run in series by passing multiple
+ * options objects. To run multiple animations in parallel, call the method
+ * multiple times. An optional callback can be provided as a final
+ * argument. The callback will be called with a boolean indicating whether
+ * the animation completed without being cancelled.
+ * @api
+ */
+ol.View.prototype.animate = function(var_args) {
+ var start = Date.now();
+ var center = this.getCenter().slice();
+ var resolution = this.getResolution();
+ var rotation = this.getRotation();
+ var animationCount = arguments.length;
+ var callback;
+ if (animationCount > 1 && typeof arguments[animationCount - 1] === 'function') {
+ callback = arguments[animationCount - 1];
+ --animationCount;
+ }
+ var series = [];
+ for (var i = 0; i < animationCount; ++i) {
+ var options = /** @type olx.AnimationOptions */ (arguments[i]);
+
+ var animation = /** @type {ol.ViewAnimation} */ ({
+ start: start,
+ complete: false,
+ anchor: options.anchor,
+ duration: options.duration || 1000,
+ easing: options.easing || ol.easing.inAndOut
+ });
+
+ if (options.center) {
+ animation.sourceCenter = center;
+ animation.targetCenter = options.center;
+ center = animation.targetCenter;
+ }
+
+ if (options.zoom !== undefined) {
+ animation.sourceResolution = resolution;
+ animation.targetResolution = this.constrainResolution(
+ this.maxResolution_, options.zoom - this.minZoom_, 0);
+ resolution = animation.targetResolution;
+ } else if (options.resolution) {
+ animation.sourceResolution = this.getResolution();
+ animation.targetResolution = options.resolution;
+ resolution = animation.targetResolution;
+ }
+
+ if (options.rotation !== undefined) {
+ animation.sourceRotation = rotation;
+ animation.targetRotation = options.rotation;
+ rotation = animation.targetRotation;
+ }
+
+ animation.callback = callback;
+ start += animation.duration;
+ series.push(animation);
+ }
+ this.animations_.push(series);
+ this.setHint(ol.View.Hint.ANIMATING, 1);
+ this.updateAnimations_();
+};
+
+
+/**
+ * Determine if the view is being animated.
+ * @return {boolean} The view is being animated.
+ */
+ol.View.prototype.getAnimating = function() {
+ return this.getHints()[ol.View.Hint.ANIMATING] > 0;
+};
+
+
+/**
+ * Cancel any ongoing animations.
+ */
+ol.View.prototype.cancelAnimations_ = function() {
+ for (var i = 0, ii = this.animations_.length; i < ii; ++i) {
+ var series = this.animations_[i];
+ if (series[0].callback) {
+ series[0].callback(false);
+ }
+ }
+ this.animations_.length = 0;
+ this.setHint(ol.View.Hint.ANIMATING, -this.getHints()[ol.View.Hint.ANIMATING]);
+};
+
+/**
+ * Update all animations.
+ */
+ol.View.prototype.updateAnimations_ = function() {
+ if (this.updateAnimationKey_ !== undefined) {
+ cancelAnimationFrame(this.updateAnimationKey_);
+ this.updateAnimationKey_ = undefined;
+ }
+ if (!this.getAnimating()) {
+ return;
+ }
+ var now = Date.now();
+ var more = false;
+ for (var i = this.animations_.length - 1; i >= 0; --i) {
+ var series = this.animations_[i];
+ var seriesComplete = true;
+ for (var j = 0, jj = series.length; j < jj; ++j) {
+ var animation = series[j];
+ if (animation.complete) {
+ continue;
+ }
+ var elapsed = now - animation.start;
+ var fraction = elapsed / animation.duration;
+ if (fraction > 1) {
+ animation.complete = true;
+ fraction = 1;
+ } else {
+ seriesComplete = false;
+ }
+ var progress = animation.easing(fraction);
+ if (animation.sourceCenter) {
+ var x0 = animation.sourceCenter[0];
+ var y0 = animation.sourceCenter[1];
+ var x1 = animation.targetCenter[0];
+ var y1 = animation.targetCenter[1];
+ var x = x0 + progress * (x1 - x0);
+ var y = y0 + progress * (y1 - y0);
+ this.set(ol.View.Property.CENTER, [x, y]);
+ }
+ if (animation.sourceResolution) {
+ var resolution = animation.sourceResolution +
+ progress * (animation.targetResolution - animation.sourceResolution);
+ if (animation.anchor) {
+ this.set(ol.View.Property.CENTER,
+ this.calculateCenterZoom(resolution, animation.anchor));
+ }
+ this.set(ol.View.Property.RESOLUTION, resolution);
+ }
+ if (animation.sourceRotation !== undefined) {
+ var rotation = animation.sourceRotation +
+ progress * (animation.targetRotation - animation.sourceRotation);
+ if (animation.anchor) {
+ this.set(ol.View.Property.CENTER,
+ this.calculateCenterRotate(rotation, animation.anchor));
+ }
+ this.set(ol.View.Property.ROTATION, rotation);
+ }
+ more = true;
+ if (!animation.complete) {
+ break;
+ }
+ }
+ if (seriesComplete) {
+ this.setHint(ol.View.Hint.ANIMATING, -1);
+ var completed = this.animations_.pop();
+ var callback = completed[0].callback;
+ if (callback) {
+ callback(true);
+ }
+ }
+ }
+ if (more && this.updateAnimationKey_ === undefined) {
+ this.updateAnimationKey_ = requestAnimationFrame(this.updateAnimations_);
+ }
+};
+
/**
* @param {number} rotation Target rotation.
* @param {ol.Coordinate} anchor Rotation anchor.
@@ -603,6 +806,9 @@ ol.View.prototype.rotate = function(rotation, opt_anchor) {
*/
ol.View.prototype.setCenter = function(center) {
this.set(ol.View.Property.CENTER, center);
+ if (this.getAnimating()) {
+ this.cancelAnimations_();
+ }
};
@@ -629,6 +835,9 @@ ol.View.prototype.setHint = function(hint, delta) {
*/
ol.View.prototype.setResolution = function(resolution) {
this.set(ol.View.Property.RESOLUTION, resolution);
+ if (this.getAnimating()) {
+ this.cancelAnimations_();
+ }
};
@@ -640,6 +849,9 @@ ol.View.prototype.setResolution = function(resolution) {
*/
ol.View.prototype.setRotation = function(rotation) {
this.set(ol.View.Property.ROTATION, rotation);
+ if (this.getAnimating()) {
+ this.cancelAnimations_();
+ }
};
diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js
index 2d32a12adb..cf5d4dacba 100644
--- a/test/spec/ol/view.test.js
+++ b/test/spec/ol/view.test.js
@@ -301,6 +301,191 @@ describe('ol.View', function() {
});
+ describe('#animate()', function() {
+
+ var originalRequestAnimationFrame = window.requestAnimationFrame;
+ var originalCancelAnimationFrame = window.cancelAnimationFrame;
+
+ beforeEach(function() {
+ window.requestAnimationFrame = function(callback) {
+ return setTimeout(callback, 1);
+ };
+ window.cancelAnimationFrame = function(key) {
+ return clearTimeout(key);
+ };
+ });
+
+ afterEach(function() {
+ window.requestAnimationFrame = originalRequestAnimationFrame;
+ window.cancelAnimationFrame = originalCancelAnimationFrame;
+ });
+
+ it('can be called to animate view properties', function(done) {
+ var view = new ol.View({
+ center: [0, 0],
+ zoom: 5
+ });
+
+ view.animate({
+ zoom: 4,
+ duration: 25
+ });
+ expect(view.getAnimating()).to.eql(true);
+
+ setTimeout(function() {
+ expect(view.getCenter()).to.eql([0, 0]);
+ expect(view.getZoom()).to.eql(4);
+ expect(view.getAnimating()).to.eql(false);
+ done();
+ }, 50);
+ });
+
+ it('prefers zoom over resolution', function(done) {
+ var view = new ol.View({
+ center: [0, 0],
+ zoom: 5
+ });
+
+ view.animate({
+ zoom: 4,
+ resolution: view.getResolution() * 3,
+ duration: 25
+ }, function(complete) {
+ expect(complete).to.be(true);
+ expect(view.getZoom()).to.be(4);
+ done();
+ });
+ });
+
+ it('calls a callback when animation completes', function(done) {
+ var view = new ol.View({
+ center: [0, 0],
+ zoom: 0
+ });
+
+ view.animate({
+ zoom: 1,
+ duration: 25
+ }, function(complete) {
+ expect(complete).to.be(true);
+ done();
+ });
+ });
+
+ it('calls callback with false when animation is interrupted', function(done) {
+ var view = new ol.View({
+ center: [0, 0],
+ zoom: 0
+ });
+
+ view.animate({
+ zoom: 1,
+ duration: 25
+ }, function(complete) {
+ expect(complete).to.be(false);
+ done();
+ });
+
+ view.setCenter([1, 2]); // interrupt the animation
+ });
+
+ it('can run multiple animations in series', function(done) {
+ var view = new ol.View({
+ center: [0, 0],
+ zoom: 0
+ });
+
+ var checked = false;
+
+ view.animate({
+ zoom: 2,
+ duration: 25
+ }, {
+ center: [10, 10],
+ duration: 25
+ }, function(complete) {
+ expect(checked).to.be(true);
+ expect(view.getZoom()).to.roughlyEqual(2, 1e-5);
+ expect(view.getCenter()).to.eql([10, 10]);
+ expect(complete).to.be(true);
+ done();
+ });
+
+ setTimeout(function() {
+ expect(view.getCenter()).to.eql([0, 0]);
+ checked = true;
+ }, 10);
+
+ });
+
+ it('properly sets the ANIMATING hint', function(done) {
+ var view = new ol.View({
+ center: [0, 0],
+ zoom: 0,
+ rotation: 0
+ });
+
+ var count = 3;
+ function decrement() {
+ --count;
+ if (count === 0) {
+ expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(0);
+ done();
+ }
+ }
+ view.animate({
+ center: [1, 2],
+ duration: 25
+ }, decrement);
+ expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(1);
+
+ view.animate({
+ zoom: 1,
+ duration: 25
+ }, decrement);
+ expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(2);
+
+ view.animate({
+ rotate: Math.PI,
+ duration: 25
+ }, decrement);
+ expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(3);
+
+ });
+
+ it('clears the ANIMATING hint when animations are cancelled', function() {
+ var view = new ol.View({
+ center: [0, 0],
+ zoom: 0,
+ rotation: 0
+ });
+
+ view.animate({
+ center: [1, 2],
+ duration: 25
+ });
+ expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(1);
+
+ view.animate({
+ zoom: 1,
+ duration: 25
+ });
+ expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(2);
+
+ view.animate({
+ rotate: Math.PI,
+ duration: 25
+ });
+ expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(3);
+
+ // cancel animations
+ view.setCenter([10, 20]);
+ expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(0);
+
+ });
+
+ });
+
describe('#getResolutions', function() {
var view;
var resolutions = [512, 256, 128, 64, 32, 16];