/* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ import ol_Object from 'ol/Object.js' import {linear as ol_easing_linear} from 'ol/easing.js' import ol_Map from 'ol/Map.js' import {getCenter as ol_extent_getCenter} from 'ol/extent.js' import {unByKey as ol_Observable_unByKey} from 'ol/Observable.js' import ol_layer_Base from 'ol/layer/Base.js' import ol_style_Style from 'ol/style/Style.js' import ol_style_Circle from 'ol/style/Circle.js' import ol_style_Stroke from 'ol/style/Stroke.js' import ol_layer_Vector from 'ol/layer/Vector.js' import ol_source_Vector from 'ol/source/Vector.js' import ol_render_getVectorContext from '../util/getVectorContext.js'; import ol_ext_getVectorContextStyle from '../util/getVectorContextStyle.js' /** Feature animation base class * Use the {@link ol.Map#animateFeature} or {@link ol.layer.Vector#animateFeature} to animate a feature * on postcompose in a map or a layer * @constructor * @fires animationstart * @fires animating * @fires animationrepeat * @fires animationend * @fires drawing * @param {ol_featureAnimationOptions} options * @param {Number} options.duration duration of the animation in ms, default 1000 * @param {bool} options.revers revers the animation direction * @param {Number} options.repeat number of time to repeat the animation, default 0 * @param {ol.style.Style} options.hiddenStyle a style to display the feature when playing the animation * to be used to make the feature selectable when playing animation * (@see {@link ../examples/map.featureanimation.select.html}), default the feature * will be hidden when playing (and not selectable) * @param {ol_easing_Function} options.fade an easing function used to fade in the feature, default none * @param {ol_easing_Function} options.easing an easing function for the animation, default ol_easing_linear */ var ol_featureAnimation = class olfeatureAnimation extends ol_Object { constructor(options) { options = options || {} super(); this.duration_ = typeof (options.duration) == 'number' ? (options.duration >= 0 ? options.duration : 0) : 1000 this.fade_ = typeof (options.fade) == 'function' ? options.fade : null this.repeat_ = Number(options.repeat) var easing = typeof (options.easing) == 'function' ? options.easing : ol_easing_linear if (options.revers) this.easing_ = function (t) { return (1 - easing(t)) } else this.easing_ = easing this.hiddenStyle = options.hiddenStyle } /** Draw a geometry * @param {olx.animateFeatureEvent} e * @param {ol.geom} geom geometry for shadow * @param {ol.geom} shadow geometry for shadow (ie. style with zIndex = -1) * @private */ drawGeom_(e, geom, shadow) { // Drawing event var drawingEvt = { type: 'drawing', time: e.time, feature: e.feature, start: e.start, stop: e.stop, rotation: e.rotation, style: e.style, extra: e.extra } this.dispatchEvent(drawingEvt) var style = (drawingEvt.style instanceof Array) ? drawingEvt.style : [drawingEvt.style]; // Draw if (this.fade_) { e.context.globalAlpha = this.fade_(1 - e.elapsed) } for (var i = 0; i < style.length; i++) { // Prevent crach if the style is not ready (image not loaded) try { var vectorContext = e.vectorContext || ol_render_getVectorContext(e) var s = ol_ext_getVectorContextStyle(e, style[i]) vectorContext.setStyle(s) if (s.getZIndex() < 0) { vectorContext.drawGeometry(shadow || geom) } else { vectorContext.drawGeometry(geom) } } catch (error) { /* ok */ } } } /** Function to perform manipulations onpostcompose. * This function is called with an ol_featureAnimationEvent argument. * The function will be overridden by the child implementation. * Return true to keep this function for the next frame, false to remove it. * @param {ol_featureAnimationEvent} e * @return {bool} true to continue animation. * @api */ animate( /* e */) { return false } } /** Hidden style: a transparent style */ ol_featureAnimation.hiddenStyle = new ol_style_Style({ image: new ol_style_Circle({}), stroke: new ol_style_Stroke({ color: 'transparent' }) }); /** An animation controler object an object to control animation with start, stop and isPlaying function. * To be used with {@link olx.Map#animateFeature} or {@link ol.layer.Vector#animateFeature} * @typedef {Object} animationControler * @property {function} start - start animation. * @property {function} stop - stop animation option arguments can be passed in animationend event. * @property {function} isPlaying - return true if animation is playing. */ /** Animate feature on a map * @function * @param {ol.Feature} feature Feature to animate * @param {ol_featureAnimation|Array} fanim the animation to play * @return {animationControler} an object to control animation with start, stop and isPlaying function */ ol_Map.prototype.animateFeature = function(feature, fanim) { // Get or create an animation layer associated with the map var layer = this._featureAnimationLayer; if (!layer) { layer = this._featureAnimationLayer = new ol_layer_Vector({ source: new ol_source_Vector() }); layer.setMap(this); } // Animate feature on this layer layer.getSource().addFeature(feature); var listener = fanim.on('animationend', function(e) { if (e.feature===feature) { // Remove feature on end layer.getSource().removeFeature(feature); ol_Observable_unByKey(listener); } }); return layer.animateFeature(feature, fanim); }; /** Animate feature on a vector layer * @fires animationstart, animationend * @param {ol.Feature} feature Feature to animate * @param {ol_featureAnimation|Array} fanim the animation to play * @param {boolean} useFilter use the filters of the layer * @return {animationControler} an object to control animation with start, stop and isPlaying function */ ol_layer_Base.prototype.animateFeature = function(feature, fanim, useFilter) { var self = this; var listenerKey; // Save style var style = feature.getStyle(); var flashStyle = style || (this.getStyleFunction ? this.getStyleFunction()(feature) : null); if (!flashStyle) flashStyle=[]; if (!(flashStyle instanceof Array)) flashStyle = [flashStyle]; // Structure pass for animating var event = { // Frame context vectorContext: null, frameState: null, start: 0, time: 0, elapsed: 0, extent: false, // Feature information feature: feature, geom: feature.getGeometry(), typeGeom: feature.getGeometry().getType(), bbox: feature.getGeometry().getExtent(), coord: ol_extent_getCenter(feature.getGeometry().getExtent()), style: flashStyle }; if (!(fanim instanceof Array)) fanim = [fanim]; // Remove null animations for (var i=fanim.length-1; i>=0; i--) { if (fanim[i].duration_===0) fanim.splice(i,1); } var nb=0, step = 0; // Filter availiable on the layer var filters = (useFilter && this.getFilters) ? this.getFilters() : []; function animate(e) { event.type = e.type; try { event.vectorContext = e.vectorContext || ol_render_getVectorContext(e); } catch(e) { /* nothing todo */ } event.frameState = e.frameState; event.inversePixelTransform = e.inversePixelTransform; if (!event.extent) { event.extent = e.frameState.extent; event.start = e.frameState.time; event.context = e.context; } event.time = e.frameState.time - event.start; event.elapsed = event.time / fanim[step].duration_; if (event.elapsed > 1) event.elapsed = 1; // Filter e.context.save(); filters.forEach(function(f) { if (f.get('active')) f.precompose(e); }); if (this.getOpacity) { e.context.globalAlpha = this.getOpacity(); } // Before anim /* var beforeEvent = { type: 'beforeanim', step: step, start: event.start, time: event.time, elapsed: event.elapsed, rotation: event.rotation||0, geom: event.geom, coordinate: event.coord, feature: feature, extra: event.extra || {}, style: flashStyle }; fanim[step].dispatchEvent(beforeEvent); self.dispatchEvent(beforeEvent); */ // Stop animation? if (!fanim[step].animate(event)) { nb++; // Repeat animation if (nb < fanim[step].repeat_) { event.extent = false; fanim[step].dispatchEvent({ type:'animationrepeat', feature: feature }); } else if (step < fanim.length-1) { // newt step fanim[step].dispatchEvent({ type:'animationend', feature: feature }); step++; nb=0; event.extent = false; } else { // the end stop(); } } else { var animEvent = { type: 'animating', step: step, start: event.start, time: event.time, elapsed: event.elapsed, rotation: event.rotation||0, geom: event.geom, coordinate: event.coord, feature: feature, extra: event.extra || {}, style: flashStyle }; fanim[step].dispatchEvent(animEvent); self.dispatchEvent(animEvent); } filters.forEach(function(f) { if (f.get('active')) f.postcompose(e); }); e.context.restore(); // tell OL3 to continue postcompose animation e.frameState.animate = true; } // Stop animation function stop(options) { ol_Observable_unByKey(listenerKey); listenerKey = null; feature.setStyle(style); event.stop = (new Date).getTime(); // Send event var eventEnd = { type:'animationend', feature: feature }; if (options) { for (var i in options) if (options.hasOwnProperty(i)) { eventEnd[i] = options[i]; } } fanim[step].dispatchEvent(eventEnd); self.dispatchEvent(eventEnd); } // Launch animation function start(options) { if (fanim.length && !listenerKey) { // Restart at stop time if (event.stop) { event.start = (new Date).getTime() - event.stop + event.start; event.stop = 0; } // Compose listenerKey = self.on(['postcompose','postrender'], animate.bind(self)); // map or layer? if (self.renderSync) { try { self.renderSync(); } catch(e) { /* ok */ } } else { self.changed(); } // Hide feature while animating feature.setStyle(fanim[step].hiddenStyle || ol_featureAnimation.hiddenStyle); // Send event var eventStart = { type:'animationstart', feature: feature }; if (options) { for (var i in options) if (options.hasOwnProperty(i)) { eventStart[i] = options[i]; } } fanim[step].dispatchEvent(eventStart); self.dispatchEvent(eventStart); } } start(); // Return animation controler return { start: start, stop: stop, isPlaying: function() { return (!!listenerKey); } }; }; export default ol_featureAnimation