/**
* Copyright (c) 2005-2012, JGraph Ltd
*/
package com.mxgraph.layout.hierarchical.stage;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingConstants;
import com.mxgraph.layout.hierarchical.mxHierarchicalLayout;
import com.mxgraph.layout.hierarchical.model.mxGraphAbstractHierarchyCell;
import com.mxgraph.layout.hierarchical.model.mxGraphHierarchyEdge;
import com.mxgraph.layout.hierarchical.model.mxGraphHierarchyModel;
import com.mxgraph.layout.hierarchical.model.mxGraphHierarchyNode;
import com.mxgraph.layout.hierarchical.model.mxGraphHierarchyRank;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxGraph;
/**
* Sets the horizontal locations of node and edge dummy nodes on each layer.
* Uses median down and up weighings as well as heuristics to straighten edges as
* far as possible.
*/
public class mxCoordinateAssignment implements mxHierarchicalLayoutStage
{
enum HierarchicalEdgeStyle
{
ORTHOGONAL, POLYLINE, STRAIGHT
}
/**
* Reference to the enclosing layout algorithm
*/
protected mxHierarchicalLayout layout;
/**
* The minimum buffer between cells on the same rank
*/
protected double intraCellSpacing = 30.0;
/**
* The minimum distance between cells on adjacent ranks
*/
protected double interRankCellSpacing = 30.0;
/**
* The distance between each parallel edge on each ranks for long edges
*/
protected double parallelEdgeSpacing = 4.0;
/**
* The buffer on either side of a vertex where edges must not connect.
*/
protected double vertexConnectionBuffer = 0.0;
/**
* The number of heuristic iterations to run
*/
protected int maxIterations = 8;
/**
* The preferred horizontal distance between edges exiting a vertex
*/
protected int prefHozEdgeSep = 5;
/**
* The preferred vertical offset between edges exiting a vertex
*/
protected int prefVertEdgeOff = 2;
/**
* The minimum distance for an edge jetty from a vertex
*/
protected int minEdgeJetty = 12;
/**
* The size of the vertical buffer in the center of inter-rank channels
* where edge control points should not be placed
*/
protected int channelBuffer = 4;
/**
* Map of internal edges and (x,y) pair of positions of the start and end jetty
* for that edge where it connects to the source and target vertices.
* Note this should technically be a WeakHashMap, but since JS does not
* have an equivalent, housekeeping must be performed before using.
* i.e. check all edges are still in the model and clear the values.
* Note that the y co-ord is the offset of the jetty, not the
* absolute point
*/
protected Map<mxGraphHierarchyEdge, double[]> jettyPositions = new HashMap<mxGraphHierarchyEdge, double[]>();
/**
* The position of the root ( start ) node(s) relative to the rest of the
* laid out graph
*/
protected int orientation = SwingConstants.NORTH;
/**
* The minimum x position node placement starts at
*/
protected double initialX;
/**
* The maximum x value this positioning lays up to
*/
protected double limitX;
/**
* The sum of x-displacements for the current iteration
*/
protected double currentXDelta;
/**
* The rank that has the widest x position
*/
protected int widestRank;
/**
* Internal cache of top-most values of Y for each rank
*/
protected double[] rankTopY;
/**
* Internal cache of bottom-most value of Y for each rank
*/
protected double[] rankBottomY;
/**
* The X-coordinate of the edge of the widest rank
*/
protected double widestRankValue;
/**
* The width of all the ranks
*/
protected double[] rankWidths;
/**
* The Y-coordinate of all the ranks
*/
protected double[] rankY;
/**
* Whether or not to perform local optimisations and iterate multiple times
* through the algorithm
*/
protected boolean fineTuning = 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 style to apply between cell layers to edge segments
*/
protected HierarchicalEdgeStyle edgeStyle = HierarchicalEdgeStyle.POLYLINE;
/**
* A store of connections to the layer above for speed
*/
protected mxGraphAbstractHierarchyCell[][] nextLayerConnectedCache;
/**
* Padding added to resized parents
*/
protected int groupPadding = 10;
/**
* A store of connections to the layer below for speed
*/
protected mxGraphAbstractHierarchyCell[][] previousLayerConnectedCache;
/** The logger for this class */
private static Logger logger = Logger
.getLogger("com.jgraph.layout.hierarchical.JGraphCoordinateAssignment");
/**
* Creates a coordinate assignment.
*
* @param intraCellSpacing
* the minimum buffer between cells on the same rank
* @param interRankCellSpacing
* the minimum distance between cells on adjacent ranks
* @param orientation
* the position of the root node(s) relative to the graph
* @param initialX
* the leftmost coordinate node placement starts at
*/
public mxCoordinateAssignment(mxHierarchicalLayout layout,
double intraCellSpacing, double interRankCellSpacing,
int orientation, double initialX, double parallelEdgeSpacing)
{
this.layout = layout;
this.intraCellSpacing = intraCellSpacing;
this.interRankCellSpacing = interRankCellSpacing;
this.orientation = orientation;
this.initialX = initialX;
this.parallelEdgeSpacing = parallelEdgeSpacing;
setLoggerLevel(Level.OFF);
}
/**
* Utility method to display the x co-ords
*/
public void printStatus()
{
mxGraphHierarchyModel model = layout.getModel();
System.out.println("======Coord assignment debug=======");
for (int j = 0; j < model.ranks.size(); j++)
{
System.out.print("Rank " + j + " : " );
mxGraphHierarchyRank rank = model.ranks
.get(new Integer(j));
Iterator<mxGraphAbstractHierarchyCell> iter = rank
.iterator();
while (iter.hasNext())
{
mxGraphAbstractHierarchyCell cell = iter.next();
System.out.print(cell.getX(j) + " ");
}
System.out.println();
}
System.out.println("====================================");
}
/**
* A basic horizontal coordinate assignment algorithm
*/
public void execute(Object parent)
{
mxGraphHierarchyModel model = layout.getModel();
currentXDelta = 0.0;
initialCoords(layout.getGraph(), model);
if (fineTuning)
{
minNode(model);
}
double bestXDelta = 100000000.0;
if (fineTuning)
{
for (int i = 0; i < maxIterations; i++)
{
// Median Heuristic
if (i != 0)
{
medianPos(i, model);
minNode(model);
}
// if the total offset is less for the current positioning,
// there are less heavily angled edges and so the current
// positioning is used
if (currentXDelta < bestXDelta)
{
for (int j = 0; j < model.ranks.size(); j++)
{
mxGraphHierarchyRank rank = model.ranks
.get(new Integer(j));
Iterator<mxGraphAbstractHierarchyCell> iter = rank
.iterator();
while (iter.hasNext())
{
mxGraphAbstractHierarchyCell cell = iter.next();
cell.setX(j, cell.getGeneralPurposeVariable(j));
}
}
bestXDelta = currentXDelta;
}
else
{
// Restore the best positions
for (int j = 0; j < model.ranks.size(); j++)
{
mxGraphHierarchyRank rank = model.ranks
.get(new Integer(j));
Iterator<mxGraphAbstractHierarchyCell> iter = rank
.iterator();
while (iter.hasNext())
{
mxGraphAbstractHierarchyCell cell = iter.next();
cell.setGeneralPurposeVariable(j,
(int) cell.getX(j));
}
}
}
minPath(model);
currentXDelta = 0;
}
}
setCellLocations(layout.getGraph(), model);
}
/**
* Performs one median positioning sweep in both directions
*
* @param model
* an internal model of the hierarchical layout
*/
private void minNode(mxGraphHierarchyModel model)
{
// Queue all nodes
LinkedList<WeightedCellSorter> nodeList = new LinkedList<WeightedCellSorter>();
// Need to be able to map from cell to cellWrapper
Map<mxGraphAbstractHierarchyCell, WeightedCellSorter> map = new Hashtable<mxGraphAbstractHierarchyCell, WeightedCellSorter>();
mxGraphAbstractHierarchyCell[][] rank = new mxGraphAbstractHierarchyCell[model.maxRank + 1][];
for (int i = 0; i <= model.maxRank; i++)
{
mxGraphHierarchyRank rankSet = model.ranks.get(new Integer(i));
rank[i] = rankSet.toArray(new mxGraphAbstractHierarchyCell[rankSet
.size()]);
for (int j = 0; j < rank[i].length; j++)
{
// Use the weight to store the rank and visited to store whether
// or not the cell is in the list
mxGraphAbstractHierarchyCell cell = rank[i][j];
WeightedCellSorter cellWrapper = new WeightedCellSorter(cell, i);
cellWrapper.rankIndex = j;
cellWrapper.visited = true;
nodeList.add(cellWrapper);
map.put(cell, cellWrapper);
}
}
// Set a limit of the maximum number of times we will access the queue
// in case a loop appears
int maxTries = nodeList.size() * 10;
int count = 0;
// Don't move cell within this value of their median
int tolerance = 1;
while (!nodeList.isEmpty() && count <= maxTries)
{
WeightedCellSorter cellWrapper = nodeList.getFirst();
mxGraphAbstractHierarchyCell cell = cellWrapper.cell;
int rankValue = cellWrapper.weightedValue;
int rankIndex = cellWrapper.rankIndex;
Object[] nextLayerConnectedCells = cell.getNextLayerConnectedCells(
rankValue).toArray();
Object[] previousLayerConnectedCells = cell
.getPreviousLayerConnectedCells(rankValue).toArray();
int numNextLayerConnected = nextLayerConnectedCells.length;
int numPreviousLayerConnected = previousLayerConnectedCells.length;
int medianNextLevel = medianXValue(nextLayerConnectedCells,
rankValue + 1);
int medianPreviousLevel = medianXValue(previousLayerConnectedCells,
rankValue - 1);
int numConnectedNeighbours = numNextLayerConnected
+ numPreviousLayerConnected;
int currentPosition = cell.getGeneralPurposeVariable(rankValue);
double cellMedian = currentPosition;
if (numConnectedNeighbours > 0)
{
cellMedian = (medianNextLevel * numNextLayerConnected + medianPreviousLevel
* numPreviousLayerConnected)
/ numConnectedNeighbours;
}
// Flag storing whether or not position has changed
boolean positionChanged = false;
if (cellMedian < currentPosition - tolerance)
{
if (rankIndex == 0)
{
cell.setGeneralPurposeVariable(rankValue, (int) cellMedian);
positionChanged = true;
}
else
{
mxGraphAbstractHierarchyCell leftCell = rank[rankValue][rankIndex - 1];
int leftLimit = leftCell
.getGeneralPurposeVariable(rankValue);
leftLimit = leftLimit + (int) leftCell.width / 2
+ (int) intraCellSpacing + (int) cell.width / 2;
if (leftLimit < cellMedian)
{
cell.setGeneralPurposeVariable(rankValue,
(int) cellMedian);
positionChanged = true;
}
else if (leftLimit < cell
.getGeneralPurposeVariable(rankValue) - tolerance)
{
cell.setGeneralPurposeVariable(rankValue, leftLimit);
positionChanged = true;
}
}
}
else if (cellMedian > currentPosition + tolerance)
{
int rankSize = rank[rankValue].length;
if (rankIndex == rankSize - 1)
{
cell.setGeneralPurposeVariable(rankValue, (int) cellMedian);
positionChanged = true;
}
else
{
mxGraphAbstractHierarchyCell rightCell = rank[rankValue][rankIndex + 1];
int rightLimit = rightCell
.getGeneralPurposeVariable(rankValue);
rightLimit = rightLimit - (int) rightCell.width / 2
- (int) intraCellSpacing - (int) cell.width / 2;
if (rightLimit > cellMedian)
{
cell.setGeneralPurposeVariable(rankValue,
(int) cellMedian);
positionChanged = true;
}
else if (rightLimit > cell
.getGeneralPurposeVariable(rankValue) + tolerance)
{
cell.setGeneralPurposeVariable(rankValue, rightLimit);
positionChanged = true;
}
}
}
if (positionChanged)
{
// Add connected nodes to map and list
for (int i = 0; i < nextLayerConnectedCells.length; i++)
{
mxGraphAbstractHierarchyCell connectedCell = (mxGraphAbstractHierarchyCell) nextLayerConnectedCells[i];
WeightedCellSorter connectedCellWrapper = map
.get(connectedCell);
if (connectedCellWrapper != null)
{
if (connectedCellWrapper.visited == false)
{
connectedCellWrapper.visited = true;
nodeList.add(connectedCellWrapper);
}
}
}
// Add connected nodes to map and list
for (int i = 0; i < previousLayerConnectedCells.length; i++)
{
mxGraphAbstractHierarchyCell connectedCell = (mxGraphAbstractHierarchyCell) previousLayerConnectedCells[i];
WeightedCellSorter connectedCellWrapper = map
.get(connectedCell);
if (connectedCellWrapper != null)
{
if (connectedCellWrapper.visited == false)
{
connectedCellWrapper.visited = true;
nodeList.add(connectedCellWrapper);
}
}
}
}
nodeList.removeFirst();
cellWrapper.visited = false;
count++;
}
}
/**
* Performs one median positioning sweep in one direction
*
* @param i
* the iteration of the whole process
* @param model
* an internal model of the hierarchical layout
*/
private void medianPos(int i, mxGraphHierarchyModel model)
{
// Reverse sweep direction each time through this method
boolean downwardSweep = (i % 2 == 0);
if (downwardSweep)
{
for (int j = model.maxRank; j > 0; j--)
{
rankMedianPosition(j - 1, model, j);
}
}
else
{
for (int j = 0; j < model.maxRank - 1; j++)
{
rankMedianPosition(j + 1, model, j);
}
}
}
/**
* Performs median minimisation over one rank.
*
* @param rankValue
* the layer number of this rank
* @param model
* an internal model of the hierarchical layout
* @param nextRankValue
* the layer number whose connected cels are to be laid out
* relative to
*/
protected void rankMedianPosition(int rankValue,
mxGraphHierarchyModel model, int nextRankValue)
{
mxGraphHierarchyRank rankSet = model.ranks.get(new Integer(rankValue));
Object[] rank = rankSet.toArray();
// Form an array of the order in which the cells are to be processed
// , the order is given by the weighted sum of the in or out edges,
// depending on whether we're travelling up or down the hierarchy.
WeightedCellSorter[] weightedValues = new WeightedCellSorter[rank.length];
Map<mxGraphAbstractHierarchyCell, WeightedCellSorter> cellMap = new Hashtable<mxGraphAbstractHierarchyCell, WeightedCellSorter>(
rank.length);
for (int i = 0; i < rank.length; i++)
{
mxGraphAbstractHierarchyCell currentCell = (mxGraphAbstractHierarchyCell) rank[i];
weightedValues[i] = new WeightedCellSorter();
weightedValues[i].cell = currentCell;
weightedValues[i].rankIndex = i;
cellMap.put(currentCell, weightedValues[i]);
Collection<mxGraphAbstractHierarchyCell> nextLayerConnectedCells = null;
if (nextRankValue < rankValue)
{
nextLayerConnectedCells = currentCell
.getPreviousLayerConnectedCells(rankValue);
}
else
{
nextLayerConnectedCells = currentCell
.getNextLayerConnectedCells(rankValue);
}
// Calculate the weighing based on this node type and those this
// node is connected to on the next layer
weightedValues[i].weightedValue = calculatedWeightedValue(
currentCell, nextLayerConnectedCells);
}
Arrays.sort(weightedValues);
// Set the new position of each node within the rank using
// its temp variable
for (int i = 0; i < weightedValues.length; i++)
{
int numConnectionsNextLevel = 0;
mxGraphAbstractHierarchyCell cell = weightedValues[i].cell;
Object[] nextLayerConnectedCells = null;
int medianNextLevel = 0;
if (nextRankValue < rankValue)
{
nextLayerConnectedCells = cell.getPreviousLayerConnectedCells(
rankValue).toArray();
}
else
{
nextLayerConnectedCells = cell.getNextLayerConnectedCells(
rankValue).toArray();
}
if (nextLayerConnectedCells != null)
{
numConnectionsNextLevel = nextLayerConnectedCells.length;
if (numConnectionsNextLevel > 0)
{
medianNextLevel = medianXValue(nextLayerConnectedCells,
nextRankValue);
}
else
{
// For case of no connections on the next level set the
// median to be the current position and try to be
// positioned there
medianNextLevel = cell.getGeneralPurposeVariable(rankValue);
}
}
double leftBuffer = 0.0;
double leftLimit = -100000000.0;
for (int j = weightedValues[i].rankIndex - 1; j >= 0;)
{
WeightedCellSorter weightedValue = cellMap.get(rank[j]);
if (weightedValue != null)
{
mxGraphAbstractHierarchyCell leftCell = weightedValue.cell;
if (weightedValue.visited)
{
// The left limit is the right hand limit of that
// cell plus any allowance for unallocated cells
// in-between
leftLimit = leftCell
.getGeneralPurposeVariable(rankValue)
+ leftCell.width
/ 2.0
+ intraCellSpacing
+ leftBuffer + cell.width / 2.0;
j = -1;
}
else
{
leftBuffer += leftCell.width + intraCellSpacing;
j--;
}
}
}
double rightBuffer = 0.0;
double rightLimit = 100000000.0;
for (int j = weightedValues[i].rankIndex + 1; j < weightedValues.length;)
{
WeightedCellSorter weightedValue = cellMap.get(rank[j]);
if (weightedValue != null)
{
mxGraphAbstractHierarchyCell rightCell = weightedValue.cell;
if (weightedValue.visited)
{
// The left limit is the right hand limit of that
// cell plus any allowance for unallocated cells
// in-between
rightLimit = rightCell
.getGeneralPurposeVariable(rankValue)
- rightCell.width
/ 2.0
- intraCellSpacing
- rightBuffer - cell.width / 2.0;
j = weightedValues.length;
}
else
{
rightBuffer += rightCell.width + intraCellSpacing;
j++;
}
}
}
if (medianNextLevel >= leftLimit && medianNextLevel <= rightLimit)
{
cell.setGeneralPurposeVariable(rankValue, medianNextLevel);
}
else if (medianNextLevel < leftLimit)
{
// Couldn't place at median value, place as close to that
// value as possible
cell.setGeneralPurposeVariable(rankValue, (int) leftLimit);
currentXDelta += leftLimit - medianNextLevel;
}
else if (medianNextLevel > rightLimit)
{
// Couldn't place at median value, place as close to that
// value as possible
cell.setGeneralPurposeVariable(rankValue, (int) rightLimit);
currentXDelta += medianNextLevel - rightLimit;
}
weightedValues[i].visited = true;
}
}
/**
* Calculates the priority the specified cell has based on the type of its
* cell and the cells it is connected to on the next layer
*
* @param currentCell
* the cell whose weight is to be calculated
* @param collection
* the cells the specified cell is connected to
* @return the total weighted of the edges between these cells
*/
private int calculatedWeightedValue(
mxGraphAbstractHierarchyCell currentCell,
Collection<mxGraphAbstractHierarchyCell> collection)
{
int totalWeight = 0;
Iterator<mxGraphAbstractHierarchyCell> iter = collection.iterator();
while (iter.hasNext())
{
mxGraphAbstractHierarchyCell cell = iter.next();
if (currentCell.isVertex() && cell.isVertex())
{
totalWeight++;
}
else if (currentCell.isEdge() && cell.isEdge())
{
totalWeight += 8;
}
else
{
totalWeight += 2;
}
}
return totalWeight;
}
/**
* Calculates the median position of the connected cell on the specified
* rank
*
* @param connectedCells
* the cells the candidate connects to on this level
* @param rankValue
* the layer number of this rank
* @return the median rank order ( not x position ) of the connected cells
*/
private int medianXValue(Object[] connectedCells, int rankValue)
{
if (connectedCells.length == 0)
{
return 0;
}
int[] medianValues = new int[connectedCells.length];
for (int i = 0; i < connectedCells.length; i++)
{
medianValues[i] = ((mxGraphAbstractHierarchyCell) connectedCells[i])
.getGeneralPurposeVariable(rankValue);
}
Arrays.sort(medianValues);
if (connectedCells.length % 2 == 1)
{
// For odd numbers of adjacent vertices return the median
return medianValues[connectedCells.length / 2];
}
else
{
int medianPoint = connectedCells.length / 2;
int leftMedian = medianValues[medianPoint - 1];
int rightMedian = medianValues[medianPoint];
return ((leftMedian + rightMedian) / 2);
}
}
/**
* Sets up the layout in an initial positioning. The ranks are all centered
* as much as possible along the middle vertex in each rank. The other cells
* are then placed as close as possible on either side.
*
* @param facade
* the facade describing the input graph
* @param model
* an internal model of the hierarchical layout
*/
private void initialCoords(mxGraph facade, mxGraphHierarchyModel model)
{
calculateWidestRank(facade, model);
// Sweep up and down from the widest rank
for (int i = widestRank; i >= 0; i--)
{
if (i < model.maxRank)
{
rankCoordinates(i, facade, model);
}
}
for (int i = widestRank + 1; i <= model.maxRank; i++)
{
if (i > 0)
{
rankCoordinates(i, facade, model);
}
}
}
/**
* Sets up the layout in an initial positioning. All the first cells in each
* rank are moved to the left and the rest of the rank inserted as close
* together as their size and buffering permits. This method works on just
* the specified rank.
*
* @param rankValue
* the current rank being processed
* @param graph
* the facade describing the input graph
* @param model
* an internal model of the hierarchical layout
*/
protected void rankCoordinates(int rankValue, mxGraph graph,
mxGraphHierarchyModel model)
{
mxGraphHierarchyRank rank = model.ranks.get(new Integer(rankValue));
double maxY = 0.0;
double localX = initialX + (widestRankValue - rankWidths[rankValue])
/ 2;
// Store whether or not any of the cells' bounds were unavailable so
// to only issue the warning once for all cells
boolean boundsWarning = false;
for (mxGraphAbstractHierarchyCell cell : rank)
{
if (cell.isVertex())
{
mxGraphHierarchyNode node = (mxGraphHierarchyNode) cell;
mxRectangle bounds = layout.getVertexBounds(node.cell);
if (bounds != null)
{
if (orientation == SwingConstants.NORTH
|| orientation == SwingConstants.SOUTH)
{
cell.width = bounds.getWidth();
cell.height = bounds.getHeight();
}
else
{
cell.width = bounds.getHeight();
cell.height = bounds.getWidth();
}
}
else
{
boundsWarning = true;
}
maxY = Math.max(maxY, cell.height);
}
else if (cell.isEdge())
{
mxGraphHierarchyEdge edge = (mxGraphHierarchyEdge) cell;
// The width is the number of additional parallel edges
// time the parallel edge spacing
int numEdges = 1;
if (edge.edges != null)
{
numEdges = edge.edges.size();
}
else
{
logger.info("edge.edges is null");
}
cell.width = (numEdges - 1) * parallelEdgeSpacing;
}
// Set the initial x-value as being the best result so far
localX += cell.width / 2.0;
cell.setX(rankValue, localX);
cell.setGeneralPurposeVariable(rankValue, (int) localX);
localX += cell.width / 2.0;
localX += intraCellSpacing;
}
if (boundsWarning == true)
{
logger.info("At least one cell has no bounds");
}
}
/**
* Calculates the width rank in the hierarchy. Also set the y value of each
* rank whilst performing the calculation
*
* @param graph
* the facade describing the input graph
* @param model
* an internal model of the hierarchical layout
*/
protected void calculateWidestRank(mxGraph graph,
mxGraphHierarchyModel model)
{
// Starting y co-ordinate
double y = -interRankCellSpacing;
// Track the widest cell on the last rank since the y
// difference depends on it
double lastRankMaxCellHeight = 0.0;
rankWidths = new double[model.maxRank + 1];
rankY = new double[model.maxRank + 1];
for (int rankValue = model.maxRank; rankValue >= 0; rankValue--)
{
// Keep track of the widest cell on this rank
double maxCellHeight = 0.0;
mxGraphHierarchyRank rank = model.ranks.get(new Integer(rankValue));
double localX = initialX;
// Store whether or not any of the cells' bounds were unavailable so
// to only issue the warning once for all cells
boolean boundsWarning = false;
Iterator<mxGraphAbstractHierarchyCell> iter = rank.iterator();
while (iter.hasNext())
{
mxGraphAbstractHierarchyCell cell = iter.next();
if (cell.isVertex())
{
mxGraphHierarchyNode node = (mxGraphHierarchyNode) cell;
mxRectangle bounds = layout.getVertexBounds(node.cell);
if (bounds != null)
{
if (orientation == SwingConstants.NORTH
|| orientation == SwingConstants.SOUTH)
{
cell.width = bounds.getWidth();
cell.height = bounds.getHeight();
}
else
{
cell.width = bounds.getHeight();
cell.height = bounds.getWidth();
}
}
else
{
boundsWarning = true;
}
maxCellHeight = Math.max(maxCellHeight, cell.height);
}
else if (cell.isEdge())
{
mxGraphHierarchyEdge edge = (mxGraphHierarchyEdge) cell;
// The width is the number of additional parallel edges
// time the parallel edge spacing
int numEdges = 1;
if (edge.edges != null)
{
numEdges = edge.edges.size();
}
else
{
logger.info("edge.edges is null");
}
cell.width = (numEdges - 1) * parallelEdgeSpacing;
}
// Set the initial x-value as being the best result so far
localX += cell.width / 2.0;
cell.setX(rankValue, localX);
cell.setGeneralPurposeVariable(rankValue, (int) localX);
localX += cell.width / 2.0;
localX += intraCellSpacing;
if (localX > widestRankValue)
{
widestRankValue = localX;
widestRank = rankValue;
}
rankWidths[rankValue] = localX;
}
if (boundsWarning == true)
{
logger.info("At least one cell has no bounds");
}
rankY[rankValue] = y;
double distanceToNextRank = maxCellHeight / 2.0
+ lastRankMaxCellHeight / 2.0 + interRankCellSpacing;
lastRankMaxCellHeight = maxCellHeight;
if (orientation == SwingConstants.NORTH
|| orientation == SwingConstants.WEST)
{
y += distanceToNextRank;
}
else
{
y -= distanceToNextRank;
}
iter = rank.iterator();
while (iter.hasNext())
{
mxGraphAbstractHierarchyCell cell = iter.next();
cell.setY(rankValue, y);
}
}
}
/**
* Straightens out chains of virtual nodes where possible
*
* @param model
* an internal model of the hierarchical layout
*/
protected void minPath(mxGraphHierarchyModel model)
{
// Work down and up each edge with at least 2 control points
// trying to straighten each one out. If the same number of
// straight segments are formed in both directions, the
// preferred direction used is the one where the final
// control points have the least offset from the connectable
// region of the terminating vertices
Map<Object, mxGraphHierarchyEdge> edges = model.getEdgeMapper();
for (mxGraphAbstractHierarchyCell cell : edges.values())
{
if (cell.maxRank > cell.minRank + 2)
{
int numEdgeLayers = cell.maxRank - cell.minRank - 1;
// At least two virtual nodes in the edge
// Check first whether the edge is already straight
int referenceX = cell
.getGeneralPurposeVariable(cell.minRank + 1);
boolean edgeStraight = true;
int refSegCount = 0;
for (int i = cell.minRank + 2; i < cell.maxRank; i++)
{
int x = cell.getGeneralPurposeVariable(i);
if (referenceX != x)
{
edgeStraight = false;
referenceX = x;
}
else
{
refSegCount++;
}
}
if (edgeStraight)
{
continue;
}
int upSegCount = 0;
int downSegCount = 0;
double upXPositions[] = new double[numEdgeLayers - 1];
double downXPositions[] = new double[numEdgeLayers - 1];
double currentX = cell.getX(cell.minRank + 1);
for (int i = cell.minRank + 1; i < cell.maxRank - 1; i++)
{
// Attempt to straight out the control point on the
// next segment up with the current control point.
double nextX = cell.getX(i + 1);
if (currentX == nextX)
{
upXPositions[i - cell.minRank - 1] = currentX;
upSegCount++;
}
else if (repositionValid(model, cell, i + 1, currentX))
{
upXPositions[i - cell.minRank - 1] = currentX;
upSegCount++;
// Leave currentX at same value
}
else
{
upXPositions[i - cell.minRank - 1] = nextX;
currentX = nextX;
}
}
currentX = cell.getX(cell.maxRank - 1);
for (int i = cell.maxRank - 1; i > cell.minRank + 1; i--)
{
// Attempt to straight out the control point on the
// next segment down with the current control point.
double nextX = cell.getX(i - 1);
if (currentX == nextX)
{
downXPositions[i - cell.minRank - 2] = currentX;
downSegCount++;
}
else if (repositionValid(model, cell, i - 1, currentX))
{
downXPositions[i - cell.minRank - 2] = currentX;
downSegCount++;
// Leave currentX at same value
}
else
{
downXPositions[i - cell.minRank - 2] = cell.getX(i-1);
currentX = nextX;
}
}
if (downSegCount <= refSegCount && upSegCount <= refSegCount)
{
// Neither of the new calculation provide a straighter edge
continue;
}
if (downSegCount >= upSegCount)
{
// Apply down calculation values
for (int i = cell.maxRank - 2; i > cell.minRank; i--)
{
cell.setX(i, (int) downXPositions[i - cell.minRank - 1]);
}
}
else if (upSegCount > downSegCount)
{
// Apply up calculation values
for (int i = cell.minRank + 2; i < cell.maxRank; i++)
{
cell.setX(i, (int) upXPositions[i - cell.minRank - 2]);
}
}
else
{
// Neither direction provided a favourable result
// But both calculations are better than the
// existing solution, so apply the one with minimal
// offset to attached vertices at either end.
}
}
}
}
/**
* Determines whether or not a node may be moved to the specified x
* position on the specified rank
* @param model the layout model
* @param cell the cell being analysed
* @param rank the layer of the cell
* @param position the x position being sought
* @return whether or not the virtual node can be moved to this position
*/
protected boolean repositionValid(mxGraphHierarchyModel model,
mxGraphAbstractHierarchyCell cell, int rank, double position)
{
mxGraphHierarchyRank rankSet = model.ranks.get(new Integer(rank));
mxGraphAbstractHierarchyCell[] rankArray = rankSet
.toArray(new mxGraphAbstractHierarchyCell[rankSet.size()]);
int rankIndex = -1;
for (int i = 0; i < rankArray.length; i++)
{
if (cell == rankArray[i])
{
rankIndex = i;
break;
}
}
if (rankIndex < 0)
{
return false;
}
int currentX = cell.getGeneralPurposeVariable(rank);
if (position < currentX)
{
// Trying to move node to the left.
if (rankIndex == 0)
{
// Left-most node, can move anywhere
return true;
}
mxGraphAbstractHierarchyCell leftCell = rankArray[rankIndex - 1];
int leftLimit = leftCell.getGeneralPurposeVariable(rank);
leftLimit = leftLimit + (int) leftCell.width / 2
+ (int) intraCellSpacing + (int) cell.width / 2;
if (leftLimit <= position)
{
return true;
}
else
{
return false;
}
}
else if (position > currentX)
{
// Trying to move node to the right.
if (rankIndex == rankArray.length - 1)
{
// Right-most node, can move anywhere
return true;
}
mxGraphAbstractHierarchyCell rightCell = rankArray[rankIndex + 1];
int rightLimit = rightCell.getGeneralPurposeVariable(rank);
rightLimit = rightLimit - (int) rightCell.width / 2
- (int) intraCellSpacing - (int) cell.width / 2;
if (rightLimit >= position)
{
return true;
}
else
{
return false;
}
}
return true;
}
/**
* Sets the cell locations in the facade to those stored after this layout
* processing step has completed.
*
* @param graph
* the facade describing the input graph
* @param model
* an internal model of the hierarchical layout
*/
protected void setCellLocations(mxGraph graph, mxGraphHierarchyModel model)
{
rankTopY = new double[model.ranks.size()];
rankBottomY = new double[model.ranks.size()];
for (int i = 0; i < model.ranks.size(); i++)
{
rankTopY[i] = Double.MAX_VALUE;
rankBottomY[i] = 0.0;
}
Set<Object> parentsChanged = null;
if (layout.isResizeParent())
{
parentsChanged = new HashSet<Object>();
}
Map<Object, mxGraphHierarchyEdge> edges = model.getEdgeMapper();
Map<Object, mxGraphHierarchyNode> vertices = model.getVertexMapper();
// Process vertices all first, since they define the lower and
// limits of each rank. Between these limits lie the channels
// where the edges can be routed across the graph
for (mxGraphHierarchyNode cell : vertices.values())
{
setVertexLocation(cell);
if (layout.isResizeParent())
{
parentsChanged.add(graph.getModel().getParent(cell.cell));
}
}
if (layout.isResizeParent())
{
adjustParents(parentsChanged);
}
// Post process edge styles. Needs the vertex locations set for initial
// values of the top and bottoms of each rank
if (this.edgeStyle == HierarchicalEdgeStyle.ORTHOGONAL
|| this.edgeStyle == HierarchicalEdgeStyle.POLYLINE)
{
localEdgeProcessing(model);
}
for (mxGraphAbstractHierarchyCell cell : edges.values())
{
setEdgePosition(cell);
}
}
/**
* Adjust parent cells whose child geometries have changed. The default
* implementation adjusts the group to just fit around the children with
* a padding.
*/
protected void adjustParents(Set<Object> parentsChanged)
{
layout.arrangeGroups(mxUtils.sortCells(parentsChanged, true).toArray(), groupPadding);
}
/**
* Separates the x position of edges as they connect to vertices
*
* @param model
* an internal model of the hierarchical layout
*/
protected void localEdgeProcessing(mxGraphHierarchyModel model)
{
// Check the map of jetty positions doesn't contain
// any deleted edges. We can't use a WeakHashMap because
// it doesn't translate to JS.
Map<Object, mxGraphHierarchyEdge> edgeMapping = model.getEdgeMapper();
if (edgeMapping != null && jettyPositions.size() != edgeMapping.size())
{
jettyPositions = new HashMap<mxGraphHierarchyEdge, double[]>();
}
//jettyPositions.removeAll();
// Iterate through each vertex, look at the edges connected in
// both directions.
for (int i = 0; i < model.ranks.size(); i++)
{
mxGraphHierarchyRank rank = model.ranks.get(new Integer(i));
// Iterate over the top rank and fill in the connection information
Iterator<mxGraphAbstractHierarchyCell> iter = rank.iterator();
while (iter.hasNext())
{
mxGraphAbstractHierarchyCell cell = iter.next();
if (cell.isVertex())
{
mxGraphAbstractHierarchyCell[] currentCells = (cell
.getPreviousLayerConnectedCells(i))
.toArray(new mxGraphAbstractHierarchyCell[cell
.getPreviousLayerConnectedCells(i).size()]);
int currentRank = i - 1;
// Two loops, last connected cells, and next
for (int k = 0; k < 2; k++)
{
if (currentRank > -1
&& currentRank < model.ranks.size()
&& currentCells != null
&& currentCells.length > 0)
{
WeightedCellSorter[] sortedCells = new WeightedCellSorter[currentCells.length];
for (int j = 0; j < currentCells.length; j++)
{
sortedCells[j] = new WeightedCellSorter(
currentCells[j],
-(int) currentCells[j].getX(currentRank));
}
Arrays.sort(sortedCells);
mxGraphHierarchyNode node = (mxGraphHierarchyNode) cell;
double leftLimit = node.x[0] - node.width / 2;
double rightLimit = leftLimit + node.width;
// Connected edge count starts at 1 to allow for buffer
// with edge of vertex
int connectedEdgeCount = 0;
int connectedEdgeGroupCount = 0;
mxGraphHierarchyEdge[] connectedEdges = new mxGraphHierarchyEdge[sortedCells.length];
// Calculate width requirements for all connected edges
for (int j = 0; j < sortedCells.length; j++)
{
mxGraphAbstractHierarchyCell innerCell = sortedCells[j].cell;
Collection<mxGraphHierarchyEdge> connections;
if (innerCell.isVertex())
{
// Get the connecting edge
if (k == 0)
{
connections = ((mxGraphHierarchyNode) cell).connectsAsSource;
}
else
{
connections = ((mxGraphHierarchyNode) cell).connectsAsTarget;
}
for (mxGraphHierarchyEdge connectedEdge : connections)
{
if (connectedEdge.source == innerCell
|| connectedEdge.target == innerCell)
{
connectedEdgeCount += connectedEdge.edges
.size();
connectedEdgeGroupCount++;
connectedEdges[j] = connectedEdge;
}
}
}
else
{
connectedEdgeCount += ((mxGraphHierarchyEdge) innerCell).edges
.size();
connectedEdgeGroupCount++;
connectedEdges[j] = (mxGraphHierarchyEdge) innerCell;
}
}
double requiredWidth = (connectedEdgeCount + 1)
* prefHozEdgeSep;
// Add a buffer on the edges of the vertex if the edge count allows
if (cell.width > requiredWidth
+ (2 * prefHozEdgeSep))
{
leftLimit += prefHozEdgeSep;
rightLimit -= prefHozEdgeSep;
}
double availableWidth = rightLimit - leftLimit;
double edgeSpacing = availableWidth / connectedEdgeCount;
double currentX = leftLimit + edgeSpacing / 2.0;
double currentYOffset = minEdgeJetty - prefVertEdgeOff;
double maxYOffset = 0;
for (int j = 0; j < connectedEdges.length; j++)
{
int numActualEdges = connectedEdges[j].edges
.size();
double[] pos = jettyPositions
.get(connectedEdges[j]);
if (pos == null
|| pos.length != 4 * numActualEdges)
{
pos = new double[4 * numActualEdges];
jettyPositions.put(connectedEdges[j], pos);
}
if (j < (float)connectedEdgeCount / 2.0f)
{
currentYOffset += prefVertEdgeOff;
}
else if (j > (float)connectedEdgeCount / 2.0f)
{
currentYOffset -= prefVertEdgeOff;
}
// Ignore the case if equals, this means the second of 2
// jettys with the same y (even number of edges)
for (int m = 0; m < numActualEdges; m++)
{
pos[m * 4 + k * 2] = currentX;
currentX += edgeSpacing;
pos[m * 4 + k * 2 + 1] = currentYOffset;
}
maxYOffset = Math.max(maxYOffset,
currentYOffset);
}
}
currentCells = (cell.getNextLayerConnectedCells(i))
.toArray(new mxGraphAbstractHierarchyCell[cell
.getNextLayerConnectedCells(i).size()]);
currentRank = i + 1;
}
}
}
}
}
/**
* Fixes the control points
* @param cell
*/
protected void setEdgePosition(mxGraphAbstractHierarchyCell cell)
{
mxGraphHierarchyEdge edge = (mxGraphHierarchyEdge) cell;
// For parallel edges we need to separate out the points a
// little
double offsetX = 0.0;
// Only set the edge control points once
if (edge.temp[0] != 101207)
{
int maxRank = edge.maxRank;
int minRank = edge.minRank;
if (maxRank == minRank)
{
maxRank = edge.source.maxRank;
minRank = edge.target.minRank;
}
Iterator<Object> parallelEdges = edge.edges.iterator();
int parallelEdgeCount = 0;
double[] jettys = jettyPositions.get(edge);
Object source = edge.isReversed() ? edge.target.cell : edge.source.cell;
while (parallelEdges.hasNext())
{
Object realEdge = parallelEdges.next();
Object realSource = layout.getGraph().getView().getVisibleTerminal(realEdge, true);
List<mxPoint> newPoints = new ArrayList<mxPoint>(edge.x.length);
// Single length reversed edges end up with the jettys in the wrong
// places. Since single length edges only have jettys, not segment
// control points, we just say the edge isn't reversed in this section
boolean reversed = edge.isReversed();
if (realSource != source)
{
// The real edges include all core model edges and these can go
// in both directions. If the source of the hierarchical model edge
// isn't the source of the specific real edge in this iteration
// treat if as reversed
reversed = !reversed;
}
// First jetty of edge
if (jettys != null)
{
int arrayOffset = reversed ? 2 : 0;
double y = reversed ? rankTopY[minRank] : rankBottomY[maxRank];
double jetty = jettys[parallelEdgeCount * 4 + 1 + arrayOffset];
// If the edge is reversed invert the y position within the channel,
// unless it is a single length edge
if (reversed)
{
jetty = -jetty;
}
y += jetty;
double x = jettys[parallelEdgeCount * 4 + arrayOffset];
if (orientation == SwingConstants.NORTH
|| orientation == SwingConstants.SOUTH)
{
newPoints.add(new mxPoint(x, y));
}
else
{
newPoints.add(new mxPoint(y, x));
}
}
// Declare variables to define loop through edge points and
// change direction if edge is reversed
int loopStart = edge.x.length - 1;
int loopLimit = -1;
int loopDelta = -1;
int currentRank = edge.maxRank - 1;
if (reversed)
{
loopStart = 0;
loopLimit = edge.x.length;
loopDelta = 1;
currentRank = edge.minRank + 1;
}
// Reversed edges need the points inserted in
// reverse order
for (int j = loopStart; (edge.maxRank != edge.minRank) && j != loopLimit; j += loopDelta)
{
// The horizontal position in a vertical layout
double positionX = edge.x[j] + offsetX;
// Work out the vertical positions in a vertical layout
// in the edge buffer channels above and below this rank
double topChannelY = (rankTopY[currentRank] + rankBottomY[currentRank + 1]) / 2.0;
double bottomChannelY = (rankTopY[currentRank - 1] + rankBottomY[currentRank]) / 2.0;
if (reversed)
{
double tmp = topChannelY;
topChannelY = bottomChannelY;
bottomChannelY = tmp;
}
if (orientation == SwingConstants.NORTH
|| orientation == SwingConstants.SOUTH)
{
newPoints.add(new mxPoint(positionX, topChannelY));
newPoints.add(new mxPoint(positionX, bottomChannelY));
}
else
{
newPoints.add(new mxPoint(topChannelY, positionX));
newPoints.add(new mxPoint(bottomChannelY, positionX));
}
limitX = Math.max(limitX, positionX);
// double currentY = (rankTopY[currentRank] + rankBottomY[currentRank]) / 2.0;
// System.out.println("topChannelY = " + topChannelY + " , "
// + "exact Y = " + edge.y[j]);
currentRank += loopDelta;
}
// Second jetty of edge
if (jettys != null)
{
int arrayOffset = reversed ? 2 : 0;
double rankY = reversed ? rankBottomY[maxRank] : rankTopY[minRank];
double jetty = jettys[parallelEdgeCount * 4 + 3 - arrayOffset];
if (reversed)
{
jetty = -jetty;
}
double y = rankY - jetty;
double x = jettys[parallelEdgeCount * 4 + 2 - arrayOffset];
if (orientation == SwingConstants.NORTH
|| orientation == SwingConstants.SOUTH)
{
newPoints.add(new mxPoint(x, y));
}
else
{
newPoints.add(new mxPoint(y, x));
}
}
if (edge.isReversed())
{
processReversedEdge(edge, realEdge);
}
layout.setEdgePoints(realEdge, newPoints);
// Increase offset so next edge is drawn next to
// this one
if (offsetX == 0.0)
{
offsetX = parallelEdgeSpacing;
}
else if (offsetX > 0)
{
offsetX = -offsetX;
}
else
{
offsetX = -offsetX + parallelEdgeSpacing;
}
parallelEdgeCount++;
}
edge.temp[0] = 101207;
}
}
/**
* Fixes the position of the specified vertex
* @param cell the vertex to position
*/
protected void setVertexLocation(mxGraphAbstractHierarchyCell cell)
{
mxGraphHierarchyNode node = (mxGraphHierarchyNode) cell;
Object realCell = node.cell;
double positionX = node.x[0] - node.width / 2;
double positionY = node.y[0] - node.height / 2;
// if (cell.minRank == -1)
// {
// System.out.println("invalid rank, never set");
// }
rankTopY[cell.minRank] = Math.min(rankTopY[cell.minRank], positionY);
rankBottomY[cell.minRank] = Math.max(rankBottomY[cell.minRank],
positionY + node.height);
if (orientation == SwingConstants.NORTH
|| orientation == SwingConstants.SOUTH)
{
layout.setVertexLocation(realCell, positionX, positionY);
}
else
{
layout.setVertexLocation(realCell, positionY, positionX);
}
limitX = Math.max(limitX, positionX + node.width);
}
/**
* Hook to add additional processing
*
* @param edge
* The hierarchical model edge
* @param realEdge
* The real edge in the graph
*/
protected void processReversedEdge(mxGraphHierarchyEdge edge,
Object realEdge)
{
// Added as hook for customer
}
/**
* A utility class used to track cells whilst sorting occurs on the weighted
* sum of their connected edges. Does not violate (x.compareTo(y)==0) ==
* (x.equals(y))
*/
protected class WeightedCellSorter implements Comparable<Object>
{
/**
* The weighted value of the cell stored
*/
public int weightedValue = 0;
/**
* Whether or not to flip equal weight values.
*/
public boolean nudge = false;
/**
* Whether or not this cell has been visited in the current assignment
*/
public boolean visited = false;
/**
* The index this cell is in the model rank
*/
public int rankIndex;
/**
* The cell whose median value is being calculated
*/
public mxGraphAbstractHierarchyCell cell = null;
public WeightedCellSorter()
{
this(null, 0);
}
public WeightedCellSorter(mxGraphAbstractHierarchyCell cell,
int weightedValue)
{
this.cell = cell;
this.weightedValue = weightedValue;
}
/**
* comparator on the medianValue
*
* @param arg0
* the object to be compared to
* @return the standard return you would expect when comparing two
* double
*/
public int compareTo(Object arg0)
{
if (arg0 instanceof WeightedCellSorter)
{
if (weightedValue > ((WeightedCellSorter) arg0).weightedValue)
{
return -1;
}
else if (weightedValue < ((WeightedCellSorter) arg0).weightedValue)
{
return 1;
}
}
return 0;
}
}
/**
* Utility class that stores a collection of vertices and edge points within
* a certain area. This area includes the buffer lengths of cells.
*/
protected class AreaSpatialCache extends Rectangle2D.Double
{
public Set<Object> cells = new HashSet<Object>();
}
/**
* @return Returns the interRankCellSpacing.
*/
public double getInterRankCellSpacing()
{
return interRankCellSpacing;
}
/**
* @param interRankCellSpacing
* The interRankCellSpacing to set.
*/
public void setInterRankCellSpacing(double interRankCellSpacing)
{
this.interRankCellSpacing = interRankCellSpacing;
}
/**
* @return Returns the intraCellSpacing.
*/
public double getIntraCellSpacing()
{
return intraCellSpacing;
}
/**
* @param intraCellSpacing
* The intraCellSpacing to set.
*/
public void setIntraCellSpacing(double intraCellSpacing)
{
this.intraCellSpacing = intraCellSpacing;
}
/**
* @return Returns the orientation.
*/
public int getOrientation()
{
return orientation;
}
/**
* @param orientation
* The orientation to set.
*/
public void setOrientation(int orientation)
{
this.orientation = orientation;
}
/**
* @return Returns the limitX.
*/
public double getLimitX()
{
return limitX;
}
/**
* @param limitX
* The limitX to set.
*/
public void setLimitX(double limitX)
{
this.limitX = limitX;
}
/**
* @return Returns the fineTuning.
*/
public boolean isFineTuning()
{
return fineTuning;
}
/**
* @param fineTuning
* The fineTuning to set.
*/
public void setFineTuning(boolean fineTuning)
{
this.fineTuning = fineTuning;
}
/**
* Sets the logging level of this class
*
* @param level
* the logging level to set
*/
public void setLoggerLevel(Level level)
{
try
{
logger.setLevel(level);
}
catch (SecurityException e)
{
// Probably running in an applet
}
}
}