/**
* $Id: mxFastOrganicLayout.java,v 1.28 2011-04-28 13:10:32 david Exp $
* Copyright (c) 2007, Gaudenz Alder
*/
package com.mxgraph.layout;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.view.mxGraph;
/**
* Fast organic layout algorithm.
*/
public class mxFastOrganicLayout extends mxGraphLayout
{
/**
* Specifies if the top left corner of the input cells should be the origin
* of the layout result. Default is true.
*/
protected boolean useInputOrigin = true;
/**
* Specifies if all edge points of traversed edges should be removed.
* Default is true.
*/
protected boolean resetEdges = true;
/**
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
protected boolean disableEdgeStyle = true;
/**
* The force constant by which the attractive forces are divided and the
* replusive forces are multiple by the square of. The value equates to the
* average radius there is of free space around each node. Default is 50.
*/
protected double forceConstant = 50;
/**
* Cache of <forceConstant>^2 for performance.
*/
protected double forceConstantSquared = 0;
/**
* Minimal distance limit. Default is 2. Prevents of
* dividing by zero.
*/
protected double minDistanceLimit = 2;
/**
* Cached version of <minDistanceLimit> squared.
*/
protected double minDistanceLimitSquared = 0;
/**
* The maximum distance between vertices, beyond which their
* repulsion no longer has an effect
*/
protected double maxDistanceLimit = 500;
/**
* Start value of temperature. Default is 200.
*/
protected double initialTemp = 200;
/**
* Temperature to limit displacement at later stages of layout.
*/
protected double temperature = 0;
/**
* Total number of iterations to run the layout though.
*/
protected int maxIterations = 0;
/**
* Current iteration count.
*/
protected int iteration = 0;
/**
* An array of all vertices to be laid out.
*/
protected Object[] vertexArray;
/**
* An array of locally stored X co-ordinate displacements for the vertices.
*/
protected double[] dispX;
/**
* An array of locally stored Y co-ordinate displacements for the vertices.
*/
protected double[] dispY;
/**
* An array of locally stored co-ordinate positions for the vertices.
*/
protected double[][] cellLocation;
/**
* The approximate radius of each cell, nodes only.
*/
protected double[] radius;
/**
* The approximate radius squared of each cell, nodes only.
*/
protected double[] radiusSquared;
/**
* Array of booleans representing the movable states of the vertices.
*/
protected boolean[] isMoveable;
/**
* Local copy of cell neighbours.
*/
protected int[][] neighbours;
/**
* Boolean flag that specifies if the layout is allowed to run. If this is
* set to false, then the layout exits in the following iteration.
*/
protected boolean allowedToRun = true;
/**
* Maps from vertices to indices.
*/
protected Hashtable<Object, Integer> indices = new Hashtable<Object, Integer>();
/**
* Constructs a new fast organic layout for the specified graph.
*/
public mxFastOrganicLayout(mxGraph graph)
{
super(graph);
}
/**
* Returns a boolean indicating if the given <mxCell> should be ignored as a
* vertex. This returns true if the cell has no connections.
*
* @param vertex Object that represents the vertex to be tested.
* @return Returns true if the vertex should be ignored.
*/
public boolean isVertexIgnored(Object vertex)
{
return super.isVertexIgnored(vertex)
|| graph.getConnections(vertex).length == 0;
}
/**
*
*/
public boolean isUseInputOrigin()
{
return useInputOrigin;
}
/**
*
* @param value
*/
public void setUseInputOrigin(boolean value)
{
useInputOrigin = value;
}
/**
*
*/
public boolean isResetEdges()
{
return resetEdges;
}
/**
*
* @param value
*/
public void setResetEdges(boolean value)
{
resetEdges = value;
}
/**
*
*/
public boolean isDisableEdgeStyle()
{
return disableEdgeStyle;
}
/**
*
* @param value
*/
public void setDisableEdgeStyle(boolean value)
{
disableEdgeStyle = value;
}
/**
*
*/
public int getMaxIterations()
{
return maxIterations;
}
/**
*
* @param value
*/
public void setMaxIterations(int value)
{
maxIterations = value;
}
/**
*
*/
public double getForceConstant()
{
return forceConstant;
}
/**
*
* @param value
*/
public void setForceConstant(double value)
{
forceConstant = value;
}
/**
*
*/
public double getMinDistanceLimit()
{
return minDistanceLimit;
}
/**
*
* @param value
*/
public void setMinDistanceLimit(double value)
{
minDistanceLimit = value;
}
/**
* @return the maxDistanceLimit
*/
public double getMaxDistanceLimit()
{
return maxDistanceLimit;
}
/**
* @param maxDistanceLimit the maxDistanceLimit to set
*/
public void setMaxDistanceLimit(double maxDistanceLimit)
{
this.maxDistanceLimit = maxDistanceLimit;
}
/**
*
*/
public double getInitialTemp()
{
return initialTemp;
}
/**
*
* @param value
*/
public void setInitialTemp(double value)
{
initialTemp = value;
}
/**
* Reduces the temperature of the layout from an initial setting in a linear
* fashion to zero.
*/
protected void reduceTemperature()
{
temperature = initialTemp * (1.0 - iteration / maxIterations);
}
/* (non-Javadoc)
* @see com.mxgraph.layout.mxIGraphLayout#move(java.lang.Object, double, double)
*/
public void moveCell(Object cell, double x, double y)
{
// TODO: Map the position to a child index for
// the cell to be placed closest to the position
}
/* (non-Javadoc)
* @see com.mxgraph.layout.mxIGraphLayout#execute(java.lang.Object)
*/
public void execute(Object parent)
{
mxIGraphModel model = graph.getModel();
// Finds the relevant vertices for the layout
Object[] vertices = graph.getChildVertices(parent);
List<Object> tmp = new ArrayList<Object>(vertices.length);
for (int i = 0; i < vertices.length; i++)
{
if (!isVertexIgnored(vertices[i]))
{
tmp.add(vertices[i]);
}
}
vertexArray = tmp.toArray();
mxRectangle initialBounds = (useInputOrigin) ? graph.getBoundsForCells(
vertexArray, false, false, true) : null;
int n = vertexArray.length;
dispX = new double[n];
dispY = new double[n];
cellLocation = new double[n][];
isMoveable = new boolean[n];
neighbours = new int[n][];
radius = new double[n];
radiusSquared = new double[n];
minDistanceLimitSquared = minDistanceLimit * minDistanceLimit;
if (forceConstant < 0.001)
{
forceConstant = 0.001;
}
forceConstantSquared = forceConstant * forceConstant;
// Create a map of vertices first. This is required for the array of
// arrays called neighbours which holds, for each vertex, a list of
// ints which represents the neighbours cells to that vertex as
// the indices into vertexArray
for (int i = 0; i < vertexArray.length; i++)
{
Object vertex = vertexArray[i];
cellLocation[i] = new double[2];
// Set up the mapping from array indices to cells
indices.put(vertex, new Integer(i));
mxRectangle bounds = getVertexBounds(vertex);
// Set the X,Y value of the internal version of the cell to
// the center point of the vertex for better positioning
double width = bounds.getWidth();
double height = bounds.getHeight();
// Randomize (0, 0) locations
double x = bounds.getX();
double y = bounds.getY();
cellLocation[i][0] = x + width / 2.0;
cellLocation[i][1] = y + height / 2.0;
radius[i] = Math.min(width, height);
radiusSquared[i] = radius[i] * radius[i];
}
// Moves cell location back to top-left from center locations used in
// algorithm, resetting the edge points is part of the transaction
model.beginUpdate();
try
{
for (int i = 0; i < n; i++)
{
dispX[i] = 0;
dispY[i] = 0;
isMoveable[i] = isVertexMovable(vertexArray[i]);
// Get lists of neighbours to all vertices, translate the cells
// obtained in indices into vertexArray and store as an array
// against the original cell index
Object[] edges = graph.getConnections(vertexArray[i], parent);
for (int k = 0; k < edges.length; k++)
{
if (isResetEdges())
{
graph.resetEdge(edges[k]);
}
if (isDisableEdgeStyle())
{
setEdgeStyleEnabled(edges[k], false);
}
}
Object[] cells = graph.getOpposites(edges, vertexArray[i]);
neighbours[i] = new int[cells.length];
for (int j = 0; j < cells.length; j++)
{
Integer index = indices.get(cells[j]);
// Check the connected cell in part of the vertex list to be
// acted on by this layout
if (index != null)
{
neighbours[i][j] = index.intValue();
}
// Else if index of the other cell doesn't correspond to
// any cell listed to be acted upon in this layout. Set
// the index to the value of this vertex (a dummy self-loop)
// so the attraction force of the edge is not calculated
else
{
neighbours[i][j] = i;
}
}
}
temperature = initialTemp;
// If max number of iterations has not been set, guess it
if (maxIterations == 0)
{
maxIterations = (int) (20 * Math.sqrt(n));
}
// Main iteration loop
for (iteration = 0; iteration < maxIterations; iteration++)
{
if (!allowedToRun)
{
return;
}
// Calculate repulsive forces on all vertices
calcRepulsion();
// Calculate attractive forces through edges
calcAttraction();
calcPositions();
reduceTemperature();
}
Double minx = null;
Double miny = null;
for (int i = 0; i < vertexArray.length; i++)
{
Object vertex = vertexArray[i];
mxGeometry geo = model.getGeometry(vertex);
if (geo != null)
{
cellLocation[i][0] -= geo.getWidth() / 2.0;
cellLocation[i][1] -= geo.getHeight() / 2.0;
double x = graph.snap(cellLocation[i][0]);
double y = graph.snap(cellLocation[i][1]);
setVertexLocation(vertex, x, y);
if (minx == null)
{
minx = new Double(x);
}
else
{
minx = new Double(Math.min(minx.doubleValue(), x));
}
if (miny == null)
{
miny = new Double(y);
}
else
{
miny = new Double(Math.min(miny.doubleValue(), y));
}
}
}
// Modifies the cloned geometries in-place. Not needed
// to clone the geometries again as we're in the same
// undoable change.
double dx = (minx != null) ? -minx.doubleValue() - 1 : 0;
double dy = (miny != null) ? -miny.doubleValue() - 1 : 0;
if (initialBounds != null)
{
dx += initialBounds.getX();
dy += initialBounds.getY();
}
graph.moveCells(vertexArray, dx, dy);
}
finally
{
model.endUpdate();
}
}
/**
* Takes the displacements calculated for each cell and applies them to the
* local cache of cell positions. Limits the displacement to the current
* temperature.
*/
protected void calcPositions()
{
for (int index = 0; index < vertexArray.length; index++)
{
if (isMoveable[index])
{
// Get the distance of displacement for this node for this
// iteration
double deltaLength = Math.sqrt(dispX[index] * dispX[index]
+ dispY[index] * dispY[index]);
if (deltaLength < 0.001)
{
deltaLength = 0.001;
}
// Scale down by the current temperature if less than the
// displacement distance
double newXDisp = dispX[index] / deltaLength
* Math.min(deltaLength, temperature);
double newYDisp = dispY[index] / deltaLength
* Math.min(deltaLength, temperature);
// reset displacements
dispX[index] = 0;
dispY[index] = 0;
// Update the cached cell locations
cellLocation[index][0] += newXDisp;
cellLocation[index][1] += newYDisp;
}
}
}
/**
* Calculates the attractive forces between all laid out nodes linked by
* edges
*/
protected void calcAttraction()
{
// Check the neighbours of each vertex and calculate the attractive
// force of the edge connecting them
for (int i = 0; i < vertexArray.length; i++)
{
for (int k = 0; k < neighbours[i].length; k++)
{
// Get the index of the othe cell in the vertex array
int j = neighbours[i][k];
// Do not proceed self-loops
if (i != j)
{
double xDelta = cellLocation[i][0] - cellLocation[j][0];
double yDelta = cellLocation[i][1] - cellLocation[j][1];
// The distance between the nodes
double deltaLengthSquared = xDelta * xDelta + yDelta
* yDelta - radiusSquared[i] - radiusSquared[j];
if (deltaLengthSquared < minDistanceLimitSquared)
{
deltaLengthSquared = minDistanceLimitSquared;
}
double deltaLength = Math.sqrt(deltaLengthSquared);
double force = (deltaLengthSquared) / forceConstant;
double displacementX = (xDelta / deltaLength) * force;
double displacementY = (yDelta / deltaLength) * force;
if (isMoveable[i])
{
this.dispX[i] -= displacementX;
this.dispY[i] -= displacementY;
}
if (isMoveable[j])
{
dispX[j] += displacementX;
dispY[j] += displacementY;
}
}
}
}
}
/**
* Calculates the repulsive forces between all laid out nodes
*/
protected void calcRepulsion()
{
int vertexCount = vertexArray.length;
for (int i = 0; i < vertexCount; i++)
{
for (int j = i; j < vertexCount; j++)
{
// Exits if the layout is no longer allowed to run
if (!allowedToRun)
{
return;
}
if (j != i)
{
double xDelta = cellLocation[i][0] - cellLocation[j][0];
double yDelta = cellLocation[i][1] - cellLocation[j][1];
if (xDelta == 0)
{
xDelta = 0.01 + Math.random();
}
if (yDelta == 0)
{
yDelta = 0.01 + Math.random();
}
// Distance between nodes
double deltaLength = Math.sqrt((xDelta * xDelta)
+ (yDelta * yDelta));
double deltaLengthWithRadius = deltaLength - radius[i]
- radius[j];
if (deltaLengthWithRadius > maxDistanceLimit)
{
// Ignore vertices too far apart
continue;
}
if (deltaLengthWithRadius < minDistanceLimit)
{
deltaLengthWithRadius = minDistanceLimit;
}
double force = forceConstantSquared / deltaLengthWithRadius;
double displacementX = (xDelta / deltaLength) * force;
double displacementY = (yDelta / deltaLength) * force;
if (isMoveable[i])
{
dispX[i] += displacementX;
dispY[i] += displacementY;
}
if (isMoveable[j])
{
dispX[j] -= displacementX;
dispY[j] -= displacementY;
}
}
}
}
}
}