package uk.ac.rhul.cs.graph;
import java.util.List;
/**
* Algorithm that places the vertices of a graph according to the
* Fruchterman-Reingold layout algorithm.
*
* @author ntamas
*/
public class FruchtermanReingoldLayoutAlgorithm extends GraphLayoutAlgorithm {
/**
* Number of iterations to perform
*/
private int iterationCount = 500;
/**
* Cooling exponent
*/
private double coolingExponent = 0.99;
/**
* Class holding local data associated to a graph node in the FR layout algorithm
*/
private class NodeData {
double x;
double y;
double dx;
double dy;
public NodeData() {
x = Math.random();
y = Math.random();
dx = 0;
dy = 0;
}
public void addDxDy(double dx0, double dy0) {
this.dx += dx0;
this.dy += dy0;
}
public double getVelocity() {
return Math.sqrt(dx * dx + dy * dy);
}
public void scaleDxDy(double factor) {
dx *= factor; dy *= factor;
}
public void move() {
x += dx; y += dy;
dx = 0; dy = 0;
}
}
/**
* Constructs an FR-layout instance not associated to any graph
*/
public FruchtermanReingoldLayoutAlgorithm() {
super();
}
/**
* Constructs an FR-layout instance associated to the given graph
*/
public FruchtermanReingoldLayoutAlgorithm(Graph graph) {
super(graph);
}
/**
* Returns the number of FR iterations to perform in the layout algorithm
*/
public int getIterationCount() {
return iterationCount;
}
/**
* Returns the calculated layout
*/
public Layout getResults() {
int numberOfNodes = graph.getNodeCount();
double maxDelta = numberOfNodes;
double repulseRadius = iterationCount * maxDelta;
double area = maxDelta * maxDelta;
double frk = Math.sqrt(area / numberOfNodes);
int i, j;
double xd, yd, ded, force;
if (numberOfNodes == 0)
return new Layout(graph);
// Initialize node data
NodeData[] nodeDataArray = new NodeData[numberOfNodes];
for (i = 0; i < numberOfNodes; i++) {
nodeDataArray[i] = new NodeData();
}
// Convert the graph to a list of edges and normalize weights
List<Edge> edges = graph.getEdgeList();
xd = 0;
for (Edge edge: edges) {
if (edge.weight < 0)
edge.weight = 1;
if (edge.weight > xd)
xd = edge.weight;
}
for (Edge edge: edges) {
edge.weight /= xd;
}
for (int iter = 0; iter < iterationCount; iter++) {
// Set the temperature
double t = maxDelta * Math.pow(1 - (iter / (double)iterationCount), coolingExponent);
// Calculate repulsive forces for each undirected vertex pair
for (i = 0; i < numberOfNodes; i++) {
NodeData firstNode = nodeDataArray[i];
for (j = i+1; j < numberOfNodes; j++) {
NodeData secondNode = nodeDataArray[j];
xd = firstNode.x - secondNode.x;
yd = firstNode.y - secondNode.y;
ded = Math.sqrt(xd*xd + yd*yd);
if (ded == 0)
continue;
xd /= ded; yd /= ded;
force = frk * frk * (1.0 / ded - ded * ded / repulseRadius);
xd *= force; yd *= force;
firstNode.addDxDy(xd, yd);
secondNode.addDxDy(-xd, -yd);
}
}
// Calculate attraction forces for each edge
for (Edge edge: edges) {
NodeData firstNode = nodeDataArray[edge.source];
NodeData secondNode = nodeDataArray[edge.target];
xd = firstNode.x - secondNode.x;
yd = firstNode.y - secondNode.y;
ded = Math.sqrt(xd*xd + yd*yd);
if (ded == 0)
continue;
force = -ded * ded / frk * edge.weight;
xd *= force; yd *= force;
firstNode.addDxDy(xd, yd);
secondNode.addDxDy(-xd, -yd);
}
// Dampen motion and move the points
for (NodeData nodeData: nodeDataArray) {
ded = nodeData.getVelocity();
if (ded > t) {
// Dampen to t
nodeData.scaleDxDy(t/ded);
}
nodeData.move();
}
}
// Copy the results into the layout
Layout layout = new Layout(graph);
for (i = 0; i < numberOfNodes; i++) {
layout.setCoordinates(i, nodeDataArray[i].x, nodeDataArray[i].y);
}
return layout;
}
/**
* Sets the number of FR iterations to perform in the layout algorithm
*
* @param iterationCount the number of iterations to perform
*/
public void setIterationCount(int iterationCount) {
if (iterationCount >= 1)
this.iterationCount = iterationCount;
}
}