/* * Copyright (c) 2005, David Benson * * All rights reserved. * * This file is licensed under the JGraph software license, a copy of which * will have been provided to you in the file LICENSE at the root of your * installation directory. If you are unable to locate this file please * contact JGraph sales for another copy. */ package com.jgraph.layout.hierarchical; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; 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.jgraph.layout.JGraphFacade; import com.jgraph.layout.hierarchical.model.JGraphAbstractHierarchyCell; import com.jgraph.layout.hierarchical.model.JGraphHierarchyEdge; import com.jgraph.layout.hierarchical.model.JGraphHierarchyModel; import com.jgraph.layout.hierarchical.model.JGraphHierarchyNode; import com.jgraph.layout.hierarchical.model.JGraphHierarchyRank; /** * Sets the horizontal locations of node and edge dummy nodes on each layer. * Uses median down and up weighings as well heuristic to straighten edges as * far as possible. */ public class JGraphCoordinateAssignment implements JGraphHierarchicalLayoutStep { /** * 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 = 10.0; /** * The number of heuristic iterations to run */ protected int maxIterations = 8; /** * 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; /** * The X-coordinate of the edge of the widest rank */ protected double widestRankValue; /** * The width of all the ranks */ protected double[] rankWidths; /* * The y co-ordinate 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; /** * Whether or not to pull together sections of layout into empty space */ protected boolean compactLayout = false; /** * A store of connections to the layer above for speed */ protected JGraphAbstractHierarchyCell[][] nextLayerConnectedCache; /** * A store of connections to the layer below for speed */ protected JGraphAbstractHierarchyCell[][] previousLayerConnectedCache; /** The logger for this class */ private static Logger logger = Logger .getLogger("com.jgraph.layout.hierarchical.JGraphCoordinateAssignment"); /** * Creates a JGraphCoordinateAssignment * * @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 JGraphCoordinateAssignment(double intraCellSpacing, double interRankCellSpacing, int orientation, boolean compactLayout, double initialX, double parallelEdgeSpacing) { this.intraCellSpacing = intraCellSpacing; this.interRankCellSpacing = interRankCellSpacing; this.orientation = orientation; this.compactLayout = compactLayout; this.initialX = initialX; this.parallelEdgeSpacing = parallelEdgeSpacing; setLoggerLevel(Level.OFF); } /** * A basic horizontal coordinate assignment algorithm * * @param facade * the facade describing the input graph * @param model * an internal model of the hierarchical layout * @return the updated hierarchy model */ public JGraphHierarchyModel run(JGraphFacade facade, JGraphHierarchyModel model) { currentXDelta = 0.0; initialise(model); initialCoords(facade, 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++) { JGraphHierarchyRank rank = (JGraphHierarchyRank) model.ranks .get(new Integer(j)); Iterator iter = rank.iterator(); while (iter.hasNext()) { JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) iter .next(); cell.setX(j, cell.getGeneralPurposeVariable(j)); } } bestXDelta = currentXDelta; } else { // Restore the best positions for (int j = 0; j < model.ranks.size(); j++) { JGraphHierarchyRank rank = (JGraphHierarchyRank) model.ranks .get(new Integer(j)); Iterator iter = rank.iterator(); while (iter.hasNext()) { JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) iter .next(); cell.setGeneralPurposeVariable(j, (int) cell .getX(j)); } } } currentXDelta = 0; } } if (compactLayout) { // Not yet working // compactLayout(model); } setCellLocations(facade, model); return model; } /** * Performs one median positioning sweep in both directions * * @param model * an internal model of the hierarchical layout */ private void minNode(JGraphHierarchyModel model) { // Queue all nodes LinkedList nodeList = new LinkedList(); // Need to be able to map from cell to cellWrapper Map map = new Hashtable(); Object[][] rank = new Object[model.maxRank + 1][]; for (int i = 0; i <= model.maxRank; i++) { JGraphHierarchyRank rankSet = (JGraphHierarchyRank) model.ranks .get(new Integer(i)); rank[i] = rankSet.toArray(); 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 JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) 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 = (WeightedCellSorter) nodeList .getFirst(); JGraphAbstractHierarchyCell 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 { JGraphAbstractHierarchyCell leftCell = (JGraphAbstractHierarchyCell) 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 { JGraphAbstractHierarchyCell rightCell = (JGraphAbstractHierarchyCell) 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++) { JGraphAbstractHierarchyCell connectedCell = (JGraphAbstractHierarchyCell) nextLayerConnectedCells[i]; WeightedCellSorter connectedCellWrapper = (WeightedCellSorter) 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++) { JGraphAbstractHierarchyCell connectedCell = (JGraphAbstractHierarchyCell) previousLayerConnectedCells[i]; WeightedCellSorter connectedCellWrapper = (WeightedCellSorter) 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, JGraphHierarchyModel 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, JGraphHierarchyModel model, int nextRankValue) { JGraphHierarchyRank rankSet = (JGraphHierarchyRank) model.ranks .get(new Integer(rankValue)); Object[] rank = rankSet.toArray(); // Form an array of the order in which the cell 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 cellMap = new Hashtable(rank.length); for (int i = 0; i < rank.length; i++) { JGraphAbstractHierarchyCell currentCell = (JGraphAbstractHierarchyCell) rank[i]; weightedValues[i] = new WeightedCellSorter(); weightedValues[i].cell = currentCell; weightedValues[i].rankIndex = i; cellMap.put(currentCell, weightedValues[i]); Collection nextLayerConnectedCells = null; if (nextRankValue < rankValue) { nextLayerConnectedCells = currentCell .getPreviousLayerConnectedCells(rankValue); } else { nextLayerConnectedCells = currentCell .getNextLayerConnectedCells(rankValue); } // Calcuate the weighing based on this node type and those this // node is connected to on the next layer weightedValues[i].weightedValue = calculatedWeightedValue( currentCell, nextLayerConnectedCells); } try { Arrays.sort(weightedValues); } catch (Exception e) { System.out.println(e.getMessage()); System.out.println(Arrays.toString(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; JGraphAbstractHierarchyCell 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 = (WeightedCellSorter) cellMap .get(rank[j]); if (weightedValue != null) { JGraphAbstractHierarchyCell 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 = (WeightedCellSorter) cellMap .get(rank[j]); if (weightedValue != null) { JGraphAbstractHierarchyCell 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, (int) 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( JGraphAbstractHierarchyCell currentCell, Collection collection) { int totalWeight = 0; Iterator iter = collection.iterator(); while (iter.hasNext()) { JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) 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] = ((JGraphAbstractHierarchyCell) 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 cached information for speed * * @param model * the model to cache */ private void initialise(JGraphHierarchyModel model) { // for (int i = 0; i < model.maxRank; i++) { // JGraphHierarchyRank rankSet = (JGraphHierarchyRank) model.ranks // .get(new Integer(i)); // rank[i] = rankSet.toArray(); // 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 // JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) // rank[i][j]; // WeightedCellSorter cellWrapper = new WeightedCellSorter(cell, i); // cellWrapper.rankIndex = j; // cellWrapper.visited = true; // nodeList.add(cellWrapper); // map.put(cell, cellWrapper); // } // } // // nextLayerConnectedCache } /** * 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(JGraphFacade facade, JGraphHierarchyModel 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 facade * the facade describing the input graph * @param model * an internal model of the hierarchical layout */ protected void rankCoordinates(int rankValue, JGraphFacade facade, JGraphHierarchyModel model) { JGraphHierarchyRank rank = (JGraphHierarchyRank) model.ranks .get(new Integer(rankValue)); // Pad out the initial cell spacing to give a better chance of a cell // not being restricted in one direction. double extraCellSpacing = (widestRankValue - rankWidths[rankValue]) / (rank.size() + 1); double localIntraCellSpacing = intraCellSpacing + extraCellSpacing; // Check this doesn't make the rank too wide, if it does, reduce it if (extraCellSpacing * (rank.size() + 1) + rankWidths[rankValue] > widestRankValue) { localIntraCellSpacing = intraCellSpacing; } double maxY = 0.0; double localX = initialX + extraCellSpacing; Iterator iter = rank.iterator(); // Store whether or not any of the cells' bounds were unavailable so // to only issue the warning once for all cells boolean boundsWarning = false; while (iter.hasNext()) { JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) iter .next(); if (cell.isVertex()) { JGraphHierarchyNode node = (JGraphHierarchyNode) cell; Rectangle2D bounds = facade.getBounds(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()) { JGraphHierarchyEdge edge = (JGraphHierarchyEdge) 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 += localIntraCellSpacing; } 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 facade * the facade describing the input graph * @param model * an internal model of the hierarchical layout */ protected void calculateWidestRank(JGraphFacade facade, JGraphHierarchyModel 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; JGraphHierarchyRank rank = (JGraphHierarchyRank) model.ranks .get(new Integer(rankValue)); double localX = initialX; Iterator iter = rank.iterator(); // Store whether or not any of the cells' bounds were unavailable so // to only issue the warning once for all cells boolean boundsWarning = false; while (iter.hasNext()) { JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) iter .next(); if (cell.isVertex()) { JGraphHierarchyNode node = (JGraphHierarchyNode) cell; Rectangle2D bounds = facade.getBounds(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()) { JGraphHierarchyEdge edge = (JGraphHierarchyEdge) 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()) { JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) iter .next(); cell.setY(rankValue, y); } } } /** * Removes empty space between parts of the layout * * @param model */ // private void compactLayout(JGraphHierarchyModel model) { // // List of separate areas in layout // Set areas = new HashSet(); // for (int i = 0; i < model.ranks.size(); i++) { // JGraphHierarchyRank rank = (JGraphHierarchyRank) model.ranks // .get(new Integer(i)); // Iterator iter = rank.iterator(); // while (iter.hasNext()) { // JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) iter // .next(); // double positionX = 0; // if (cell.isVertex()) { // JGraphHierarchyNode node = (JGraphHierarchyNode) cell; // positionX = node.x[0] - node.width / 2; // Rectangle2D area = new Rectangle2D.Double(positionX-intraCellSpacing, 0, // node.width+(intraCellSpacing*2), 100000); // integrateNewArea(area, areas, cell); // } else if (cell.isEdge()) { // JGraphHierarchyEdge edge = (JGraphHierarchyEdge) cell; // // For parallel edges we need to seperate out the points a // // little // int numParallelEdges = edge.edges.size(); // int parallelEdgeBuffer = (int)parallelEdgeSpacing * numParallelEdges/2; // for (int j = edge.x.length - 1; j >= 0; j--) { // positionX = edge.x[j]; // if (orientation == SwingConstants.EAST // || orientation == SwingConstants.WEST) { // positionX = edge.y[j]; // } // Rectangle2D area = new Rectangle2D.Double(positionX-intraCellSpacing, 0, // parallelEdgeBuffer+(intraCellSpacing*2), 100000); // integrateNewArea(area, areas, cell); // } // } // } // } // // If there is more than one area need to compact sections // if (areas.size() > 1) { // Iterator iter = areas.iterator(); // while (iter.hasNext()) { // Rectangle2D area = (Rectangle2D)iter.next(); // } // } // } /** * Adds a new rectangle to any intersecting rectangles stored in areas. If * no intersection a new area is created from its values. * * @param area * @param areas */ // private void integrateNewArea(Rectangle2D area, Set areas, // JGraphAbstractHierarchyCell cell) { // Iterator iter = areas.iterator(); // // Whether or not a cached area is found that contains area // boolean areaFound = false; // while (iter.hasNext()) { // AreaSpatialCache cachedArea = (AreaSpatialCache)iter.next(); // if (cachedArea.intersects(area)) { // cachedArea.setRect(cachedArea.createUnion(area)); // if (areaFound == false) { // cachedArea.cells.add(cell); // areaFound = true; // } // } // } // if (areaFound == false) { // // Create new area to hold cell's area // AreaSpatialCache newArea = new AreaSpatialCache(); // newArea.setRect(area.getX(), 0, area.getWidth(), 100000); // areas.add(newArea); // newArea.cells.add(cell); // // } // // Check if any of the cached areas now overlap, if they do, merge them // Set removedAreas = new HashSet(); // while (iter.hasNext()) { // AreaSpatialCache cachedArea = (AreaSpatialCache)iter.next(); // // Skip area if already flagged for removal // if (!removedAreas.contains(cachedArea)) { // Iterator iter2 = areas.iterator(); // while (iter2.hasNext()) { // AreaSpatialCache cachedArea2 = (AreaSpatialCache) iter2.next(); // if (cachedArea.intersects(cachedArea2)) { // cachedArea.setRect(cachedArea.createUnion(cachedArea2)); // removedAreas.add(cachedArea2); // } // } // } // } // areas.removeAll(removedAreas); // } /** * Sets the cell locations in the facade to those stored after this layout * processing step has completed. * * @param facade * the facade describing the input graph * @param model * an internal model of the hierarchical layout */ private void setCellLocations(JGraphFacade facade, JGraphHierarchyModel model) { // Stores any translation needs to separate this context properly // from any last context double contextTranslation = 0.0; // Store all the vertices in case we need to translate them List vertices = new ArrayList(); // Run through the ranks twice, once for vertices, then for edges // The reason for this is if the vertices need to offset again from // the last context for (int cellType = 0; cellType < 2; cellType++) { for (int i = 0; i < model.ranks.size(); i++) { JGraphHierarchyRank rank = (JGraphHierarchyRank) model.ranks .get(new Integer(i)); Iterator iter = rank.iterator(); while (iter.hasNext()) { JGraphAbstractHierarchyCell cell = (JGraphAbstractHierarchyCell) iter .next(); if (cellType == 0 && cell.isVertex()) { JGraphHierarchyNode node = (JGraphHierarchyNode) cell; Object realCell = node.cell; vertices.add(realCell); double positionX = node.x[0] - node.width / 2; double positionY = node.y[0] - node.height / 2; if (orientation == SwingConstants.NORTH || orientation == SwingConstants.SOUTH) { facade.setLocation(realCell, positionX, positionY); } else { facade.setLocation(realCell, positionY, positionX); } // Stores the positive X limit of this graph context to // know where the next context can start limitX = Math.max(limitX, positionX + node.width); // It is possible that a rank sticks out further in the // -ve x direction than the widest rank. In this case // store a shift that is required to apply to all cells // after positioning is complete. if (positionX + 1 < initialX) { contextTranslation = initialX - positionX; } } else if (cellType == 1 && cell.isEdge()) { JGraphHierarchyEdge edge = (JGraphHierarchyEdge) cell; // For parallel edges we need to seperate out the points // a // little Iterator parallelEdges = edge.edges.iterator(); double offsetX = 0.0; // Only set the edge control points once if (edge.temp[0] != 101207) { while (parallelEdges.hasNext()) { Object realEdge = parallelEdges.next(); List oldPoints = facade.getPoints(realEdge); List newPoints = new ArrayList( (edge.x.length) + 2); newPoints.add(oldPoints.get(0)); if (edge.isReversed()) { // Reversed edges need the points inserted // in // reverse order for (int j = 0; j < edge.x.length; j++) { double positionX = edge.x[j] + offsetX + contextTranslation; if (orientation == SwingConstants.NORTH || orientation == SwingConstants.SOUTH) { newPoints.add(new Point2D.Double( positionX, edge.y[j])); } else { newPoints.add(new Point2D.Double( edge.y[j], positionX)); } limitX = Math.max(limitX, positionX); } processReversedEdge(edge, realEdge); } else { for (int j = edge.x.length - 1; j >= 0; j--) { double positionX = edge.x[j] + offsetX + contextTranslation; if (orientation == SwingConstants.NORTH || orientation == SwingConstants.SOUTH) { newPoints.add(new Point2D.Double( positionX, edge.y[j])); } else { newPoints.add(new Point2D.Double( edge.y[j], positionX)); } limitX = Math.max(limitX, positionX); } } newPoints.add(oldPoints .get(oldPoints.size() - 1)); facade.setPoints(realEdge, newPoints); facade.disableRouting(realEdge); // 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; } } edge.temp[0] = 101207; } } } } } // Move the context by the amount it's overlapping the last context if (contextTranslation >= 1.0) { if (orientation == SwingConstants.NORTH || orientation == SwingConstants.SOUTH) { facade.translateCells(vertices, contextTranslation, 0); } else if (orientation == SwingConstants.EAST || orientation == SwingConstants.WEST) { facade.translateCells(vertices, 0, contextTranslation); } } // Increase the limit of this context accordingly limitX += contextTranslation; } /** * Hook to add additional processing * * @param edge * The hierarchical model edge * @param realEdge * The real edge in the graph */ private void processReversedEdge(JGraphHierarchyEdge 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 static class WeightedCellSorter implements Comparable<WeightedCellSorter> { /** * 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 JGraphAbstractHierarchyCell cell = null; public WeightedCellSorter() { this(null, 0); } public WeightedCellSorter(JGraphAbstractHierarchyCell cell, int weightedValue) { this.cell = cell; this.weightedValue = weightedValue; } /** * comparator on the medianValue * * @param other * the object to be compared to * @return the standard return you would expect when comparing two * double */ public int compareTo(WeightedCellSorter other) { if (weightedValue > other.weightedValue) { return -1; } else if (weightedValue < other.weightedValue) { return 1; } else { return new Boolean(nudge).compareTo(other.nudge); } } @Override public boolean equals(Object obj) { if (obj instanceof WeightedCellSorter) { WeightedCellSorter other = (WeightedCellSorter)obj; return weightedValue == other.weightedValue && nudge == other.nudge; } return false; } @Override public String toString() { return Integer.toString(weightedValue); } } /** * 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 cells = new HashSet(); } /** * @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; } /** * @return Returns the compactLayout. */ public boolean isCompactLayout() { return compactLayout; } /** * @param compactLayout * The compactLayout to set. */ public void setCompactLayout(boolean compactLayout) { this.compactLayout = compactLayout; } /** * 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 } } }