ol-ext
Version:
A set of cool extensions for OpenLayers (ol) in node modules structure
341 lines (316 loc) • 11.6 kB
JavaScript
/*
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<ol_featureAnimation>} 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<ol_featureAnimation>} 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