/*******************************************************************************
* Copyright (c) 2005, 2016 The Chisel Group and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors: Jingwei Wu, Rob Lintern, Casey Best, Ian Bull (The Chisel Group) - initial API and implementation
* Mateusz Matela - "Tree Views for Zest" contribution, Google Summer of Code 2009
* Matthias Wienand (itemis AG) - refactorings
* Alexander Nyßen (itemis AG) - refactorings
*
******************************************************************************/
package org.eclipse.gef.layout.algorithms;
import java.util.HashMap;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.geometry.planar.Rectangle;
import org.eclipse.gef.graph.Edge;
import org.eclipse.gef.graph.Node;
import org.eclipse.gef.layout.ILayoutAlgorithm;
import org.eclipse.gef.layout.LayoutContext;
import org.eclipse.gef.layout.LayoutProperties;
/**
* The SpringLayoutAlgorithm has its own data repository and relation
* repository. A user can populate the repository, specify the layout
* conditions, do the computation and query the computed results.
*
* @author Jingwei Wu
* @author Rob Lintern
* @author Casey Best
* @author Ian Bull
* @author Mateusz Matela
* @author mwienand
*/
public class SpringLayoutAlgorithm implements ILayoutAlgorithm {
/**
* The default value for the spring layout number of iterations.
*/
private static final int DEFAULT_SPRING_ITERATIONS = 1000;
/**
* the default value for the time algorithm runs.
*/
private static final long MAX_SPRING_TIME = 10000;
/**
* The default value for positioning nodes randomly.
*/
private static final boolean DEFAULT_SPRING_RANDOM = true;
/**
* The default value for the spring layout move-control.
*/
private static final double DEFAULT_SPRING_MOVE = 1.0f;
/**
* The default value for the spring layout strain-control.
*/
private static final double DEFAULT_SPRING_STRAIN = 1.0f;
/**
* The default value for the spring layout length-control.
*/
private static final double DEFAULT_SPRING_LENGTH = 3.0f;
/**
* The default value for the spring layout gravitation-control.
*/
private static final double DEFAULT_SPRING_GRAVITATION = 2.0f;
/**
* Minimum distance considered between nodes
*/
private static final double MIN_DISTANCE = 1.0d;
/**
* The variable can be customized to set the number of iterations used.
*/
private int sprIterations = DEFAULT_SPRING_ITERATIONS;
/**
* This variable can be customized to set the max number of MS the algorithm
* should run
*/
private long maxTimeMS = MAX_SPRING_TIME;
/**
* The variable can be customized to set whether or not the spring layout
* nodes are positioned randomly before beginning iterations.
*/
private boolean sprRandom = DEFAULT_SPRING_RANDOM;
/**
* The variable can be customized to set the spring layout move-control.
*/
private double sprMove = DEFAULT_SPRING_MOVE;
/**
* The variable can be customized to set the spring layout strain-control.
*/
private double sprStrain = DEFAULT_SPRING_STRAIN;
/**
* The variable can be customized to set the spring layout length-control.
*/
private double sprLength = DEFAULT_SPRING_LENGTH;
/**
* The variable can be customized to set the spring layout
* gravitation-control.
*/
private double sprGravitation = DEFAULT_SPRING_GRAVITATION;
/**
* Variable indicating whether the algorithm should resize elements.
*/
private boolean resize = false;
private int iteration;
private double[][] srcDestToSumOfWeights;
private Node[] entities;
private double[] forcesX, forcesY;
private double[] locationsX, locationsY;
private double[] sizeW, sizeH;
private Rectangle bounds;
private double boundsScaleX = 0.2;
private double boundsScaleY = 0.2;
// XXX: Needed by performNIteration(int), see below.
private LayoutContext layoutContext;
// TODO: expose field
private boolean fitWithinBounds = true;
public void applyLayout(LayoutContext layoutContext, boolean clean) {
this.layoutContext = layoutContext;
initLayout(layoutContext);
if (!clean) {
return;
}
while (performAnotherNonContinuousIteration()) {
computeOneIteration();
}
saveLocations();
if (resize)
AlgorithmHelper.maximizeSizes(entities);
if (fitWithinBounds) {
Rectangle bounds2 = new Rectangle(bounds);
int insets = 4;
bounds2.setX(bounds2.getX() + insets);
bounds2.setY(bounds2.getY() + insets);
bounds2.setWidth(bounds2.getWidth() - 2 * insets);
bounds2.setHeight(bounds2.getHeight() - 2 * insets);
AlgorithmHelper.fitWithinBounds(entities, bounds2, resize);
}
}
/**
* Performs the given number of iterations.
*
* @param n
* The number of iterations to perform.
*/
public void performNIteration(int n) {
layoutContext.preLayout();
if (iteration == 0) {
entities = layoutContext.getNodes();
loadLocations();
initLayout(layoutContext);
}
bounds = LayoutProperties.getBounds(layoutContext.getGraph());
for (int i = 0; i < n; i++) {
computeOneIteration();
saveLocations();
}
layoutContext.postLayout();
}
/**
* Performs one single iteration.
*
*/
public void performOneIteration() {
layoutContext.preLayout();
if (iteration == 0) {
entities = layoutContext.getNodes();
loadLocations();
initLayout(layoutContext);
}
bounds = LayoutProperties.getBounds(layoutContext.getGraph());
computeOneIteration();
saveLocations();
layoutContext.postLayout();
}
/**
*
* @return true if this algorithm is set to resize elements
*/
public boolean isResizing() {
return resize;
}
/**
*
* @param resizing
* true if this algorithm should resize elements (default is
* false)
*/
public void setResizing(boolean resizing) {
resize = resizing;
}
/**
* Sets the spring layout move-control.
*
* @param move
* The move-control value.
*/
public void setSpringMove(double move) {
sprMove = move;
}
/**
* Returns the move-control value of this SpringLayoutAlgorithm in double
* precision.
*
* @return The move-control value.
*/
public double getSpringMove() {
return sprMove;
}
/**
* Sets the spring layout strain-control.
*
* @param strain
* The strain-control value.
*/
public void setSpringStrain(double strain) {
sprStrain = strain;
}
/**
* Returns the strain-control value of this SpringLayoutAlgorithm in double
* precision.
*
* @return The strain-control value.
*/
public double getSpringStrain() {
return sprStrain;
}
/**
* Sets the spring layout length-control.
*
* @param length
* The length-control value.
*/
public void setSpringLength(double length) {
sprLength = length;
}
/**
* Gets the max time this algorithm will run for
*
* @return the timeout up to which this algorithm may run
*/
public long getSpringTimeout() {
return maxTimeMS;
}
/**
* Sets the spring timeout to the given value (in millis).
*
* @param timeout
* The new spring timeout (in millis).
*/
public void setSpringTimeout(long timeout) {
maxTimeMS = timeout;
}
/**
* Returns the length-control value of this {@link SpringLayoutAlgorithm} in
* double precision.
*
* @return The length-control value.
*/
public double getSpringLength() {
return sprLength;
}
/**
* Sets the spring layout gravitation-control.
*
* @param gravitation
* The gravitation-control value.
*/
public void setSpringGravitation(double gravitation) {
sprGravitation = gravitation;
}
/**
* Returns the gravitation-control value of this SpringLayoutAlgorithm in
* double precision.
*
* @return The gravitation-control value.
*/
public double getSpringGravitation() {
return sprGravitation;
}
/**
* Sets the number of iterations to be used.
*
* @param iterations
* The number of iterations.
*/
public void setIterations(int iterations) {
sprIterations = iterations;
}
/**
* Returns the number of iterations to be used.
*
* @return The number of iterations.
*/
public int getIterations() {
return sprIterations;
}
/**
* Sets whether or not this SpringLayoutAlgorithm will layout the nodes
* randomly before beginning iterations.
*
* @param random
* The random placement value.
*/
public void setRandom(boolean random) {
sprRandom = random;
}
/**
* Returns whether or not this {@link SpringLayoutAlgorithm} will layout the
* nodes randomly before beginning iterations.
*
* @return <code>true</code> if this algorithm will layout the nodes
* randomly before iterating, otherwise <code>false</code>.
*/
public boolean getRandom() {
return sprRandom;
}
private long startTime = 0;
private void initLayout(LayoutContext context) {
entities = context.getNodes();
bounds = LayoutProperties.getBounds(context.getGraph());
loadLocations();
srcDestToSumOfWeights = new double[entities.length][entities.length];
HashMap<Node, Integer> entityToPosition = new HashMap<>();
for (int i = 0; i < entities.length; i++) {
entityToPosition.put(entities[i], new Integer(i));
}
Edge[] connections = context.getEdges();
for (int i = 0; i < connections.length; i++) {
Edge connection = connections[i];
Integer source = entityToPosition.get(connection.getSource());
Integer target = entityToPosition.get(connection.getTarget());
if (source == null || target == null)
continue;
double weight = LayoutProperties.getWeight(connection);
weight = (weight <= 0 ? 0.1 : weight);
srcDestToSumOfWeights[source.intValue()][target
.intValue()] += weight;
srcDestToSumOfWeights[target.intValue()][source
.intValue()] += weight;
}
if (sprRandom)
placeRandomly(); // put vertices in random places
iteration = 1;
startTime = System.currentTimeMillis();
}
private void loadLocations() {
if (locationsX == null || locationsX.length != entities.length) {
int length = entities.length;
locationsX = new double[length];
locationsY = new double[length];
sizeW = new double[length];
sizeH = new double[length];
forcesX = new double[length];
forcesY = new double[length];
}
for (int i = 0; i < entities.length; i++) {
Point location = LayoutProperties.getLocation(entities[i]);
locationsX[i] = location.x;
locationsY[i] = location.y;
Dimension size = LayoutProperties.getSize(entities[i]);
sizeW[i] = size.width;
sizeH[i] = size.height;
}
}
private void saveLocations() {
if (entities == null)
return;
for (int i = 0; i < entities.length; i++) {
// TODO ensure no dynamic layout passes are triggered as a result of
// storing the positions
// TODO: check where NaN values originate from
if (Double.isNaN(locationsX[i]) || Double.isNaN(locationsY[i])) {
locationsX[i] = 0;
locationsY[i] = 0;
}
LayoutProperties.setLocation(entities[i],
new Point(locationsX[i], locationsY[i]));
}
}
/**
* Scales the current iteration counter based on how long the algorithm has
* been running for. You can set the MaxTime in maxTimeMS!
*/
private void setSprIterationsBasedOnTime() {
if (maxTimeMS <= 0)
return;
long currentTime = System.currentTimeMillis();
double fractionComplete = (double) ((double) (currentTime - startTime)
/ ((double) maxTimeMS));
int currentIteration = (int) (fractionComplete * sprIterations);
if (currentIteration > iteration) {
iteration = currentIteration;
}
}
/**
* Performs one iteration based on time.
*
* @return <code>true</code> if the maximum number of iterations was not
* reached yet, otherwise <code>false</code>.
*/
protected boolean performAnotherNonContinuousIteration() {
setSprIterationsBasedOnTime();
return (iteration <= sprIterations);
}
/**
* Returns the current iteration.
*
* @return The current iteration.
*/
protected int getCurrentLayoutStep() {
return iteration;
}
/**
* Returns the maximum number of iterations.
*
* @return The maximum number of iterations.
*/
protected int getTotalNumberOfLayoutSteps() {
return sprIterations;
}
/**
* Computes one iteration (forces, positions) and increases the iteration
* counter.
*/
protected void computeOneIteration() {
computeForces();
computePositions();
Rectangle currentBounds = getLayoutBounds();
improveBoundScaleX(currentBounds);
improveBoundScaleY(currentBounds);
moveToCenter(currentBounds);
iteration++;
}
/**
* Puts vertices in random places, all between (0,0) and (1,1).
*/
protected void placeRandomly() {
if (locationsX.length == 0) {
return;
}
// If only one node in the data repository, put it in the middle
if (locationsX.length == 1) {
// If only one node in the data repository, put it in the middle
locationsX[0] = bounds.getX() + 0.5 * bounds.getWidth();
locationsY[0] = bounds.getY() + 0.5 * bounds.getHeight();
} else {
locationsX[0] = bounds.getX();
locationsY[0] = bounds.getY();
locationsX[1] = bounds.getX() + bounds.getWidth();
locationsY[1] = bounds.getY() + bounds.getHeight();
for (int i = 2; i < locationsX.length; i++) {
locationsX[i] = bounds.getX()
+ Math.random() * bounds.getWidth();
locationsY[i] = bounds.getY()
+ Math.random() * bounds.getHeight();
}
}
}
/**
* Computes the force for each node in this SpringLayoutAlgorithm. The
* computed force will be stored in the data repository
*/
protected void computeForces() {
double forcesX[][] = new double[2][this.forcesX.length];
double forcesY[][] = new double[2][this.forcesX.length];
double locationsX[] = new double[this.forcesX.length];
double locationsY[] = new double[this.forcesX.length];
// // initialize all forces to zero
for (int j = 0; j < 2; j++) {
for (int i = 0; i < this.forcesX.length; i++) {
forcesX[j][i] = 0;
forcesY[j][i] = 0;
locationsX[i] = this.locationsX[i];
locationsY[i] = this.locationsY[i];
}
}
// TODO: Again really really slow!
for (int k = 0; k < 2; k++) {
for (int i = 0; i < this.locationsX.length; i++) {
for (int j = i + 1; j < locationsX.length; j++) {
double dx = (locationsX[i] - locationsX[j])
/ bounds.getWidth() / boundsScaleX;
double dy = (locationsY[i] - locationsY[j])
/ bounds.getHeight() / boundsScaleY;
double distance_sq = dx * dx + dy * dy;
// make sure distance and distance squared not too small
distance_sq = Math.max(MIN_DISTANCE * MIN_DISTANCE,
distance_sq);
double distance = Math.sqrt(distance_sq);
// If there are relationships between srcObj and destObj
// then decrease force on srcObj (a pull) in direction of
// destObj
// If no relation between srcObj and destObj then increase
// force on srcObj (a push) from direction of destObj.
double sumOfWeights = srcDestToSumOfWeights[i][j];
double f;
if (sumOfWeights > 0) {
// nodes are pulled towards each other
f = -sprStrain * Math.log(distance / sprLength)
* sumOfWeights;
} else {
// nodes are repelled from each other
f = sprGravitation / (distance_sq);
}
double dfx = f * dx / distance;
double dfy = f * dy / distance;
forcesX[k][i] += dfx;
forcesY[k][i] += dfy;
forcesX[k][j] -= dfx;
forcesY[k][j] -= dfy;
}
}
for (int i = 0; i < entities.length; i++) {
if (LayoutProperties.isMovable(entities[i])) {
double deltaX = sprMove * forcesX[k][i];
double deltaY = sprMove * forcesY[k][i];
// constrain movement, so that nodes don't shoot way off to
// the
// edge
double dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
double maxMovement = 0.2d * sprMove;
if (dist > maxMovement) {
deltaX *= maxMovement / dist;
deltaY *= maxMovement / dist;
}
locationsX[i] += deltaX * bounds.getWidth() * boundsScaleX;
locationsY[i] += deltaY * bounds.getHeight() * boundsScaleY;
}
}
}
// // initialize all forces to zero
for (int i = 0; i < this.entities.length; i++) {
if (forcesX[0][i] * forcesX[1][i] < 0) {
this.forcesX[i] = 0;
} else {
this.forcesX[i] = forcesX[1][i];
}
if (forcesY[0][i] * forcesY[1][i] < 0) {
this.forcesY[i] = 0;
} else {
this.forcesY[i] = forcesY[1][i];
}
}
}
/**
* Computes the position for each node in this SpringLayoutAlgorithm. The
* computed position will be stored in the data repository. position =
* position + sprMove * force
*/
protected void computePositions() {
for (int i = 0; i < entities.length; i++) {
if (LayoutProperties.isMovable(entities[i])) {
double deltaX = sprMove * forcesX[i];
double deltaY = sprMove * forcesY[i];
// constrain movement, so that nodes don't shoot way off to the
// edge
double dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
double maxMovement = 0.2d * sprMove;
if (dist > maxMovement) {
deltaX *= maxMovement / dist;
deltaY *= maxMovement / dist;
}
locationsX[i] += deltaX * bounds.getWidth() * boundsScaleX;
locationsY[i] += deltaY * bounds.getHeight() * boundsScaleY;
}
}
}
private Rectangle getLayoutBounds() {
double minX, maxX, minY, maxY;
minX = minY = Double.POSITIVE_INFINITY;
maxX = maxY = Double.NEGATIVE_INFINITY;
for (int i = 0; i < locationsX.length; i++) {
maxX = Math.max(maxX, locationsX[i] + sizeW[i] / 2);
minX = Math.min(minX, locationsX[i] - sizeW[i] / 2);
maxY = Math.max(maxY, locationsY[i] + sizeH[i] / 2);
minY = Math.min(minY, locationsY[i] - sizeH[i] / 2);
}
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
private void improveBoundScaleX(Rectangle currentBounds) {
double boundaryProportionX = currentBounds.getWidth()
/ bounds.getWidth();
if (boundaryProportionX < 0.9) {
boundsScaleX *= 1.01;
} else if (boundaryProportionX > 1) {
if (boundsScaleX < 0.01)
return;
boundsScaleX /= 1.01;
}
}
private void improveBoundScaleY(Rectangle currentBounds) {
double boundaryProportionY = currentBounds.getHeight()
/ bounds.getHeight();
if (boundaryProportionY < 0.9) {
boundsScaleY *= 1.01;
} else if (boundaryProportionY > 1) {
if (boundsScaleY < 0.01)
return;
boundsScaleY /= 1.01;
}
}
private void moveToCenter(Rectangle currentBounds) {
double moveX = (currentBounds.getX() + currentBounds.getWidth() / 2)
- (bounds.getX() + bounds.getWidth() / 2);
double moveY = (currentBounds.getY() + currentBounds.getHeight() / 2)
- (bounds.getY() + bounds.getHeight() / 2);
for (int i = 0; i < locationsX.length; i++) {
locationsX[i] -= moveX;
locationsY[i] -= moveY;
}
}
}