src/org/un/cava/birdeye/ravis/graphLayout/layout/AnimatedBaseLayouter.as
/*
* The MIT License
*
* Copyright (c) 2007 The SixDegrees Project Team
* (Jason Bellone, Juan Rodriguez, Segolene de Basquiat, Daniel Lang).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.un.cava.birdeye.ravis.graphLayout.layout {
import flash.events.TimerEvent;
import flash.geom.Point;
import flash.utils.Dictionary;
import flash.utils.Timer;
import flash.utils.setTimeout;
import org.un.cava.birdeye.ravis.graphLayout.data.INode;
import org.un.cava.birdeye.ravis.graphLayout.visual.IVisualGraph;
import org.un.cava.birdeye.ravis.graphLayout.visual.IVisualNode;
import org.un.cava.birdeye.ravis.graphLayout.visual.events.VisualGraphEvent;
import org.un.cava.birdeye.ravis.utils.Geometry;
import org.un.cava.birdeye.ravis.utils.GraphicUtils;
import org.un.cava.birdeye.ravis.utils.LogUtil;
/**
* This subclass to the BaseLayouter encapsulates the methods
* for animation, since they are typically common in layouters.
* */
public class AnimatedBaseLayouter extends BaseLayouter implements ILayoutAlgorithm {
private static const _LOG:String = "graphLayout.layout.AnimatedBaseLayouter";
/**
* @internal
* Constant to define how often to redraw the node renderers during a redraw. It
* says update the node renderers everything X'th animation step. This is a performance
* optimization for diagrams with complicated node renderers
*/
private const _ANIMREFRESHINTERVAL:Number = 5;
/**
* constant to define the radial animation type, which
* interpolates node's polar coordinates.
* */
public const ANIM_RADIAL:int = 1;
/**
* constant to define the radial animation type, which
* interpolates node's polar coordinates.
* */
public const ANIM_STRAIGHT:int = 2;
/**
* @internal
* The amount of interpolation steps for the animation.
* 100 seems reasonable. The interpolation will be
* broken into exactly that amount of steps.
* */
private const _ANIMATIONSTEPS:int = 50;
/**
* @internal
* The timing of the animation steps is done using the arctan()
* function, to achieve a slow-in-slow-out effect. The input to the
* arctan() function is a range of the negative of this value
* to the positive of this value. The larger the interval, the
* longer the slow-in and slow-out part of the animation.
* We keep it rather short.
* */
private const _ANIMATIONTIMINGINTERVALSIZE:Number = 10; // -4 ; 4
/**
* @internal
* This is the maximum timer delay for each animation step.
* The animation steps will be timed between 0 ms
* and this value in ms. This value will be multiplied with the
* result of the current steps fraction of the timing interval
* (see above) to trigger the next animation step
* */
private const _MAXANIMTIMERDELAY:int = 100;
/**
* Indicator if there is currently an animation in progress
* */
protected var _animInProgress:Boolean = false;
private var _animationType:int = ANIM_RADIAL;
/**
* @internal
* the current step in the animation cycle */
private var _animStep:int;
/**
* @internal
* timer object for the animation */
private var _animTimer:Timer;
/**
* this holds the data for a layout drawing.
* */
private var _currentDrawing:BaseLayoutDrawing;
/**
* The constructor initializes the layouter and may assign
* already a VisualGraph object, but this can also be set later.
* @param vg The VisualGraph object on which this layouter should work on.
* */
public function AnimatedBaseLayouter(vg:IVisualGraph = null) {
super(vg);
_animInProgress = false;
}
/**
* @inheritDoc
* */
override public function get animInProgress():Boolean {
return _animInProgress;
}
/**
* @inheritDoc
* */
override public function resetAll():void {
super.resetAll();
killTimer();
}
/**
* @inheritDoc
* */
override protected function set currentDrawing(dr:BaseLayoutDrawing):void {
_currentDrawing = dr;
/* also set in the super class */
super.currentDrawing = dr;
}
/**
* Access to the type of animation, currently supported
* type is:
* ANIM_RADIAL which does interpolation of polar coordinates and
* ANIM_STRAIGHT which interpolates cartesian coordinates.
* @param type ANIM RADIAL or ANIM_STRAIGHT
* */
protected function set animationType(type:int):void {
_animationType = type;
}
/**
* This method kills any currently running timers
* which is needed if some data is going to be
* reinitialised. Remaining timer events could trigger
* code referring to stale data and crash the program
* otherwise.
* */
protected function killTimer():void {
if(_animTimer != null) {
//LogUtil.debug(_LOG, "timer killed");
_animTimer.stop();
_animTimer.reset();
//_animTimer = null;
}
}
/**
* Reset/Reinitialise animation related variables.
* */
protected function resetAnimation():void {
/* reset animation cycle */
_animStep = 0;
}
/**
* This method starts the animation according
* to the preset type in the _animationType
* variable. Currently only valid type is
* "RADIAL" which animates by interpolating
* polar coordinates. Other types like "LINEAR"
* (interpolating cartesian coordinates) may be
* added.
* */
protected function startAnimation():void {
var cyclefinished:Boolean;
if(!_disableAnimation) {
/* indicate an animation in progress */
_animInProgress = true;
switch(_animationType) {
case ANIM_RADIAL:
cyclefinished = interpolatePolarCoords();
break;
case ANIM_STRAIGHT:
cyclefinished = interpolateCartCoords();
break;
default:
LogUtil.warn(_LOG, "Illegal animation Type, default to ANIM_RADIAL");
cyclefinished = interpolatePolarCoords();
break;
}
/* make sure the edges are redrawn */
_layoutChanged = true;
if(_animStep == 0) {
dispatchEvent(new VisualGraphEvent(VisualGraphEvent.BEGIN_ANIMATION));
}
if(_animStep % _ANIMREFRESHINTERVAL == 0 || cyclefinished) {
_vgraph.refresh();
}
/* check if we ran out of anim cycles, but are not finished */
if (cyclefinished) {
//LogUtil.debug(_LOG, "Achieved final node positions, terminating animation...");
_animInProgress = false;
dispatchEvent(new VisualGraphEvent(VisualGraphEvent.END_ANIMATION));
} else if(_animStep >= _ANIMATIONSTEPS) {
LogUtil.info(_LOG, "Exceeded animation steps, setting nodes to final positions...");
applyTargetToNodes(_vgraph.visibleVNodes);
_animInProgress = false;
} else {
++_animStep;
startAnimTimer();
}
} else {
cyclefinished = setCoords();
if(cyclefinished == false)
{
//if we havent completed successfully
//due to not being fully initialized try
//again in 1 mili seconds
setTimeout(startAnimation,1);
}
else
{
_animInProgress = false;
/* make sure the edges are redrawn */
_layoutChanged = true;
_vgraph.refresh();
//_vgraph.redrawNodes();
}
}
}
/**
* Interpolates the target Polar coordinates with the current real coordinates
* achieving a smooth animation.
*
* This method always executes only one step as part of the animation.
* For each node, it's current coordinates are requested (and translated
* into the relative polar coordinates. From the drawing object, the target
* coordinates are taken and divided by the remaining interpolation steps.
* With that the coordinates of the current interpolation step can be
* calculated, which are subsequently directly applied to the node.
* The method then checks if the animation target coordinates have been
* reached. If not, and there are no more animation steps left, the
* actual target coordinates are applied.
* If there are animation steps left, a timer is started to call
* the same function again for the next animation step.
* */
protected function interpolatePolarCoords():Boolean {
var visVNodes:Array;
var vn:IVisualNode;
var n:INode;
var i:int;
var currRadius:Number;
var currPhi:Number;
var currPoint:Point;
var targetRadius:Number;
var targetPhi:Number;
var deltaRadius:Number;
var deltaPhi:Number;
var stepRadius:Number;
var stepPhi:Number;
var stepPoint:Point;
var cyclefinished:Boolean;
cyclefinished = true; // init to true, if any one node is not target, will be set to false
/* careful for invisible nodes, the values are not
* calculated (obviously), so we need to make sure
* to exclude them */
visVNodes = _vgraph.visibleVNodes;
for each(vn in visVNodes) {
/* should be visible otherwise somethings wrong */
if(!vn.isVisible) {
throw Error("received invisible vnode from list of visible vnodes");
}
n = vn.node;
/* get relative target coordinates in polar form */
targetRadius = _currentDrawing.getPolarR(n);
targetPhi = _currentDrawing.getPolarPhi(n);
/* when we get the current values, we have to make sure
* that we convert the coordinates into relative ones,
* i.e. we need to subtract the origin */
n.vnode.refresh();
currPoint = new Point(vn.x, vn.y);
currPoint = currPoint.subtract(_currentDrawing.originOffset);
if(_currentDrawing.centeredLayout) {
currPoint = currPoint.subtract(_currentDrawing.centerOffset);
}
currRadius = Geometry.polarRadius(currPoint);
currPhi = Geometry.polarAngleDeg(currPoint);
/* not sure if this really fixes the animation end cycle ... */
deltaRadius = (targetRadius - currRadius) * _animStep / _ANIMATIONSTEPS;
/* New logic for interpolating polar angles
* Take the minimum angle to the final position */
if ( Math.abs(targetPhi - currPhi) < (360 - Math.abs(targetPhi - currPhi)) ) {
deltaPhi = (targetPhi - currPhi) * _animStep / _ANIMATIONSTEPS;
} else { // difference b/w initial and final angles more than 180 degrees
if (targetPhi < currPhi) {
//crossing over from a very large angle to a very small angle
deltaPhi = (360 + targetPhi - currPhi) * _animStep / _ANIMATIONSTEPS;
} else {
//crossing over from a very small angle to a very large angle
deltaPhi = (targetPhi - currPhi - 360) * _animStep / _ANIMATIONSTEPS;
}
}
/* calculate the intermediate coordinates */
stepRadius = currRadius + deltaRadius;
stepPhi = currPhi + deltaPhi;
/* check if we are already done or not */
if(!GraphicUtils.equal(currPoint, _currentDrawing.getRelCartCoordinates(n))) {
cyclefinished = false;
}
/* we cannot set the coordinates in the _currentDrawing,
* as we store our target coordinates there,
* we need to set them directly in the vnode */
stepPoint = Geometry.cartFromPolarDeg(stepRadius,stepPhi);
/* adjust the origin */
stepPoint = stepPoint.add(_currentDrawing.originOffset);
/* here we may need to add the center offset */
if(_currentDrawing.centeredLayout) {
stepPoint = stepPoint.add(_currentDrawing.centerOffset);
}
/*
LogUtil.debug(_LOG, "interpolating node:"+n.id+" cP:"+currPoint.toString()+" cr:"+currRadius+" cp:"+currPhi+
" tr:"+targetRadius+" tp:"+targetPhi+" sP:"+stepPoint.toString()+" sr:"+stepRadius+
" sp:"+stepPhi);
*/
/* set into the vnode */
vn.x = stepPoint.x;
vn.y = stepPoint.y;
/* commit, i.e. move the node */
vn.commit();
}
return cyclefinished;
}
/**
* Interpolates the target coordinates with the current real coordinates
* achieving a smooth animation.
* It works in the same way as interpolatePolarCoords() but
* uses cartesian coordinates.
* @see interpolatePolarCoords()
* */
protected function interpolateCartCoords():Boolean {
var visVNodes:Array;
var vn:IVisualNode;
var n:INode;
var i:int;
var currPoint:Point;
var deltaPoint:Point;
var targetPoint:Point;
var cyclefinished:Boolean;
cyclefinished = true; // init to true, if any one node is not target, will be set to false
/* careful for invisible nodes, the values are not
* calculated (obviously), so we need to make sure
* to exclude them */
visVNodes = _vgraph.visibleVNodes;
for each(vn in visVNodes) {
/* should be visible otherwise somethings wrong */
if(!vn.isVisible) {
throw Error("received invisible vnode from list of visible vnodes");
}
n = vn.node;
/* get abs target coordinates in cartesian form */
targetPoint = _currentDrawing.getAbsCartCoordinates(n);
/* when we get the current values, we have to make sure
* that we convert the coordinates into relative ones,
* i.e. we need to subtract the origin */
n.vnode.refresh();
currPoint = new Point(vn.x, vn.y);
/* check if we are already done or not */
if(!GraphicUtils.equal(currPoint, targetPoint)) {
cyclefinished = false;
}
deltaPoint = new Point( (targetPoint.x - currPoint.x) * _animStep / _ANIMATIONSTEPS,
(targetPoint.y - currPoint.y) * _animStep / _ANIMATIONSTEPS);
currPoint = currPoint.add(deltaPoint);
/* set into the vnode */
vn.x = currPoint.x;
vn.y = currPoint.y;
/* commit, i.e. move the node */
vn.commit();
}
return cyclefinished;
}
/**
* Directly sets the target coordinates not animating anything.
* Will be used if the disableAnimation flag is set.
* */
protected function setCoords():Boolean {
var visVNodes:Array;
var vn:IVisualNode;
var n:INode;
var targetPoint:Point;
/* careful for invisible nodes, the values are not
* calculated (obviously), so we need to make sure
* to exclude them */
visVNodes = _vgraph.visibleVNodes;
for each(vn in visVNodes) {
/* should be visible otherwise somethings wrong */
if(!vn.isVisible) {
throw Error("received invisible vnode from list of visible vnodes");
}
/* check if we are already done or not */
if(vn.view.initialized == false ) {
return false;
}
}
for each(vn in visVNodes) {
n = vn.node;
/* get relative target coordinates in cartesian form */
targetPoint = _currentDrawing.getAbsCartCoordinates(n);
/* set into the vnode */
vn.x = targetPoint.x;
vn.y = targetPoint.y;
/* commit, i.e. move the node */
vn.commit();
}
return true;
}
/**
* @internal
* This calculates the timer delay for a slow-in / slow-out
* animation in each animation step.
* */
private function startAnimTimer():void {
var timerdelay:Number;
var factor:Number;
var signedAnimStep:int;
var factorinput:Number;
/* modify the current animation step to range from -/+ around 0 */
signedAnimStep = _animStep - (_ANIMATIONSTEPS / 2); // so we should be at 0 at the middle
/* this is the input into into the atan() function, which depends on the
* timing interval and the current signed animation step */
factorinput = _ANIMATIONTIMINGINTERVALSIZE * (signedAnimStep / _ANIMATIONSTEPS);
//LogUtil.debug(_LOG, "factor input:"+factorinput);
/* calculate the timing factor using the atan() function
* since we take the absolute value,
* its range goes from PI / 2 to 0 back to PI / 2 */
factor = Math.abs(Math.atan(factorinput));
//LogUtil.debug(_LOG, "factor fraction of PI:"+(factor / Math.PI));
/* now the delay for our timer is now the factors fraction
* of PI/2 times the maximum timer delay, i.e. the full timer
* delay if the factor has a value of PI / 2 */
timerdelay = (factor / (Math.PI / 2)) * _MAXANIMTIMERDELAY;
//LogUtil.debug(_LOG, "Setting timerdelay to:"+timerdelay+" milliseconds in step:"+_animStep);
/* now creating the new timer with the specified delay
* and ask for one execution, then the event handler will be
* called, which does nothing except to call the interpolation
* method again */
if (_animTimer == null) {
_animTimer = new Timer(timerdelay, 1);
_animTimer.addEventListener(TimerEvent.TIMER_COMPLETE, animTimerFired);
} else {
_animTimer.stop();
if (timerdelay > 0) _animTimer.delay = timerdelay;
_animTimer.reset();
}
_animTimer.start();
}
/**
* @internal
* Event handler when the timer fired, just calls the
* interpolation function again to do another animation
* cycle.
* @param event The fired timer event, will be ignored anyway.
* */
private function animTimerFired(event:TimerEvent = null):void {
//LogUtil.debug(_LOG, "Timer fired!");
startAnimation();
//event.updateAfterEvent();
}
}
}