diff --git a/src/ol/interaction/draginteraction.js b/src/ol/interaction/draginteraction.js index b41e548a6b..1d525bae7d 100644 --- a/src/ol/interaction/draginteraction.js +++ b/src/ol/interaction/draginteraction.js @@ -80,6 +80,13 @@ ol.interaction.Drag.prototype.handleDragEnd = goog.nullFunction; ol.interaction.Drag.prototype.handleDragStart = goog.functions.FALSE; +/** + * @param {ol.MapBrowserEvent} mapBrowserEvent Event. + * @protected + */ +ol.interaction.Drag.prototype.handleDown = goog.nullFunction; + + /** * @inheritDoc */ @@ -91,6 +98,10 @@ ol.interaction.Drag.prototype.handleMapBrowserEvent = } var view = map.getView(); var browserEvent = mapBrowserEvent.browserEvent; + if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DOWN) { + goog.asserts.assert(browserEvent instanceof goog.events.BrowserEvent); + this.handleDown(mapBrowserEvent); + } if (this.dragging_) { if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DRAG) { goog.asserts.assert(browserEvent instanceof goog.events.BrowserEvent); diff --git a/src/ol/interaction/dragpaninteraction.js b/src/ol/interaction/dragpaninteraction.js index 3fcc4d4157..5c516d62ff 100644 --- a/src/ol/interaction/dragpaninteraction.js +++ b/src/ol/interaction/dragpaninteraction.js @@ -16,8 +16,9 @@ goog.require('ol.interaction.Drag'); * @constructor * @extends {ol.interaction.Drag} * @param {ol.interaction.ConditionType} condition Condition. + * @param {ol.Kinetic=} opt_kinetic Kinetic object. */ -ol.interaction.DragPan = function(condition) { +ol.interaction.DragPan = function(condition, opt_kinetic) { goog.base(this); @@ -27,6 +28,18 @@ ol.interaction.DragPan = function(condition) { */ this.condition_ = condition; + /** + * @private + * @type {ol.Kinetic|undefined} + */ + this.kinetic_ = opt_kinetic; + + /** + * @private + * @type {?ol.PreRenderFunction} + */ + this.kineticPreRenderFn_ = null; + }; goog.inherits(ol.interaction.DragPan, ol.interaction.Drag); @@ -35,6 +48,9 @@ goog.inherits(ol.interaction.DragPan, ol.interaction.Drag); * @inheritDoc */ ol.interaction.DragPan.prototype.handleDrag = function(mapBrowserEvent) { + if (this.kinetic_) { + this.kinetic_.update(mapBrowserEvent.browserEvent); + } var map = mapBrowserEvent.map; // FIXME works for View2D only var view = map.getView(); @@ -55,9 +71,27 @@ ol.interaction.DragPan.prototype.handleDrag = function(mapBrowserEvent) { * @inheritDoc */ ol.interaction.DragPan.prototype.handleDragEnd = function(mapBrowserEvent) { + + // FIXME works for View2D only + var map = mapBrowserEvent.map; - map.requestRenderFrame(); - map.getView().setHint(ol.ViewHint.PANNING, -1); + var view = map.getView(); + view.setHint(ol.ViewHint.PANNING, -1); + + if (this.kinetic_ && this.kinetic_.end()) { + var distance = this.kinetic_.getDistance(); + var angle = this.kinetic_.getAngle(); + var center = view.getCenter(); + this.kineticPreRenderFn_ = this.kinetic_.createPanFrom(center); + map.addPreRenderFunction(this.kineticPreRenderFn_); + + var centerpx = map.getPixelFromCoordinate(center); + var destpx = new ol.Pixel( + centerpx.x - distance * Math.cos(angle), + centerpx.y - distance * Math.sin(angle)); + var dest = map.getCoordinateFromPixel(destpx); + view.setCenter(dest); + } }; @@ -67,6 +101,9 @@ ol.interaction.DragPan.prototype.handleDragEnd = function(mapBrowserEvent) { ol.interaction.DragPan.prototype.handleDragStart = function(mapBrowserEvent) { var browserEvent = mapBrowserEvent.browserEvent; if (this.condition_(browserEvent)) { + if (this.kinetic_) { + this.kinetic_.begin(browserEvent); + } var map = mapBrowserEvent.map; map.requestRenderFrame(); map.getView().setHint(ol.ViewHint.PANNING, 1); @@ -75,3 +112,21 @@ ol.interaction.DragPan.prototype.handleDragStart = function(mapBrowserEvent) { return false; } }; + + +/** + * @inheritDoc + */ +ol.interaction.DragPan.prototype.handleDown = function(mapBrowserEvent) { + var map = mapBrowserEvent.map; + // FIXME works for View2D only + var view = map.getView(); + goog.asserts.assert(view instanceof ol.View2D); + goog.asserts.assert(!goog.isNull(mapBrowserEvent.frameState)); + if (!goog.isNull(this.kineticPreRenderFn_) && + map.removePreRenderFunction(this.kineticPreRenderFn_)) { + map.requestRenderFrame(); + view.setCenter(mapBrowserEvent.frameState.view2DState.center); + this.kineticPreRenderFn_ = null; + } +}; diff --git a/src/ol/kinetic.js b/src/ol/kinetic.js new file mode 100644 index 0000000000..44aabe850b --- /dev/null +++ b/src/ol/kinetic.js @@ -0,0 +1,154 @@ + +goog.provide('ol.Kinetic'); + +goog.require('goog.array'); +goog.require('ol.Pixel'); +goog.require('ol.animation'); + + +/** + * @typedef {{x: number, + * y: number, + * t: number}} + */ +ol.KineticPoint; + + + +/** + * @constructor + * @param {number} decay Rate of decay (must be negative). + * @param {number} minVelocity Minimum velocity (pixels/millisecond). + * @param {number} delay Delay to consider to calculate the kinetic + * initial values (milliseconds). + */ +ol.Kinetic = function(decay, minVelocity, delay) { + + /** + * @private + * @type {number} + */ + this.decay_ = decay; + + /** + * @private + * @type {number} + */ + this.minVelocity_ = minVelocity; + + /** + * @private + * @type {number} + */ + this.delay_ = delay; + + /** + * @private + * @type {Array.} + */ + this.points_ = []; + + /** + * @private + * @type {number} + */ + this.angle_ = 0; + + /** + * @private + * @type {number} + */ + this.initialVelocity_ = 0; +}; + + +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + */ +ol.Kinetic.prototype.begin = function(browserEvent) { + this.points_.length = 0; + this.angle_ = 0; + this.initialVelocity_ = 0; + this.update(browserEvent); +}; + + +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + */ +ol.Kinetic.prototype.update = function(browserEvent) { + this.points_.push({ + x: browserEvent.clientX, + y: browserEvent.clientY, + t: goog.now() + }); +}; + + +/** + * @return {boolean} Whether we should do kinetic animation. + */ +ol.Kinetic.prototype.end = function() { + var now = goog.now(); + var lastIndex = this.points_.length - 1; + var firstIndex = lastIndex - 1; + while (firstIndex >= 0 && this.points_[firstIndex].t > now - this.delay_) { + firstIndex--; + } + if (firstIndex >= 0) { + var first = this.points_[firstIndex]; + var last = this.points_[lastIndex]; + var dx = last.x - first.x; + var dy = last.y - first.y; + this.angle_ = Math.atan2(dy, dx); + this.initialVelocity_ = Math.sqrt(dx * dx + dy * dy) / (last.t - first.t); + return this.initialVelocity_ > this.minVelocity_; + } + return false; +}; + + +/** + * @param {ol.Coordinate} source Source coordinate for the animation. + * @return {ol.PreRenderFunction} Pre-render function for kinetic animation. + */ +ol.Kinetic.prototype.createPanFrom = function(source) { + var decay = this.decay_; + var initialVelocity = this.initialVelocity_; + var minVelocity = this.minVelocity_; + var duration = this.getDuration_(); + var easingFunction = function(t) { + return initialVelocity * (Math.exp((decay * t) * duration) - 1) / + (minVelocity - initialVelocity); + }; + return ol.animation.createPanFrom({ + source: source, + duration: duration, + easing: easingFunction + }); +}; + + +/** + * @private + * @return {number} Duration of animation (milliseconds). + */ +ol.Kinetic.prototype.getDuration_ = function() { + return Math.log(this.minVelocity_ / this.initialVelocity_) / this.decay_; +}; + + +/** + * @return {number} Total distance travelled (pixels). + */ +ol.Kinetic.prototype.getDistance = function() { + return (this.minVelocity_ - this.initialVelocity_) / this.decay_; +}; + + +/** + * @return {number} Angle of the kinetic panning animation (radians). + */ +ol.Kinetic.prototype.getAngle = function() { + return this.angle_; +}; diff --git a/src/ol/map.js b/src/ol/map.js index 46e9743e9f..22609eb4a6 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -29,6 +29,7 @@ goog.require('ol.Color'); goog.require('ol.Coordinate'); goog.require('ol.Extent'); goog.require('ol.FrameState'); +goog.require('ol.Kinetic'); goog.require('ol.MapBrowserEvent'); goog.require('ol.Object'); goog.require('ol.Pixel'); @@ -325,6 +326,15 @@ ol.Map.prototype.addPreRenderFunctions = function(preRenderFunctions) { }; +/** + * @param {ol.PreRenderFunction} preRenderFunction Pre-render function. + * @return {boolean} Whether the preRenderFunction has been found and removed. + */ +ol.Map.prototype.removePreRenderFunction = function(preRenderFunction) { + return goog.array.remove(this.preRenderFunctions_, preRenderFunction); +}; + + /** * * @inheritDoc @@ -532,6 +542,7 @@ ol.Map.prototype.handleControlsRemove_ = function(collectionEvent) { * @param {ol.MapBrowserEvent} mapBrowserEvent The event to handle. */ ol.Map.prototype.handleMapBrowserEvent = function(mapBrowserEvent) { + mapBrowserEvent.frameState = this.frameState_; var interactions = this.getInteractions(); var interactionsArray = /** @type {Array.} */ (interactions.getArray()); @@ -984,7 +995,8 @@ ol.Map.createInteractions_ = function(mapOptions) { mapOptions.dragPan : true; if (dragPan) { interactions.push( - new ol.interaction.DragPan(ol.interaction.condition.noModifierKeys)); + new ol.interaction.DragPan(ol.interaction.condition.noModifierKeys, + new ol.Kinetic(-0.005, 0.05, 100))); } var keyboard = goog.isDef(mapOptions.keyboard) ? diff --git a/src/ol/mapbrowserevent.js b/src/ol/mapbrowserevent.js index cdb40d3e66..a6b0f20be7 100644 --- a/src/ol/mapbrowserevent.js +++ b/src/ol/mapbrowserevent.js @@ -21,10 +21,11 @@ goog.require('ol.Pixel'); * @param {string} type Event type. * @param {ol.Map} map Map. * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @param {?ol.FrameState=} opt_frameState Frame state. */ -ol.MapBrowserEvent = function(type, map, browserEvent) { +ol.MapBrowserEvent = function(type, map, browserEvent, opt_frameState) { - goog.base(this, type, map); + goog.base(this, type, map, opt_frameState); /** * @type {goog.events.BrowserEvent} @@ -230,6 +231,9 @@ ol.MapBrowserEventHandler.prototype.handleUp_ = function(browserEvent) { * @private */ ol.MapBrowserEventHandler.prototype.handleDown_ = function(browserEvent) { + var newEvent = new ol.MapBrowserEvent( + ol.MapBrowserEvent.EventType.DOWN, this.map_, browserEvent); + this.dispatchEvent(newEvent); if (!this.previous_) { this.touchEnableBrowserEvent_(browserEvent); this.down_ = browserEvent; @@ -306,5 +310,6 @@ ol.MapBrowserEvent.EventType = { DBLCLICK: goog.events.EventType.DBLCLICK, DRAGSTART: 'dragstart', DRAG: 'drag', - DRAGEND: 'dragend' + DRAGEND: 'dragend', + DOWN: 'down' };