package flare.vis.operator.layout {
import flare.physics.Particle;
import flare.physics.Simulation;
import flare.physics.Spring;
import flare.vis.data.Data;
import flare.vis.data.DataSprite;
import flare.vis.data.EdgeSprite;
import flare.vis.data.NodeSprite;
/**
* Layout that places nodes based on a physics simulation of
* interacting forces. By default, nodes repel each other, edges act as
* springs, and drag forces (similar to air resistance) are applied. This
* algorithm can be run for multiple iterations for a run-once layout
* computation or repeatedly run in an animated fashion for a dynamic and
* interactive layout (set Visualization.continuousUpdates = true
*
).
*
*
The running time of this layout algorithm is the greater of O(N log N)
* and O(E), where N is the number of nodes and E the number of edges.
* The addition of custom forces to the simulation may affect this.
*
* The force directed layout is implemented using the physics simulator
* provided by the flare.physics
package. The
* Simulation
used to drive this layout can be set explicitly,
* allowing any number of custom force directed layouts to be created
* through the selection of IForce
modules. Each node in the
* layout is mapped to a Particle
instance and each edge
* to a Spring
in the simulation. Once the simulation has been
* initialized, you can retrieve these instances through the
* node.props.particle
and edge.props.spring
* properties, respectively.
*
* @see flare.physics
*/
public class ForceDirectedLayout extends Layout
{
private var _sim:Simulation;
private var _step:Number = 1;
private var _iter:int = 1;
private var _gen:uint = 0;
private var _enforceBounds:Boolean = false;
// simulation defaults
private var _mass:Number = 1;
private var _restLength:Number = 30;
private var _tension:Number = 0.3;
private var _damping:Number = 0.1;
/** The default mass value for node/particles. */
public function get defaultParticleMass():Number { return _mass; }
public function set defaultParticleMass(v:Number):void { _mass = v; }
/** The default spring rest length for edge/springs. */
public function get defaultSpringLength():Number { return _restLength; }
public function set defaultSpringLength(v:Number):void { _restLength = v; }
/** The default spring tension for edge/springs. */
public function get defaultSpringTension():Number { return _tension; }
public function set defaultSpringTension(v:Number):void { _tension = v; }
/** The number of iterations to run the simulation per invocation
* (default is 1, expecting continuous updates). */
public function get iterations():int { return _iter; }
public function set iterations(iter:int):void { _iter = iter; }
/** The number of time ticks to advance the simulation on each
* iteration (default is 1). */
public function get ticksPerIteration():int { return _step; }
public function set ticksPerIteration(ticks:int):void { _step = ticks; }
/** The physics simulation driving this layout. */
public function get simulation():Simulation { return _sim; }
/** Flag indicating if the layout bounds should be enforced.
* If true, the layoutBounds will limit node placement. */
public function get enforceBounds():Boolean { return _enforceBounds; }
public function set enforceBounds(b:Boolean):void { _enforceBounds = b; }
// --------------------------------------------------------------------
/**
* Creates a new ForceDirectedLayout.
* @param iterations the number of iterations to run the simulation
* per invocation
* @param sim the physics simulation to use for the layout. If null
* (the default), default simulation settings will be used
*/
public function ForceDirectedLayout(enforceBounds:Boolean=false,
iterations:int=1, sim:Simulation=null)
{
_enforceBounds = enforceBounds;
_iter = iterations;
_sim = (sim==null ? new Simulation(0, 0, 0.1, -10) : sim);
}
/** @inheritDoc */
protected override function layout():void
{
++_gen; // update generation counter
init(); // populate simulation
// run simulation
_sim.bounds = _enforceBounds ? layoutBounds : null;
for (var i:uint=0; i<_iter; ++i) {
_sim.tick(_step);
}
visualization.data.nodes.visit(update); // update positions
updateEdgePoints(_t);
}
// -- value transfer --------------------------------------------------
/**
* Transfer the physics simulation results to an item's layout.
* @param d a DataSprite
* @return true, to signal a visitor to continue
*/
protected function update(d:DataSprite):void
{
var p:Particle = d.props.particle;
if (!p.fixed) {
var o:Object = _t.$(d);
o.x = p.x;
o.y = p.y;
}
}
// -- simulation management -------------------------------------------
/**
* Initializes the Simulation for this ForceDirectedLayout
*/
protected function init():void
{
var data:Data = visualization.data, o:Object;
var p:Particle, s:Spring, n:NodeSprite, e:EdgeSprite;
// initialize all simulation entries
for each (n in data.nodes) {
p = n.props.particle;
o = _t.$(n);
if (p == null) {
n.props.particle = (p = _sim.addParticle(_mass, o.x, o.y));
p.fixed = o.fixed;
} else {
p.x = o.x;
p.y = o.y;
p.fixed = o.fixed;
}
p.tag = _gen;
}
for each (e in data.edges) {
s = e.props.spring;
if (s == null) {
e.props.spring = (s = _sim.addSpring(
e.source.props.particle, e.target.props.particle,
_restLength, _tension, _damping));
}
s.tag = _gen;
}
// set up simulation parameters
// this needs to be kept separate from the above initialization
// to ensure all simulation items are created first
if (mass != null) {
for each (n in data.nodes) {
p = n.props.particle;
p.mass = mass(n);
}
}
for each (e in data.edges) {
s = e.props.spring;
if (restLength != null)
s.restLength = restLength(e);
if (tension != null)
s.tension = tension(e);
if (damping != null)
s.damping = damping(e);
}
// clean-up unused items
for each (p in _sim.particles)
if (p.tag != _gen) p.kill();
for each (s in _sim.springs)
if (s.tag != _gen) s.kill();
}
/**
* Function for assigning mass values to particles. By default, this
* simply returns the default mass value. This function can be replaced
* to perform custom mass assignment.
*/
public var mass:Function = function(d:DataSprite):Number {
return _mass;
}
/**
* Function for assigning rest length values to springs. By default,
* this simply returns the default rest length value. This function can
* be replaced to perform custom rest length assignment.
*/
public var restLength:Function = function(e:EdgeSprite):Number {
return _restLength;
}
/**
* Function for assigning tension values to springs. By default, this
* method computes spring tension adaptively, based on the connectivity
* of the attached particles, to create more stable layouts. More
* specifically, the tension is computed as the default tension value
* divided by the square root of the maximum degree of the attached
* particles. This function can be replaced to perform custom tension
* assignment.
*/
public var tension:Function = function(e:EdgeSprite):Number {
var s:Spring = Spring(e.props.spring);
var n:Number = Math.max(s.p1.degree, s.p2.degree);
return _tension / Math.sqrt(n);
}
/**
* Function for assigning damping constant values to springs. By
* default, this simply uses the spring's computed tension value
* divided by 10. This function can be replaced to perform custom
* damping assignment.
*/
public var damping:Function = function(e:EdgeSprite):Number {
return Spring(e.props.spring).tension / 10;
}
} // end of class ForceDirectedLayout
}