package prefuse.action.layout.graph;
import java.awt.geom.Rectangle2D;
import java.util.Iterator;
import java.util.Random;
import prefuse.action.layout.Layout;
import prefuse.data.Graph;
import prefuse.data.Schema;
import prefuse.data.tuple.TupleSet;
import prefuse.util.PrefuseLib;
import prefuse.visual.EdgeItem;
import prefuse.visual.NodeItem;
import prefuse.visual.VisualItem;
/**
* <p>Layout instance implementing the Fruchterman-Reingold algorithm for
* force-directed placement of graph nodes. The computational complexity of
* this algorithm is quadratic [O(n^2)] in the number of nodes, so should
* only be applied for relatively small graphs, particularly in interactive
* situations.</p>
*
* <p>This implementation was ported from the implementation in the
* <a href="http://jung.sourceforge.net/">JUNG</a> framework.</p>
*
* @author Scott White, Yan-Biao Boey, Danyel Fisher
* @author <a href="http://jheer.org">jeffrey heer</a>
*/
public class FruchtermanReingoldLayout extends Layout {
private double forceConstant;
private double temp;
private int maxIter = 700;
protected String m_nodeGroup;
protected String m_edgeGroup;
protected int m_fidx;
private static final double EPSILON = 0.000001D;
private static final double ALPHA = 0.1;
/**
* Create a new FruchtermanReingoldLayout.
* @param graph the data field to layout. Must resolve to a Graph instance.
*/
public FruchtermanReingoldLayout(String graph) {
this(graph, 700);
}
/**
* Create a new FruchtermanReingoldLayout
* @param graph the data field to layout. Must resolve to a Graph instance.
* @param maxIter the maximum number of iterations of the algorithm to run
*/
public FruchtermanReingoldLayout(String graph, int maxIter) {
super(graph);
m_nodeGroup = PrefuseLib.getGroupName(graph, Graph.NODES);
m_edgeGroup = PrefuseLib.getGroupName(graph, Graph.EDGES);
this.maxIter = maxIter;
}
/**
* Get the maximum number of iterations to run of this algorithm.
* @return the maximum number of iterations
*/
public int getMaxIterations() {
return maxIter;
}
/**
* Set the maximum number of iterations to run of this algorithm.
* @param maxIter the maximum number of iterations to use
*/
public void setMaxIterations(int maxIter) {
this.maxIter = maxIter;
}
/**
* @see prefuse.action.Action#run(double)
*/
public void run(double frac) {
Graph g = (Graph)m_vis.getGroup(m_group);
Rectangle2D bounds = super.getLayoutBounds();
init(g, bounds);
for (int curIter=0; curIter < maxIter; curIter++ ) {
// Calculate repulsion
for (Iterator iter = g.nodes(); iter.hasNext();) {
NodeItem n = (NodeItem)iter.next();
if (n.isFixed()) continue;
calcRepulsion(g, n);
}
// Calculate attraction
for (Iterator iter = g.edges(); iter.hasNext();) {
EdgeItem e = (EdgeItem) iter.next();
calcAttraction(e);
}
for (Iterator iter = g.nodes(); iter.hasNext();) {
NodeItem n = (NodeItem)iter.next();
if (n.isFixed()) continue;
calcPositions(n,bounds);
}
cool(curIter);
}
finish(g);
}
private void init(Graph g, Rectangle2D b) {
initSchema(g.getNodes());
temp = b.getWidth() / 10;
forceConstant = 0.75 *
Math.sqrt(b.getHeight()*b.getWidth()/g.getNodeCount());
// initialize node positions
Iterator nodeIter = g.nodes();
Random rand = new Random(42); // get a deterministic layout result
double scaleW = ALPHA*b.getWidth()/2;
double scaleH = ALPHA*b.getHeight()/2;
while ( nodeIter.hasNext() ) {
NodeItem n = (NodeItem)nodeIter.next();
Params np = getParams(n);
np.loc[0] = b.getCenterX() + rand.nextDouble()*scaleW;
np.loc[1] = b.getCenterY() + rand.nextDouble()*scaleH;
}
}
private void finish(Graph g) {
Iterator nodeIter = g.nodes();
while ( nodeIter.hasNext() ) {
NodeItem n = (NodeItem)nodeIter.next();
Params np = getParams(n);
setX(n, null, np.loc[0]);
setY(n, null, np.loc[1]);
}
}
public void calcPositions(NodeItem n, Rectangle2D b) {
Params np = getParams(n);
double deltaLength = Math.max(EPSILON,
Math.sqrt(np.disp[0]*np.disp[0] + np.disp[1]*np.disp[1]));
double xDisp = np.disp[0]/deltaLength * Math.min(deltaLength, temp);
if (Double.isNaN(xDisp)) {
System.err.println("Mathematical error... (calcPositions:xDisp)");
}
double yDisp = np.disp[1]/deltaLength * Math.min(deltaLength, temp);
np.loc[0] += xDisp;
np.loc[1] += yDisp;
// don't let nodes leave the display
double borderWidth = b.getWidth() / 50.0;
double x = np.loc[0];
if (x < b.getMinX() + borderWidth) {
x = b.getMinX() + borderWidth + Math.random() * borderWidth * 2.0;
} else if (x > (b.getMaxX() - borderWidth)) {
x = b.getMaxX() - borderWidth - Math.random() * borderWidth * 2.0;
}
double y = np.loc[1];
if (y < b.getMinY() + borderWidth) {
y = b.getMinY() + borderWidth + Math.random() * borderWidth * 2.0;
} else if (y > (b.getMaxY() - borderWidth)) {
y = b.getMaxY() - borderWidth - Math.random() * borderWidth * 2.0;
}
np.loc[0] = x;
np.loc[1] = y;
}
public void calcAttraction(EdgeItem e) {
NodeItem n1 = e.getSourceItem();
Params n1p = getParams(n1);
NodeItem n2 = e.getTargetItem();
Params n2p = getParams(n2);
double xDelta = n1p.loc[0] - n2p.loc[0];
double yDelta = n1p.loc[1] - n2p.loc[1];
double deltaLength = Math.max(EPSILON,
Math.sqrt(xDelta*xDelta + yDelta*yDelta));
double force = (deltaLength*deltaLength) / forceConstant;
if (Double.isNaN(force)) {
System.err.println("Mathematical error...");
}
double xDisp = (xDelta/deltaLength) * force;
double yDisp = (yDelta/deltaLength) * force;
n1p.disp[0] -= xDisp; n1p.disp[1] -= yDisp;
n2p.disp[0] += xDisp; n2p.disp[1] += yDisp;
}
public void calcRepulsion(Graph g, NodeItem n1) {
Params np = getParams(n1);
np.disp[0] = 0.0; np.disp[1] = 0.0;
for (Iterator iter2 = g.nodes(); iter2.hasNext();) {
NodeItem n2 = (NodeItem) iter2.next();
Params n2p = getParams(n2);
if (n2.isFixed()) continue;
if (n1 != n2) {
double xDelta = np.loc[0] - n2p.loc[0];
double yDelta = np.loc[1] - n2p.loc[1];
double deltaLength = Math.max(EPSILON,
Math.sqrt(xDelta*xDelta + yDelta*yDelta));
double force = (forceConstant*forceConstant) / deltaLength;
if (Double.isNaN(force)) {
System.err.println("Mathematical error...");
}
np.disp[0] += (xDelta/deltaLength)*force;
np.disp[1] += (yDelta/deltaLength)*force;
}
}
}
private void cool(int curIter) {
temp *= (1.0 - curIter / (double) maxIter);
}
// ------------------------------------------------------------------------
// Params Schema
/**
* The data field in which the parameters used by this layout are stored.
*/
public static final String PARAMS = "_fruchtermanReingoldParams";
/**
* The schema for the parameters used by this layout.
*/
public static final Schema PARAMS_SCHEMA = new Schema();
static {
PARAMS_SCHEMA.addColumn(PARAMS, Params.class);
}
protected void initSchema(TupleSet ts) {
try {
ts.addColumns(PARAMS_SCHEMA);
} catch ( IllegalArgumentException iae ) {};
}
private Params getParams(VisualItem item) {
Params rp = (Params)item.get(PARAMS);
if ( rp == null ) {
rp = new Params();
item.set(PARAMS, rp);
}
return rp;
}
/**
* Wrapper class holding parameters used for each node in this layout.
*/
public static class Params implements Cloneable {
double[] loc = new double[2];
double[] disp = new double[2];
}
} // end of class FruchtermanReingoldLayout