blob: f1fd649bc7f746596c1038e7dae612206491a8cd [file] [log] [blame]
/*******************************************************************************
* Copyright 2005, CHISEL Group, University of Victoria, Victoria, BC, Canada.
* 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: The Chisel Group, University of Victoria
*******************************************************************************/
package org.eclipse.zest.layouts.algorithms;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.zest.layouts.LayoutStyles;
import org.eclipse.zest.layouts.dataStructures.DisplayIndependentRectangle;
import org.eclipse.zest.layouts.dataStructures.InternalNode;
import org.eclipse.zest.layouts.dataStructures.InternalRelationship;
/**
* 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.
* <p>
* Instructions for using SpringLayoutAlgorithm: <br>
* 1. Instantiate a SpringLayout object; <br>
* 2. Populate the data repository using {@link #add add(...)}; <br>
* 3. Populate the relation repository using
* {@link #addRelation addRelation(...)}; <br>
* 4. Execute {@link #compute compute()}; <br>
* 5. Execute {@link #fitWithinBounds fitWithinBounds(...)}; <br>
* 6. Query the computed results(node size and node position).
*
* @version 2.0
* @author Ian Bull
* @author Casey Best (version 1.0 by Jingwei Wu/Rob Lintern)
*/
public class SpringLayoutAlgorithm extends ContinuousLayoutAlgorithm {
private final static boolean DEFAULT_ANCHOR = false;
/**
* The default value for the spring layout number of interations.
*/
public static final int DEFAULT_SPRING_ITERATIONS = 1000;
/**
* the default value for the time algorithm runs.
*/
public static final long MAX_SPRING_TIME = 10000;
/**
* The default value for positioning nodes randomly.
*/
public static final boolean DEFAULT_SPRING_RANDOM = true;
/**
* The default value for ignoring unconnected nodes.
*/
public static final boolean DEFAULT_SPRING_IGNORE_UNCON = true;
/**
* The default value for separating connected components.
*/
public static final boolean DEFAULT_SPRING_SEPARATE_COMPONENTS = true;
/**
* The default value for the spring layout move-control.
*/
public static final double DEFAULT_SPRING_MOVE = 1.0f;
/**
* The default value for the spring layout strain-control.
*/
public static final double DEFAULT_SPRING_STRAIN = 1.0f;
/**
* The default value for the spring layout length-control.
*/
public static final double DEFAULT_SPRING_LENGTH = 1.0f;
/**
* The default value for the spring layout gravitation-control.
*/
public static final double DEFAULT_SPRING_GRAVITATION = 1.0f;
/**
* The variable can be customized to set the number of iterations used.
*/
private static int sprIterations = DEFAULT_SPRING_ITERATIONS;
/**
* This variable can be customized to set the max number of MS the algorithm
* should run
*/
private static 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 static boolean sprRandom = DEFAULT_SPRING_RANDOM;
/**
* Minimum distance considered between nodes
*/
protected static final double MIN_DISTANCE = 0.001d;
/**
* An arbitrarily small value in mathematics.
*/
protected static final double EPSILON = 0.001d;
/**
* The variable can be customerized to set the spring layout move-control.
*/
private static double sprMove = DEFAULT_SPRING_MOVE;
/**
* The variable can be customized to set the spring layout strain-control.
*/
private static double sprStrain = DEFAULT_SPRING_STRAIN;
/**
* The variable can be customized to set the spring layout length-control.
*/
private static double sprLength = DEFAULT_SPRING_LENGTH;
/**
* The variable can be customized to set the spring layout
* gravitation-control.
*/
private static double sprGravitation = DEFAULT_SPRING_GRAVITATION;
/**
* The largest movement of all vertices that has occured in the most recent
* iteration.
*/
private double largestMovement = 0;
/**
* Maps a src and dest object to the number of relations between them. Key
* is src.toString() + dest.toString(), value is an Integer
*/
private Map srcDestToNumRelsMap;
/**
* Maps a src and dest object to the average weight of the relations between
* them. Key is src.toString() + dest.toString(), value is a Double
*/
private Map srcDestToRelsAvgWeightMap;
/**
* Maps a relationship type to a weight. Key is a string, value is a Double
*/
private static Map relTypeToWeightMap = new HashMap();
private int iteration;
private int[][] srcDestToNumRels;
private double[][] srcDestToRelsAvgWeight;
private double[] tempLocationsX;
private double[] tempLocationsY;
private double[] forcesX;
private double[] forcesY;
private boolean[] anchors;
private DisplayIndependentRectangle bounds = null;
Date date = null;
/**
* Constructor.
*/
public SpringLayoutAlgorithm(int styles) {
super(styles);
srcDestToNumRelsMap = new HashMap();
srcDestToRelsAvgWeightMap = new HashMap();
date = new Date();
}
/**
* Creates a sprint layout algoirthm with no style
*
*/
public SpringLayoutAlgorithm() {
this(LayoutStyles.NONE);
}
public void setLayoutArea(double x, double y, double width, double height) {
bounds = new DisplayIndependentRectangle(x, y, width, height);
}
/**
* 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 presion.
*
* @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 presion.
*
* @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
*/
public long getSpringTimeout() {
return maxTimeMS;
}
/**
* Sets the spring timeout
*
* @param timeout
*/
public void setSpringTimeout(long timeout) {
maxTimeMS = timeout;
}
/**
* Returns the length-control value of this SpringLayoutAlgorithm in
* double presion.
*
* @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 presion.
*
* @return The gravitation-control value.
*/
public double getSpringGravitation() {
return sprGravitation;
}
/**
* Sets the number of iterations to be used.
*
* @param gravitation
* 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 SpringLayoutAlgorithm will layout the
* nodes randomly before beginning iterations.
*/
public boolean getRandom() {
return sprRandom;
}
public void setWeight(String relType, double weight) {
relTypeToWeightMap.put(relType, new Double(weight));
}
public double getWeight(String relType) {
Double weight = (Double) relTypeToWeightMap.get(relType);
return (weight == null) ? 1 : weight.doubleValue();
}
/**
* Sets the default conditions.
*/
public void setDefaultConditions() {
// sprMove = DEFAULT_SPRING_MOVE;
// sprStrain = DEFAULT_SPRING_STRAIN;
// sprLength = DEFAULT_SPRING_LENGTH;
// sprGravitation = DEFAULT_SPRING_GRAVITATION;
// sprIterations = DEFAULT_SPRING_ITERATIONS;
}
/**
* Clean up after done
*
* @param entitiesToLayout
*/
private void reset(InternalNode[] entitiesToLayout) {
tempLocationsX = null;
tempLocationsY = null;
forcesX = null;
forcesY = null;
anchors = null;
setDefaultConditions();
srcDestToNumRelsMap = new HashMap();
srcDestToRelsAvgWeightMap = new HashMap();
relTypeToWeightMap = new HashMap();
}
private long startTime = 0;
protected void preLayoutAlgorithm(InternalNode[] entitiesToLayout, InternalRelationship[] relationshipsToConsider, double x, double y, double width, double height) {
// TODO: Filter out any non-wanted entities and relationships
// super.applyLayout(entitiesToLayout, relationshipsToConsider, x, y,
// width, height);
//InternalNode[] a_entitiesToLayout = (InternalNode[]) entitiesToLayout.toArray(new InternalNode[entitiesToLayout.size()]);
bounds = new DisplayIndependentRectangle(x, y, width, height);
tempLocationsX = new double[entitiesToLayout.length];
tempLocationsY = new double[entitiesToLayout.length];
forcesX = new double[entitiesToLayout.length];
forcesY = new double[entitiesToLayout.length];
anchors = new boolean[entitiesToLayout.length];
for (int i = 0; i < entitiesToLayout.length; i++) {
anchors[i] = DEFAULT_ANCHOR;
}
for (int i = 0; i < relationshipsToConsider.length; i++) {
InternalRelationship layoutRelationship = relationshipsToConsider[i];
addRelation(layoutRelationship);
}
// do the calculations
preCompute(entitiesToLayout);
startTime = date.getTime();
}
protected void postLayoutAlgorithm(InternalNode[] entitiesToLayout, InternalRelationship[] relationshipsToConsider) {
reset(entitiesToLayout);
}
/**
* Adds a simple relation between two nodes to the relation repository.
*
* @param layoutRelationship
* The simple relation to be added
* @throws java.lang.NullPointerExcetption
* If <code>sr</code> is null
* @see SimpleRelation
*/
private void addRelation(InternalRelationship layoutRelationship) {
if (layoutRelationship == null) {
throw new IllegalArgumentException("The arguments can not be null!");
} else {
double weight = layoutRelationship.getWeight();
weight = (weight <= 0 ? 0.1 : weight);
String key1 = layoutRelationship.getSource().toString() + layoutRelationship.getDestination().toString();
String key2 = layoutRelationship.getDestination().toString() + layoutRelationship.getSource().toString();
String[] keys = { key1, key2 };
for (int i = 0; i < keys.length; i++) {
String key = keys[i];
Integer count = (Integer) srcDestToNumRelsMap.get(key);
Double avgWeight = (Double) srcDestToRelsAvgWeightMap.get(key);
if (count == null) {
count = new Integer(1);
avgWeight = new Double(weight);
} else {
int newCount = count.intValue() + 1;
double newAverage = (avgWeight.doubleValue() * count.doubleValue() + weight) / newCount;
avgWeight = new Double(newAverage);
count = new Integer(newCount);
}
srcDestToNumRelsMap.put(key, count);
srcDestToRelsAvgWeightMap.put(key, avgWeight);
}
}
}
private void preCompute(InternalNode[] entitiesToLayout) {
// count number of relationships between all nodes and the average
// weight between them
srcDestToNumRels = new int[entitiesToLayout.length][entitiesToLayout.length];
srcDestToRelsAvgWeight = new double[entitiesToLayout.length][entitiesToLayout.length];
for (int i = 0; i < entitiesToLayout.length - 1; i++) {
InternalNode layoutEntity1 = entitiesToLayout[i];
for (int j = i + 1; j < entitiesToLayout.length; j++) {
InternalNode layoutEntity2 = entitiesToLayout[j];
srcDestToNumRels[i][j] = numRelations(layoutEntity1, layoutEntity2);
srcDestToNumRels[i][j] += numRelations(layoutEntity2, layoutEntity1);
srcDestToRelsAvgWeight[i][j] = avgWeight(layoutEntity1, layoutEntity2);
}
}
if (sprRandom)
placeRandomly(entitiesToLayout); // put vertices in random places
else
convertToUnitCoordinates(entitiesToLayout);
iteration = 1;
largestMovement = Double.MAX_VALUE;
}
// TODO: This is a complete Clone! (and not in a good way)
protected DisplayIndependentRectangle getLayoutBoundsTemp(InternalNode[] entitiesToLayout, boolean includeNodeSize) {
double rightSide = Double.MIN_VALUE;
double bottomSide = Double.MIN_VALUE;
double leftSide = Double.MAX_VALUE;
double topSide = Double.MAX_VALUE;
for (int i = 0; i < entitiesToLayout.length; i++) {
double x = tempLocationsX[i];
double y = tempLocationsY[i];
leftSide = Math.min(x, leftSide);
topSide = Math.min(y, topSide);
rightSide = Math.max(x, rightSide);
bottomSide = Math.max(y, bottomSide);
}
return new DisplayIndependentRectangle(leftSide, topSide, rightSide - leftSide, bottomSide - topSide);
}
protected void convertNodePositionsBack(int i, InternalNode entityToConvert, double px, double py, double screenWidth, double screenHeight, DisplayIndependentRectangle layoutBounds) {
// If the node selected is outside the screen, map it to the boarder
if (px > screenWidth)
px = screenWidth;
if (py > screenHeight)
py = screenHeight;
if (px < 0)
px = 1;
if (py < 0)
py = 1;
double x = (px / screenWidth) * layoutBounds.width + layoutBounds.x;
double y = (py / screenHeight) * layoutBounds.height + layoutBounds.y;
tempLocationsX[i] = x;
tempLocationsY[i] = y;
//setTempLocation(entityToConvert, new DisplayIndependentPoint(x, y));
if (entityToConvert.getInternalX() < 0) {
// System.out.println("We have nodes less than 0 here!");
}
}
private void checkPreferredLocation(InternalNode[] entitiesToLayout, DisplayIndependentRectangle realBounds) {
// use 10% for the border - 5% on each side
double borderWidth = Math.min(realBounds.width, realBounds.height) / 10.0;
DisplayIndependentRectangle screenBounds = new DisplayIndependentRectangle(realBounds.x + borderWidth / 2.0, realBounds.y + borderWidth / 2.0, realBounds.width - borderWidth, realBounds.height - borderWidth);
DisplayIndependentRectangle layoutBounds = getLayoutBoundsTemp(entitiesToLayout, false);
for (int i = 0; i < entitiesToLayout.length; i++) {
InternalNode layoutEntity = entitiesToLayout[i];
if (layoutEntity.hasPreferredLocation()) {
convertNodePositionsBack(i, layoutEntity, layoutEntity.getPreferredX(), layoutEntity.getPreferredY(), screenBounds.width, screenBounds.height, layoutBounds);
}
}
}
/**
* 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 = date.getTime();
double fractionComplete = (double) ((double) (currentTime - startTime) / ((double) maxTimeMS));
int currentIteration = (int) (fractionComplete * sprIterations);
if (currentIteration > iteration) {
iteration = currentIteration;
}
}
protected boolean performAnotherNonContinuousIteration() {
setSprIterationsBasedOnTime();
if (iteration <= sprIterations && largestMovement >= sprMove)
return true;
else
return false;
}
protected int getCurrentLayoutStep() {
return iteration;
}
protected int getTotalNumberOfLayoutSteps() {
return sprIterations;
}
protected void computeOneIteration(InternalNode[] entitiesToLayout, InternalRelationship[] relationshipsToConsider, double x, double y, double width, double height) {
if (bounds == null)
bounds = new DisplayIndependentRectangle(x, y, width, height);
checkPreferredLocation(entitiesToLayout, bounds);
computeForces(entitiesToLayout);
largestMovement = Double.MAX_VALUE;
computePositions(entitiesToLayout);
for (int i = 0; i < entitiesToLayout.length; i++) {
InternalNode layoutEntity = entitiesToLayout[i];
layoutEntity.setInternalLocation(tempLocationsX[i], tempLocationsY[i]);
}
defaultFitWithinBounds(entitiesToLayout, bounds);
iteration++;
}
/**
* Puts vertices in random places, all between (0,0) and (1,1).
*/
public void placeRandomly(InternalNode[] entitiesToLayout) {
// If only one node in the data repository, put it in the middle
if (entitiesToLayout.length == 1) {
// If only one node in the data repository, put it in the middle
tempLocationsX[0] = 0.5;
tempLocationsY[0] = 0.5;
} else {
for (int i = 0; i < entitiesToLayout.length; i++) {
if (i == 0) {
tempLocationsX[i] = 0.0;
tempLocationsY[i] = 0.0;
} else if (i == 1) {
tempLocationsX[i] = 1.0;
tempLocationsY[i] = 1.0;
} else {
tempLocationsX[i] = Math.random();
tempLocationsY[i] = Math.random();
}
}
}
}
// /////////////////////////////////////////////////////////////////
// /// Protected Methods /////
// /////////////////////////////////////////////////////////////////
/**
* Computes the force for each node in this SpringLayoutAlgorithm. The
* computed force will be stored in the data repository
*/
protected void computeForces(InternalNode[] entitiesToLayout) {
// initialize all forces to zero
for (int i = 0; i < entitiesToLayout.length; i++) {
forcesX[i] = 0.0;
forcesY[i] = 0.0;
}
// TODO: Again really really slow!
for (int i = 0; i < entitiesToLayout.length - 1; i++) {
InternalNode sourceEntity = entitiesToLayout[i];
double srcLocationX = tempLocationsX[i];
double srcLocationY = tempLocationsY[i];
double fx = forcesX[i]; // force in x direction
double fy = forcesY[i]; // force in y direction
for (int j = i + 1; j < entitiesToLayout.length; j++) {
InternalNode destinationEntity = entitiesToLayout[j];
if (!destinationEntity.equals(sourceEntity)) {
double destLocationX = tempLocationsX[j];
double destLocationY = tempLocationsY[j];
double dx = srcLocationX - destLocationX;
double dy = srcLocationY - destLocationY;
double distance = Math.sqrt(dx * dx + dy * dy);
double distance_sq = distance * distance;
// make sure distance and distance squared not too small
distance = Math.max(MIN_DISTANCE, distance);
// 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.
int numRels = srcDestToNumRels[i][j];
double avgWeight = srcDestToRelsAvgWeight[i][j];
if (numRels > 0) {
// nodes are pulled towards each other
double f = sprStrain * Math.log(distance / sprLength) * numRels * avgWeight;
fx = fx - (f * dx / distance);
fy = fy - (f * dy / distance);
} else {
// nodes are repelled from each other
//double f = Math.min(100, sprGravitation / (distance*distance));
double f = sprGravitation / (distance_sq);
fx = fx + (f * dx / distance);
fy = fy + (f * dy / distance);
}
// According to Newton, "for every action, there is an equal
// and opposite reaction."
// so give the dest an opposite force
forcesX[j] = forcesX[j] - fx;
forcesY[j] = forcesY[j] - fy;
}
}
/*
* //make sure forces aren't too big if (fx > 0 ) fx = Math.min(fx,
* 10*sprMove); else fx = Math.max(fx, -10*sprMove); if (fy > 0) fy =
* Math.min(fy, 10*sprMove); else fy = Math.max(fy, -10*sprMove);
*/
forcesX[i] = fx;
forcesY[i] = fy;
// Remove the src object from the list of destinations since
// we've already calculated the force from it on all other
// objects.
// dests.remove(srcObj);
}
}
/**
* 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(InternalNode[] entitiesToLayout) {
for (int i = 0; i < entitiesToLayout.length; i++) {
if (!anchors[i] || entitiesToLayout[i].hasPreferredLocation()) {
double oldX = tempLocationsX[i];
double oldY = tempLocationsY[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 maxMovement = 0.2d * sprMove;
if (deltaX >= 0) {
deltaX = Math.min(deltaX, maxMovement);
} else {
deltaX = Math.max(deltaX, -maxMovement);
}
if (deltaY >= 0) {
deltaY = Math.min(deltaY, maxMovement);
} else {
deltaY = Math.max(deltaY, -maxMovement);
}
largestMovement = Math.max(largestMovement, Math.abs(deltaX));
largestMovement = Math.max(largestMovement, Math.abs(deltaY));
double newX = oldX + deltaX;
double newY = oldY + deltaY;
tempLocationsX[i] = newX;
tempLocationsY[i] = newY;
}
}
}
/**
* Converts the position for each node in this SpringLayoutAlgorithm
* to unit coordinates in double precision. The computed positions will be
* still stored in the data repository.
*/
protected void convertToUnitCoordinates(InternalNode[] entitiesToLayout) {
double minX = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE;
double minY = Double.MAX_VALUE;
double maxY = Double.MIN_VALUE;
for (int i = 0; i < entitiesToLayout.length; i++) {
InternalNode layoutEntity = entitiesToLayout[i];
minX = Math.min(minX, layoutEntity.getInternalX());
minY = Math.min(minY, layoutEntity.getInternalY());
maxX = Math.max(maxX, layoutEntity.getInternalX());
maxY = Math.max(maxY, layoutEntity.getInternalY());
}
double spanX = maxX - minX;
double spanY = maxY - minY;
double maxSpan = Math.max(spanX, spanY);
if (maxSpan > EPSILON) {
for (int i = 0; i < entitiesToLayout.length; i++) {
InternalNode layoutEntity = entitiesToLayout[i];
double x = (layoutEntity.getInternalX() - minX) / spanX;
double y = (layoutEntity.getInternalY() - minY) / spanY;
tempLocationsX[i] = x;
tempLocationsY[i] = y;
}
} else {
placeRandomly(entitiesToLayout);
}
}
/**
* Examines the number of specified relation between the <code>src</code>
* and the <code>dest</code> that exist in this
* SpringLayoutAlgorithm's relation repository.
*
* @param src
* The source part of the relaton to be examined.
* @param dest
* The destination part of the relation to be examined.
* @return The number of relations between src and dest.
*/
private int numRelations(Object src, Object dest) {
String key = src.toString() + dest.toString();
Integer count = (Integer) srcDestToNumRelsMap.get(key);
int intCount = (count == null) ? 0 : count.intValue();
return intCount;
}
/**
* Returns the average weight between a src and dest object.
*
* @param src
* @param dest
* @return The average weight between the given src and dest nodes
*/
private double avgWeight(Object src, Object dest) {
String key = src.toString() + dest.toString();
Double avgWeight = (Double) srcDestToRelsAvgWeightMap.get(key);
double doubleWeight = (avgWeight == null) ? 1 : avgWeight.doubleValue();
return doubleWeight;
}
protected boolean isValidConfiguration(boolean asynchronous, boolean continueous) {
if (asynchronous && continueous)
return true;
else if (asynchronous && !continueous)
return true;
else if (!asynchronous && continueous)
return false;
else if (!asynchronous && !continueous)
return true;
return false;
}
}