/** * $Id: mxOrganicLayout.java,v 1.2 2012/12/22 22:39:40 david Exp $ * Copyright (c) 2007-2013, JGraph Ltd */ package com.mxgraph.layout; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import com.mxgraph.model.mxGraphModel; import com.mxgraph.model.mxIGraphModel; import com.mxgraph.util.mxRectangle; import com.mxgraph.view.mxGraph; import com.mxgraph.view.mxGraphView; /** * An implementation of a simulated annealing layout, based on "Drawing Graphs * Nicely Using Simulated Annealing" by Davidson and Harel (1996). This * paper describes these criteria as being favourable in a graph layout: (1) * distributing nodes evenly, (2) making edge-lengths uniform, (3) * minimizing cross-crossings, and (4) keeping nodes from coming too close * to edges. These criteria are translated into energy cost functions in the * layout. Nodes or edges breaking these criteria create a larger cost function * , the total cost they contribute related to the extent that they break it. * The idea of the algorithm is to minimise the total system energy. Factors * are assigned to each of the criteria describing how important that * criteria is. Higher factors mean that those criteria are deemed to be * relatively preferable in the final layout. Most of the criteria conflict * with the others to some extent and so the setting of the factors determines * the general look of the resulting graph. * <p> * In addition to the four aesthetic criteria the concept of a border line * which induces an energy cost to nodes in proximity to the graph bounds is * introduced to attempt to restrain the graph. All of the 5 factors can be * switched on or off using the <code>isOptimize...</code> variables. * <p> * Simulated Annealing is a force-directed layout and is one of the more * expensive, but generally effective layouts of this type. Layouts like * the spring layout only really factor in edge length and inter-node * distance being the lowest CPU intensive for the most aesthetic gain. The * additional factors are more expensive but can have very attractive results. * <p> * The main loop of the algorithm consist of processing the nodes in a * deterministic order. During the processing of each node a circle of radius * <code>moveRadius</code> is made around the node and split into * <code>triesPerCell</code> equal segments. Each point between neighbour * segments is determined and the new energy of the system if the node were * moved to that position calculated. Only the necessary nodes and edges are * processed new energy values resulting in quadratic performance, O(VE), * whereas calculating the total system energy would be cubic. The default * implementation only checks 8 points around the radius of the circle, as * opposed to the suggested 30 in the paper. Doubling the number of points * double the CPU load and 8 works almost as well as 30. * <p> * The <code>moveRadius</code> replaces the temperature as the influencing * factor in the way the graph settles in later iterations. If the user does * not set the initial move radius it is set to half the maximum dimension * of the graph. Thus, in 2 iterations a node may traverse the entire graph, * and it is more sensible to find minima this way that uphill moves, which * are little more than an expensive 'tilt' method. The factor by which * the radius is multiplied by after each iteration is important, lowering * it improves performance but raising it towards 1.0 can improve the * resulting graph aesthetics. When the radius hits the minimum move radius * defined, the layout terminates. The minimum move radius should be set * a value where the move distance is too minor to be of interest. * <p> * Also, the idea of a fine tuning phase is used, as described in the paper. * This involves only calculating the edge to node distance energy cost * at the end of the algorithm since it is an expensive calculation and * it really an 'optimizating' function. <code>fineTuningRadius</code> * defines the radius value that, when reached, causes the edge to node * distance to be calculated. * <p> * There are other special cases that are processed after each iteration. * <code>unchangedEnergyRoundTermination</code> defines the number of * iterations, after which the layout terminates. If nothing is being moved * it is assumed a good layout has been found. In addition to this if * no nodes are moved during an iteration the move radius is halved, presuming * that a finer granularity is required. * */ public class mxOrganicLayout extends mxGraphLayout { /** * Whether or not the distance between edge and nodes will be calculated * as an energy cost function. This function is CPU intensive and is best * only used in the fine tuning phase. */ protected boolean isOptimizeEdgeDistance = true; /** * Whether or not edges crosses will be calculated as an energy cost * function. This function is CPU intensive, though if some iterations * without it are required, it is best to have a few cycles at the start * of the algorithm using it, then use it intermittantly through the rest * of the layout. */ protected boolean isOptimizeEdgeCrossing = true; /** * Whether or not edge lengths will be calculated as an energy cost * function. This function not CPU intensive. */ protected boolean isOptimizeEdgeLength = true; /** * Whether or not nodes will contribute an energy cost as they approach * the bound of the graph. The cost increases to a limit close to the * border and stays constant outside the bounds of the graph. This function * is not CPU intensive */ protected boolean isOptimizeBorderLine = true; /** * Whether or not node distribute will contribute an energy cost where * nodes are close together. The function is moderately CPU intensive. */ protected boolean isOptimizeNodeDistribution = true; /** * when {@link #moveRadius}reaches this value, the algorithm is terminated */ protected double minMoveRadius = 2.0; /** * The current radius around each node where the next position energy * values will be calculated for a possible move */ protected double moveRadius; /** * The initial value of <code>moveRadius</code>. If this is set to zero * the layout will automatically determine a suitable value. */ protected double initialMoveRadius = 0.0; /** * The factor by which the <code>moveRadius</code> is multiplied by after * every iteration. A value of 0.75 is a good balance between performance * and aesthetics. Increasing the value provides more chances to find * minimum energy positions and decreasing it causes the minimum radius * termination condition to occur more quickly. */ protected double radiusScaleFactor = 0.75; /** * The average amount of area allocated per node. If <code> bounds</code> * is not set this value mutiplied by the number of nodes to find * the total graph area. The graph is assumed square. */ protected double averageNodeArea = 160000; /** * The radius below which fine tuning of the layout should start * This involves allowing the distance between nodes and edges to be * taken into account in the total energy calculation. If this is set to * zero, the layout will automatically determine a suitable value */ protected double fineTuningRadius = 40.0; /** * Limit to the number of iterations that may take place. This is only * reached if one of the termination conditions does not occur first. */ protected int maxIterations = 1000; /** * Cost factor applied to energy calculations involving the distance * nodes and edges. Increasing this value tends to cause nodes to move away * from edges, at the partial cost of other graph aesthetics. * <code>isOptimizeEdgeDistance</code> must be true for edge to nodes * distances to be taken into account. */ protected double edgeDistanceCostFactor = 3000; /** * Cost factor applied to energy calculations involving edges that cross * over one another. Increasing this value tends to result in fewer edge * crossings, at the partial cost of other graph aesthetics. * <code>isOptimizeEdgeCrossing</code> must be true for edge crossings * to be taken into account. */ protected double edgeCrossingCostFactor = 6000; /** * Cost factor applied to energy calculations involving the general node * distribution of the graph. Increasing this value tends to result in * a better distribution of nodes across the available space, at the * partial cost of other graph aesthetics. * <code>isOptimizeNodeDistribution</code> must be true for this general * distribution to be applied. */ protected double nodeDistributionCostFactor = 30000; /** * Cost factor applied to energy calculations for node promixity to the * notional border of the graph. Increasing this value results in * nodes tending towards the centre of the drawing space, at the * partial cost of other graph aesthetics. * <code>isOptimizeBorderLine</code> must be true for border * repulsion to be applied. */ protected double borderLineCostFactor = 5; /** * Cost factor applied to energy calculations for the edge lengths. * Increasing this value results in the layout attempting to shorten all * edges to the minimum edge length, at the partial cost of other graph * aesthetics. * <code>isOptimizeEdgeLength</code> must be true for edge length * shortening to be applied. */ protected double edgeLengthCostFactor = 0.02; /** * The x coordinate of the final graph */ protected double boundsX = 0.0; /** * The y coordinate of the final graph */ protected double boundsY = 0.0; /** * The width coordinate of the final graph */ protected double boundsWidth = 0.0; /** * The height coordinate of the final graph */ protected double boundsHeight = 0.0; /** * current iteration number of the layout */ protected int iteration; /** * determines, in how many segments the circle around cells is divided, to * find a new position for the cell. Doubling this value doubles the CPU * load. Increasing it beyond 16 might mean a change to the * <code>performRound</code> method might further improve accuracy for a * small performance hit. The change is described in the method comment. */ protected int triesPerCell = 8; /** * prevents from dividing with zero and from creating excessive energy * values */ protected double minDistanceLimit = 2; /** * cached version of <code>minDistanceLimit</code> squared */ protected double minDistanceLimitSquared; /** * distance limit beyond which energy costs due to object repulsive is * not calculated as it would be too insignificant */ protected double maxDistanceLimit = 100; /** * cached version of <code>maxDistanceLimit</code> squared */ protected double maxDistanceLimitSquared; /** * Keeps track of how many consecutive round have passed without any energy * changes */ protected int unchangedEnergyRoundCount; /** * The number of round of no node moves taking placed that the layout * terminates */ protected int unchangedEnergyRoundTermination = 5; /** * Whether or not to use approximate node dimensions or not. Set to true * the radius squared of the smaller dimension is used. Set to false the * radiusSquared variable of the CellWrapper contains the width squared * and heightSquared is used in the obvious manner. */ protected boolean approxNodeDimensions = true; /** * Internal models collection of nodes ( vertices ) to be laid out */ protected CellWrapper[] v; /** * Internal models collection of edges to be laid out */ protected CellWrapper[] e; /** * Array of the x portion of the normalised test vectors that * are tested for a lower energy around each vertex. The vector * of the combined x and y normals are multipled by the current * radius to obtain test points for each vector in the array. */ protected double[] xNormTry; /** * Array of the y portion of the normalised test vectors that * are tested for a lower energy around each vertex. The vector * of the combined x and y normals are multipled by the current * radius to obtain test points for each vector in the array. */ protected double[] yNormTry; /** * Whether or not fine tuning is on. The determines whether or not * node to edge distances are calculated in the total system energy. * This cost function , besides detecting line intersection, is a * performance intensive component of this algorithm and best left * to optimization phase. <code>isFineTuning</code> is switched to * <code>true</code> if and when the <code>fineTuningRadius</code> * radius is reached. Switching this variable to <code>true</code> * before the algorithm runs mean the node to edge cost function * is always calculated. */ protected boolean isFineTuning = 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; /** * Specifies if all edge points of traversed edges should be removed. * Default is true. */ protected boolean resetEdges = false; /** * Constructor for mxOrganicLayout. */ public mxOrganicLayout(mxGraph graph) { super(graph); } /** * Constructor for mxOrganicLayout. */ public mxOrganicLayout(mxGraph graph, Rectangle2D bounds) { super(graph); boundsX = bounds.getX(); boundsY = bounds.getY(); boundsWidth = bounds.getWidth(); boundsHeight = bounds.getHeight(); } /** * Returns true if the given vertex has no connected edges. * * @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 false; } /** * Implements <mxGraphLayout.execute>. */ public void execute(Object parent) { mxIGraphModel model = graph.getModel(); mxGraphView view = graph.getView(); Object[] vertices = graph.getChildVertices(parent); HashSet<Object> vertexSet = new HashSet<Object>(Arrays.asList(vertices)); HashSet<Object> validEdges = new HashSet<Object>(); // Remove edges that do not have both source and target terminals visible for (int i = 0; i < vertices.length; i++) { Object[] edges = mxGraphModel.getEdges(model, vertices[i], false, true, false); for (int j = 0; j < edges.length; j++) { // Only deal with sources. To be valid in the layout, each edge must be attached // at both source and target to a vertex in the layout. Doing this avoids processing // each edge twice. if (view.getVisibleTerminal(edges[j], true) == vertices[i] && vertexSet.contains(view.getVisibleTerminal(edges[j], false))) { validEdges.add(edges[j]); } } } Object[] edges = validEdges.toArray(); // If the bounds dimensions have not been set see if the average area // per node has been mxRectangle totalBounds = null; mxRectangle bounds = null; // Form internal model of nodes Map<Object, Integer> vertexMap = new Hashtable<Object, Integer>(); v = new CellWrapper[vertices.length]; for (int i = 0; i < vertices.length; i++) { v[i] = new CellWrapper(vertices[i]); vertexMap.put(vertices[i], new Integer(i)); bounds = getVertexBounds(vertices[i]); if (totalBounds == null) { totalBounds = (mxRectangle) bounds.clone(); } else { totalBounds.add(bounds); } // 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(); v[i].x = bounds.getX() + width / 2.0; v[i].y = bounds.getY() + height / 2.0; if (approxNodeDimensions) { v[i].radiusSquared = Math.min(width, height); v[i].radiusSquared *= v[i].radiusSquared; } else { v[i].radiusSquared = width * width; v[i].heightSquared = height * height; } } if (averageNodeArea == 0.0) { if (boundsWidth == 0.0 && totalBounds != null) { // Just use current bounds of graph boundsX = totalBounds.getX(); boundsY = totalBounds.getY(); boundsWidth = totalBounds.getWidth(); boundsHeight = totalBounds.getHeight(); } } else { // find the center point of the current graph // based the new graph bounds on the average node area set double newArea = averageNodeArea * vertices.length; double squareLength = Math.sqrt(newArea); if (bounds != null) { double centreX = totalBounds.getX() + totalBounds.getWidth() / 2.0; double centreY = totalBounds.getY() + totalBounds.getHeight() / 2.0; boundsX = centreX - squareLength / 2.0; boundsY = centreY - squareLength / 2.0; } else { boundsX = 0; boundsY = 0; } boundsWidth = squareLength; boundsHeight = squareLength; // Ensure x and y are 0 or positive if (boundsX < 0.0 || boundsY < 0.0) { double maxNegativeAxis = Math.min(boundsX, boundsY); double axisOffset = -maxNegativeAxis; boundsX += axisOffset; boundsY += axisOffset; } } // If the initial move radius has not been set find a suitable value. // A good value is half the maximum dimension of the final graph area if (initialMoveRadius == 0.0) { initialMoveRadius = Math.max(boundsWidth, boundsHeight) / 2.0; } moveRadius = initialMoveRadius; minDistanceLimitSquared = minDistanceLimit * minDistanceLimit; maxDistanceLimitSquared = maxDistanceLimit * maxDistanceLimit; unchangedEnergyRoundCount = 0; // Form internal model of edges e = new CellWrapper[edges.length]; for (int i = 0; i < e.length; i++) { e[i] = new CellWrapper(edges[i]); Object sourceCell = model.getTerminal(edges[i], true); Object targetCell = model.getTerminal(edges[i], false); Integer source = null; Integer target = null; // Check if either end of the edge is not connected if (sourceCell != null) { source = vertexMap.get(sourceCell); } if (targetCell != null) { target = vertexMap.get(targetCell); } if (source != null) { e[i].source = source.intValue(); } else { // source end is not connected e[i].source = -1; } if (target != null) { e[i].target = target.intValue(); } else { // target end is not connected e[i].target = -1; } } // Set up internal nodes with information about whether edges // are connected to them or not for (int i = 0; i < v.length; i++) { v[i].relevantEdges = getRelevantEdges(i); v[i].connectedEdges = getConnectedEdges(i); } // Setup the normal vectors for the test points to move each vertex to xNormTry = new double[triesPerCell]; yNormTry = new double[triesPerCell]; for (int i = 0; i < triesPerCell; i++) { double angle = i * ((2.0 * Math.PI) / triesPerCell); xNormTry[i] = Math.cos(angle); yNormTry[i] = Math.sin(angle); } int childCount = model.getChildCount(parent); for (int i = 0; i < childCount; i++) { Object cell = model.getChildAt(parent, i); if (!isEdgeIgnored(cell)) { if (isResetEdges()) { graph.resetEdge(cell); } if (isDisableEdgeStyle()) { setEdgeStyleEnabled(cell, false); } } } // The main layout loop for (iteration = 0; iteration < maxIterations; iteration++) { performRound(); } // Obtain the final positions double[][] result = new double[v.length][2]; for (int i = 0; i < v.length; i++) { vertices[i] = v[i].cell; bounds = getVertexBounds(vertices[i]); result[i][0] = v[i].x - bounds.getWidth() / 2; result[i][1] = v[i].y - bounds.getHeight() / 2; } model.beginUpdate(); try { for (int i = 0; i < vertices.length; i++) { setVertexLocation(vertices[i], result[i][0], result[i][1]); } } finally { model.endUpdate(); } } /** * The main round of the algorithm. Firstly, a permutation of nodes * is created and worked through in that random order. Then, for each node * a number of point of a circle of radius <code>moveRadius</code> are * selected and the total energy of the system calculated if that node * were moved to that new position. If a lower energy position is found * this is accepted and the algorithm moves onto the next node. There * may be a slightly lower energy value yet to be found, but forcing * the loop to check all possible positions adds nearly the current * processing time again, and for little benefit. Another possible * strategy would be to take account of the fact that the energy values * around the circle decrease for half the loop and increase for the * other, as a general rule. If part of the decrease were seen, then * when the energy of a node increased, the previous node position was * almost always the lowest energy position. This adds about two loop * iterations to the inner loop and only makes sense with 16 tries or more. */ protected void performRound() { // sequential order cells are computed (every round the same order) // boolean to keep track of whether any moves were made in this round boolean energyHasChanged = false; for (int i = 0; i < v.length; i++) { int index = i; // Obtain the energies for the node is its current position // TODO The energy could be stored from the last iteration // and used again, rather than re-calculate double oldNodeDistribution = getNodeDistribution(index); double oldEdgeDistance = getEdgeDistanceFromNode(index); oldEdgeDistance += getEdgeDistanceAffectedNodes(index); double oldEdgeCrossing = getEdgeCrossingAffectedEdges(index); double oldBorderLine = getBorderline(index); double oldEdgeLength = getEdgeLengthAffectedEdges(index); double oldAdditionFactors = getAdditionFactorsEnergy(index); for (int j = 0; j < triesPerCell; j++) { double movex = moveRadius * xNormTry[j]; double movey = moveRadius * yNormTry[j]; // applying new move double oldx = v[index].x; double oldy = v[index].y; v[index].x = v[index].x + movex; v[index].y = v[index].y + movey; // calculate the energy delta from this move double energyDelta = calcEnergyDelta(index, oldNodeDistribution, oldEdgeDistance, oldEdgeCrossing, oldBorderLine, oldEdgeLength, oldAdditionFactors); if (energyDelta < 0) { // energy of moved node is lower, finish tries for this // node energyHasChanged = true; break; // exits loop } else { // Revert node coordinates v[index].x = oldx; v[index].y = oldy; } } } // Check if we've hit the limit number of unchanged rounds that cause // a termination condition if (energyHasChanged) { unchangedEnergyRoundCount = 0; } else { unchangedEnergyRoundCount++; // Half the move radius in case assuming it's set too high for // what might be an optimisation case moveRadius /= 2.0; } if (unchangedEnergyRoundCount >= unchangedEnergyRoundTermination) { iteration = maxIterations; } // decrement radius in controlled manner double newMoveRadius = moveRadius * radiusScaleFactor; // Don't waste time on tiny decrements, if the final pixel resolution // is 50 then there's no point doing 55,54.1, 53.2 etc if (moveRadius - newMoveRadius < minMoveRadius) { newMoveRadius = moveRadius - minMoveRadius; } // If the temperature reaches its minimum temperature then finish if (newMoveRadius <= minMoveRadius) { iteration = maxIterations; } // Switch on fine tuning below the specified temperature if (newMoveRadius < fineTuningRadius) { isFineTuning = true; } moveRadius = newMoveRadius; } /** * Calculates the change in energy for the specified node. The new energy is * calculated from the cost function methods and the old energy values for * each cost function are passed in as parameters * * @param index * The index of the node in the <code>vertices</code> array * @param oldNodeDistribution * The previous node distribution energy cost of this node * @param oldEdgeDistance * The previous edge distance energy cost of this node * @param oldEdgeCrossing * The previous edge crossing energy cost for edges connected to * this node * @param oldBorderLine * The previous border line energy cost for this node * @param oldEdgeLength * The previous edge length energy cost for edges connected to * this node * @param oldAdditionalFactorsEnergy * The previous energy cost for additional factors from * sub-classes * * @return the delta of the new energy cost to the old energy cost * */ protected double calcEnergyDelta(int index, double oldNodeDistribution, double oldEdgeDistance, double oldEdgeCrossing, double oldBorderLine, double oldEdgeLength, double oldAdditionalFactorsEnergy) { double energyDelta = 0.0; energyDelta += getNodeDistribution(index) * 2.0; energyDelta -= oldNodeDistribution * 2.0; energyDelta += getBorderline(index); energyDelta -= oldBorderLine; energyDelta += getEdgeDistanceFromNode(index); energyDelta += getEdgeDistanceAffectedNodes(index); energyDelta -= oldEdgeDistance; energyDelta -= oldEdgeLength; energyDelta += getEdgeLengthAffectedEdges(index); energyDelta -= oldEdgeCrossing; energyDelta += getEdgeCrossingAffectedEdges(index); energyDelta -= oldAdditionalFactorsEnergy; energyDelta += getAdditionFactorsEnergy(index); return energyDelta; } /** * Calculates the energy cost of the specified node relative to all other * nodes. Basically produces a higher energy the closer nodes are together. * * @param i the index of the node in the array <code>v</code> * @return the total node distribution energy of the specified node */ protected double getNodeDistribution(int i) { double energy = 0.0; // This check is placed outside of the inner loop for speed, even // though the code then has to be duplicated if (isOptimizeNodeDistribution == true) { if (approxNodeDimensions) { for (int j = 0; j < v.length; j++) { if (i != j) { double vx = v[i].x - v[j].x; double vy = v[i].y - v[j].y; double distanceSquared = vx * vx + vy * vy; distanceSquared -= v[i].radiusSquared; distanceSquared -= v[j].radiusSquared; // prevents from dividing with Zero. if (distanceSquared < minDistanceLimitSquared) { distanceSquared = minDistanceLimitSquared; } energy += nodeDistributionCostFactor / distanceSquared; } } } else { for (int j = 0; j < v.length; j++) { if (i != j) { double vx = v[i].x - v[j].x; double vy = v[i].y - v[j].y; double distanceSquared = vx * vx + vy * vy; distanceSquared -= v[i].radiusSquared; distanceSquared -= v[j].radiusSquared; // If the height separation indicates overlap, subtract // the widths from the distance. Same for width overlap // TODO if () // prevents from dividing with Zero. if (distanceSquared < minDistanceLimitSquared) { distanceSquared = minDistanceLimitSquared; } energy += nodeDistributionCostFactor / distanceSquared; } } } } return energy; } /** * This method calculates the energy of the distance of the specified * node to the notional border of the graph. The energy increases up to * a limited maximum close to the border and stays at that maximum * up to and over the border. * * @param i the index of the node in the array <code>v</code> * @return the total border line energy of the specified node */ protected double getBorderline(int i) { double energy = 0.0; if (isOptimizeBorderLine) { // Avoid very small distances and convert negative distance (i.e // outside the border to small positive ones ) double l = v[i].x - boundsX; if (l < minDistanceLimit) l = minDistanceLimit; double t = v[i].y - boundsY; if (t < minDistanceLimit) t = minDistanceLimit; double r = boundsX + boundsWidth - v[i].x; if (r < minDistanceLimit) r = minDistanceLimit; double b = boundsY + boundsHeight - v[i].y; if (b < minDistanceLimit) b = minDistanceLimit; energy += borderLineCostFactor * ((1000000.0 / (t * t)) + (1000000.0 / (l * l)) + (1000000.0 / (b * b)) + (1000000.0 / (r * r))); } return energy; } /** * Obtains the energy cost function for the specified node being moved. * This involves calling <code>getEdgeLength</code> for all * edges connected to the specified node * @param node * the node whose connected edges cost functions are to be * calculated * @return the total edge length energy of the connected edges */ protected double getEdgeLengthAffectedEdges(int node) { double energy = 0.0; for (int i = 0; i < v[node].connectedEdges.length; i++) { energy += getEdgeLength(v[node].connectedEdges[i]); } return energy; } /** * This method calculates the energy due to the length of the specified * edge. The energy is proportional to the length of the edge, making * shorter edges preferable in the layout. * * @param i the index of the edge in the array <code>e</code> * @return the total edge length energy of the specified edge */ protected double getEdgeLength(int i) { if (isOptimizeEdgeLength) { double edgeLength = Point2D.distance(v[e[i].source].x, v[e[i].source].y, v[e[i].target].x, v[e[i].target].y); return (edgeLengthCostFactor * edgeLength * edgeLength); } else { return 0.0; } } /** * Obtains the energy cost function for the specified node being moved. * This involves calling <code>getEdgeCrossing</code> for all * edges connected to the specified node * @param node * the node whose connected edges cost functions are to be * calculated * @return the total edge crossing energy of the connected edges */ protected double getEdgeCrossingAffectedEdges(int node) { double energy = 0.0; for (int i = 0; i < v[node].connectedEdges.length; i++) { energy += getEdgeCrossing(v[node].connectedEdges[i]); } return energy; } /** * This method calculates the energy of the distance from the specified * edge crossing any other edges. Each crossing add a constant factor * to the total energy * * @param i the index of the edge in the array <code>e</code> * @return the total edge crossing energy of the specified edge */ protected double getEdgeCrossing(int i) { // TODO Could have a cost function per edge int n = 0; // counts energy of edgecrossings through edge i // max and min variable for minimum bounding rectangles overlapping // checks double minjX, minjY, miniX, miniY, maxjX, maxjY, maxiX, maxiY; if (isOptimizeEdgeCrossing) { double iP1X = v[e[i].source].x; double iP1Y = v[e[i].source].y; double iP2X = v[e[i].target].x; double iP2Y = v[e[i].target].y; for (int j = 0; j < e.length; j++) { double jP1X = v[e[j].source].x; double jP1Y = v[e[j].source].y; double jP2X = v[e[j].target].x; double jP2Y = v[e[j].target].y; if (j != i) { // First check is to see if the minimum bounding rectangles // of the edges overlap at all. Since the layout tries // to separate nodes and shorten edges, the majority do not // overlap and this is a cheap way to avoid most of the // processing // Some long code to avoid a Math.max call... if (iP1X < iP2X) { miniX = iP1X; maxiX = iP2X; } else { miniX = iP2X; maxiX = iP1X; } if (jP1X < jP2X) { minjX = jP1X; maxjX = jP2X; } else { minjX = jP2X; maxjX = jP1X; } if (maxiX < minjX || miniX > maxjX) { continue; } if (iP1Y < iP2Y) { miniY = iP1Y; maxiY = iP2Y; } else { miniY = iP2Y; maxiY = iP1Y; } if (jP1Y < jP2Y) { minjY = jP1Y; maxjY = jP2Y; } else { minjY = jP2Y; maxjY = jP1Y; } if (maxiY < minjY || miniY > maxjY) { continue; } // Ignore if any end points are coincident if (((iP1X != jP1X) && (iP1Y != jP1Y)) && ((iP1X != jP2X) && (iP1Y != jP2Y)) && ((iP2X != jP1X) && (iP2Y != jP1Y)) && ((iP2X != jP2X) && (iP2Y != jP2Y))) { // Values of zero returned from Line2D.relativeCCW are // ignored because the point being exactly on the line // is very rare for double and we've already checked if // any end point share the same vertex. Should zero // ever be returned, it would be the vertex connected // to the edge that's actually on the edge and this is // dealt with by the node to edge distance cost // function. The worst case is that the vertex is // pushed off the edge faster than it would be // otherwise. Because of ignoring the zero this code // below can behave like only a 1 or -1 will be // returned. See Lines2D.linesIntersects(). boolean intersects = ((Line2D.relativeCCW(iP1X, iP1Y, iP2X, iP2Y, jP1X, jP1Y) != Line2D.relativeCCW( iP1X, iP1Y, iP2X, iP2Y, jP2X, jP2Y)) && (Line2D .relativeCCW(jP1X, jP1Y, jP2X, jP2Y, iP1X, iP1Y) != Line2D .relativeCCW(jP1X, jP1Y, jP2X, jP2Y, iP2X, iP2Y))); if (intersects) { n++; } } } } } return edgeCrossingCostFactor * n; } /** * This method calculates the energy of the distance between Cells and * Edges. This version of the edge distance cost calculates the energy * cost from a specified <strong>node</strong>. The distance cost to all * unconnected edges is calculated and the total returned. * * @param i the index of the node in the array <code>v</code> * @return the total edge distance energy of the node */ protected double getEdgeDistanceFromNode(int i) { double energy = 0.0; // This function is only performed during fine tuning for performance if (isOptimizeEdgeDistance && isFineTuning) { int[] edges = v[i].relevantEdges; for (int j = 0; j < edges.length; j++) { // Note that the distance value is squared double distSquare = Line2D.ptSegDistSq(v[e[edges[j]].source].x, v[e[edges[j]].source].y, v[e[edges[j]].target].x, v[e[edges[j]].target].y, v[i].x, v[i].y); distSquare -= v[i].radiusSquared; // prevents from dividing with Zero. No Math.abs() call // for performance if (distSquare < minDistanceLimitSquared) { distSquare = minDistanceLimitSquared; } // Only bother with the divide if the node and edge are // fairly close together if (distSquare < maxDistanceLimitSquared) { energy += edgeDistanceCostFactor / distSquare; } } } return energy; } /** * Obtains the energy cost function for the specified node being moved. * This involves calling <code>getEdgeDistanceFromEdge</code> for all * edges connected to the specified node * @param node * the node whose connected edges cost functions are to be * calculated * @return the total edge distance energy of the connected edges */ protected double getEdgeDistanceAffectedNodes(int node) { double energy = 0.0; for (int i = 0; i < (v[node].connectedEdges.length); i++) { energy += getEdgeDistanceFromEdge(v[node].connectedEdges[i]); } return energy; } /** * This method calculates the energy of the distance between Cells and * Edges. This version of the edge distance cost calculates the energy * cost from a specified <strong>edge</strong>. The distance cost to all * unconnected nodes is calculated and the total returned. * * @param i the index of the edge in the array <code>e</code> * @return the total edge distance energy of the edge */ protected double getEdgeDistanceFromEdge(int i) { double energy = 0.0; // This function is only performed during fine tuning for performance if (isOptimizeEdgeDistance && isFineTuning) { for (int j = 0; j < v.length; j++) { // Don't calculate for connected nodes if (e[i].source != j && e[i].target != j) { double distSquare = Line2D.ptSegDistSq(v[e[i].source].x, v[e[i].source].y, v[e[i].target].x, v[e[i].target].y, v[j].x, v[j].y); distSquare -= v[j].radiusSquared; // prevents from dividing with Zero. No Math.abs() call // for performance if (distSquare < minDistanceLimitSquared) distSquare = minDistanceLimitSquared; // Only bother with the divide if the node and edge are // fairly close together if (distSquare < maxDistanceLimitSquared) { energy += edgeDistanceCostFactor / distSquare; } } } } return energy; } /** * Hook method to adding additional energy factors into the layout. * Calculates the energy just for the specified node. * @param i the nodes whose energy is being calculated * @return the energy of this node caused by the additional factors */ protected double getAdditionFactorsEnergy(int i) { return 0.0; } /** * Returns all Edges that are not connected to the specified cell * * @param cellIndex * the cell index to which the edges are not connected * @return Array of all interesting Edges */ protected int[] getRelevantEdges(int cellIndex) { ArrayList<Integer> relevantEdgeList = new ArrayList<Integer>(e.length); for (int i = 0; i < e.length; i++) { if (e[i].source != cellIndex && e[i].target != cellIndex) { // Add non-connected edges relevantEdgeList.add(new Integer(i)); } } int[] relevantEdgeArray = new int[relevantEdgeList.size()]; Iterator<Integer> iter = relevantEdgeList.iterator(); //Reform the list into an array but replace Integer values with ints for (int i = 0; i < relevantEdgeArray.length; i++) { if (iter.hasNext()) { relevantEdgeArray[i] = iter.next().intValue(); } } return relevantEdgeArray; } /** * Returns all Edges that are connected with the specified cell * * @param cellIndex * the cell index to which the edges are connected * @return Array of all connected Edges */ protected int[] getConnectedEdges(int cellIndex) { ArrayList<Integer> connectedEdgeList = new ArrayList<Integer>(e.length); for (int i = 0; i < e.length; i++) { if (e[i].source == cellIndex || e[i].target == cellIndex) { // Add connected edges to list by their index number connectedEdgeList.add(new Integer(i)); } } int[] connectedEdgeArray = new int[connectedEdgeList.size()]; Iterator<Integer> iter = connectedEdgeList.iterator(); // Reform the list into an array but replace Integer values with ints for (int i = 0; i < connectedEdgeArray.length; i++) { if (iter.hasNext()) { connectedEdgeArray[i] = iter.next().intValue(); ; } } return connectedEdgeArray; } /** * Returns <code>Organic</code>, the name of this algorithm. */ public String toString() { return "Organic"; } /** * Internal representation of a node or edge that holds cached information * to enable the layout to perform more quickly and to simplify the code */ public class CellWrapper { /** * The actual graph cell this wrapper represents */ protected Object cell; /** * All edge that repel this cell, only used for nodes. This array * is equivalent to all edges unconnected to this node */ protected int[] relevantEdges = null; /** * the index of all connected edges in the <code>e</code> array * to this node. This is only used for nodes. */ protected int[] connectedEdges = null; /** * The x-coordinate position of this cell, nodes only */ protected double x; /** * The y-coordinate position of this cell, nodes only */ protected double y; /** * The approximate radius squared of this cell, nodes only. If * approxNodeDimensions is true on the layout this value holds the * width of the node squared */ protected double radiusSquared; /** * The height of the node squared, only used if approxNodeDimensions * is set to true. */ protected double heightSquared; /** * The index of the node attached to this edge as source, edges only */ protected int source; /** * The index of the node attached to this edge as target, edges only */ protected int target; /** * Constructs a new CellWrapper * @param cell the graph cell this wrapper represents */ public CellWrapper(Object cell) { this.cell = cell; } /** * @return the relevantEdges */ public int[] getRelevantEdges() { return relevantEdges; } /** * @param relevantEdges the relevantEdges to set */ public void setRelevantEdges(int[] relevantEdges) { this.relevantEdges = relevantEdges; } /** * @return the connectedEdges */ public int[] getConnectedEdges() { return connectedEdges; } /** * @param connectedEdges the connectedEdges to set */ public void setConnectedEdges(int[] connectedEdges) { this.connectedEdges = connectedEdges; } /** * @return the x */ public double getX() { return x; } /** * @param x the x to set */ public void setX(double x) { this.x = x; } /** * @return the y */ public double getY() { return y; } /** * @param y the y to set */ public void setY(double y) { this.y = y; } /** * @return the radiusSquared */ public double getRadiusSquared() { return radiusSquared; } /** * @param radiusSquared the radiusSquared to set */ public void setRadiusSquared(double radiusSquared) { this.radiusSquared = radiusSquared; } /** * @return the heightSquared */ public double getHeightSquared() { return heightSquared; } /** * @param heightSquared the heightSquared to set */ public void setHeightSquared(double heightSquared) { this.heightSquared = heightSquared; } /** * @return the source */ public int getSource() { return source; } /** * @param source the source to set */ public void setSource(int source) { this.source = source; } /** * @return the target */ public int getTarget() { return target; } /** * @param target the target to set */ public void setTarget(int target) { this.target = target; } /** * @return the cell */ public Object getCell() { return cell; } } /** * @return Returns the averageNodeArea. */ public double getAverageNodeArea() { return averageNodeArea; } /** * @param averageNodeArea The averageNodeArea to set. */ public void setAverageNodeArea(double averageNodeArea) { this.averageNodeArea = averageNodeArea; } /** * @return Returns the borderLineCostFactor. */ public double getBorderLineCostFactor() { return borderLineCostFactor; } /** * @param borderLineCostFactor The borderLineCostFactor to set. */ public void setBorderLineCostFactor(double borderLineCostFactor) { this.borderLineCostFactor = borderLineCostFactor; } /** * @return Returns the edgeCrossingCostFactor. */ public double getEdgeCrossingCostFactor() { return edgeCrossingCostFactor; } /** * @param edgeCrossingCostFactor The edgeCrossingCostFactor to set. */ public void setEdgeCrossingCostFactor(double edgeCrossingCostFactor) { this.edgeCrossingCostFactor = edgeCrossingCostFactor; } /** * @return Returns the edgeDistanceCostFactor. */ public double getEdgeDistanceCostFactor() { return edgeDistanceCostFactor; } /** * @param edgeDistanceCostFactor The edgeDistanceCostFactor to set. */ public void setEdgeDistanceCostFactor(double edgeDistanceCostFactor) { this.edgeDistanceCostFactor = edgeDistanceCostFactor; } /** * @return Returns the edgeLengthCostFactor. */ public double getEdgeLengthCostFactor() { return edgeLengthCostFactor; } /** * @param edgeLengthCostFactor The edgeLengthCostFactor to set. */ public void setEdgeLengthCostFactor(double edgeLengthCostFactor) { this.edgeLengthCostFactor = edgeLengthCostFactor; } /** * @return Returns the fineTuningRadius. */ public double getFineTuningRadius() { return fineTuningRadius; } /** * @param fineTuningRadius The fineTuningRadius to set. */ public void setFineTuningRadius(double fineTuningRadius) { this.fineTuningRadius = fineTuningRadius; } /** * @return Returns the initialMoveRadius. */ public double getInitialMoveRadius() { return initialMoveRadius; } /** * @param initialMoveRadius The initialMoveRadius to set. */ public void setInitialMoveRadius(double initialMoveRadius) { this.initialMoveRadius = initialMoveRadius; } /** * @return Returns the isFineTuning. */ public boolean isFineTuning() { return isFineTuning; } /** * @param isFineTuning The isFineTuning to set. */ public void setFineTuning(boolean isFineTuning) { this.isFineTuning = isFineTuning; } /** * @return Returns the isOptimizeBorderLine. */ public boolean isOptimizeBorderLine() { return isOptimizeBorderLine; } /** * @param isOptimizeBorderLine The isOptimizeBorderLine to set. */ public void setOptimizeBorderLine(boolean isOptimizeBorderLine) { this.isOptimizeBorderLine = isOptimizeBorderLine; } /** * @return Returns the isOptimizeEdgeCrossing. */ public boolean isOptimizeEdgeCrossing() { return isOptimizeEdgeCrossing; } /** * @param isOptimizeEdgeCrossing The isOptimizeEdgeCrossing to set. */ public void setOptimizeEdgeCrossing(boolean isOptimizeEdgeCrossing) { this.isOptimizeEdgeCrossing = isOptimizeEdgeCrossing; } /** * @return Returns the isOptimizeEdgeDistance. */ public boolean isOptimizeEdgeDistance() { return isOptimizeEdgeDistance; } /** * @param isOptimizeEdgeDistance The isOptimizeEdgeDistance to set. */ public void setOptimizeEdgeDistance(boolean isOptimizeEdgeDistance) { this.isOptimizeEdgeDistance = isOptimizeEdgeDistance; } /** * @return Returns the isOptimizeEdgeLength. */ public boolean isOptimizeEdgeLength() { return isOptimizeEdgeLength; } /** * @param isOptimizeEdgeLength The isOptimizeEdgeLength to set. */ public void setOptimizeEdgeLength(boolean isOptimizeEdgeLength) { this.isOptimizeEdgeLength = isOptimizeEdgeLength; } /** * @return Returns the isOptimizeNodeDistribution. */ public boolean isOptimizeNodeDistribution() { return isOptimizeNodeDistribution; } /** * @param isOptimizeNodeDistribution The isOptimizeNodeDistribution to set. */ public void setOptimizeNodeDistribution(boolean isOptimizeNodeDistribution) { this.isOptimizeNodeDistribution = isOptimizeNodeDistribution; } /** * @return Returns the maxIterations. */ public int getMaxIterations() { return maxIterations; } /** * @param maxIterations The maxIterations to set. */ public void setMaxIterations(int maxIterations) { this.maxIterations = maxIterations; } /** * @return Returns the minDistanceLimit. */ public double getMinDistanceLimit() { return minDistanceLimit; } /** * @param minDistanceLimit The minDistanceLimit to set. */ public void setMinDistanceLimit(double minDistanceLimit) { this.minDistanceLimit = minDistanceLimit; } /** * @return Returns the minMoveRadius. */ public double getMinMoveRadius() { return minMoveRadius; } /** * @param minMoveRadius The minMoveRadius to set. */ public void setMinMoveRadius(double minMoveRadius) { this.minMoveRadius = minMoveRadius; } /** * @return Returns the nodeDistributionCostFactor. */ public double getNodeDistributionCostFactor() { return nodeDistributionCostFactor; } /** * @param nodeDistributionCostFactor The nodeDistributionCostFactor to set. */ public void setNodeDistributionCostFactor(double nodeDistributionCostFactor) { this.nodeDistributionCostFactor = nodeDistributionCostFactor; } /** * @return Returns the radiusScaleFactor. */ public double getRadiusScaleFactor() { return radiusScaleFactor; } /** * @param radiusScaleFactor The radiusScaleFactor to set. */ public void setRadiusScaleFactor(double radiusScaleFactor) { this.radiusScaleFactor = radiusScaleFactor; } /** * @return Returns the triesPerCell. */ public int getTriesPerCell() { return triesPerCell; } /** * @param triesPerCell The triesPerCell to set. */ public void setTriesPerCell(int triesPerCell) { this.triesPerCell = triesPerCell; } /** * @return Returns the unchangedEnergyRoundTermination. */ public int getUnchangedEnergyRoundTermination() { return unchangedEnergyRoundTermination; } /** * @param unchangedEnergyRoundTermination The unchangedEnergyRoundTermination to set. */ public void setUnchangedEnergyRoundTermination( int unchangedEnergyRoundTermination) { this.unchangedEnergyRoundTermination = unchangedEnergyRoundTermination; } /** * @return Returns the maxDistanceLimit. */ public double getMaxDistanceLimit() { return maxDistanceLimit; } /** * @param maxDistanceLimit The maxDistanceLimit to set. */ public void setMaxDistanceLimit(double maxDistanceLimit) { this.maxDistanceLimit = maxDistanceLimit; } /** * @return the approxNodeDimensions */ public boolean isApproxNodeDimensions() { return approxNodeDimensions; } /** * @param approxNodeDimensions the approxNodeDimensions to set */ public void setApproxNodeDimensions(boolean approxNodeDimensions) { this.approxNodeDimensions = approxNodeDimensions; } /** * @return the disableEdgeStyle */ public boolean isDisableEdgeStyle() { return disableEdgeStyle; } /** * @param disableEdgeStyle the disableEdgeStyle to set */ public void setDisableEdgeStyle(boolean disableEdgeStyle) { this.disableEdgeStyle = disableEdgeStyle; } /** * @return the resetEdges */ public boolean isResetEdges() { return resetEdges; } /** * @param resetEdges the resetEdges to set */ public void setResetEdges(boolean resetEdges) { this.resetEdges = resetEdges; } }