diff --git a/js/ui/mxgraph/src/js/layout/hierarchical/stage/mxCoordinateAssignment.js b/js/ui/mxgraph/src/js/layout/hierarchical/stage/mxCoordinateAssignment.js new file mode 100644 index 0000000..70c9bbd --- /dev/null +++ b/js/ui/mxgraph/src/js/layout/hierarchical/stage/mxCoordinateAssignment.js @@ -0,0 +1,1830 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxCoordinateAssignment + * + * 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. + * + * Constructor: mxCoordinateAssignment + * + * Creates a coordinate assignment. + * + * Arguments: + * + * intraCellSpacing - the minimum buffer between cells on the same rank + * interRankCellSpacing - the minimum distance between cells on adjacent ranks + * orientation - the position of the root node(s) relative to the graph + * initialX - the leftmost coordinate node placement starts at + */ +function mxCoordinateAssignment(layout, intraCellSpacing, interRankCellSpacing, + orientation, initialX, parallelEdgeSpacing) +{ + this.layout = layout; + this.intraCellSpacing = intraCellSpacing; + this.interRankCellSpacing = interRankCellSpacing; + this.orientation = orientation; + this.initialX = initialX; + this.parallelEdgeSpacing = parallelEdgeSpacing; +}; + +/** + * Extends mxHierarchicalLayoutStage. + */ +mxCoordinateAssignment.prototype = new mxHierarchicalLayoutStage(); +mxCoordinateAssignment.prototype.constructor = mxCoordinateAssignment; + +/** + * Variable: layout + * + * Reference to the enclosing . + */ +mxCoordinateAssignment.prototype.layout = null; + +/** + * Variable: intraCellSpacing + * + * The minimum buffer between cells on the same rank. Default is 30. + */ +mxCoordinateAssignment.prototype.intraCellSpacing = 30; + +/** + * Variable: interRankCellSpacing + * + * The minimum distance between cells on adjacent ranks. Default is 10. + */ +mxCoordinateAssignment.prototype.interRankCellSpacing = 100; + +/** + * Variable: parallelEdgeSpacing + * + * The distance between each parallel edge on each ranks for long edges. + * Default is 10. + */ +mxCoordinateAssignment.prototype.parallelEdgeSpacing = 10; + +/** + * Variable: maxIterations + * + * The number of heuristic iterations to run. Default is 8. + */ +mxCoordinateAssignment.prototype.maxIterations = 8; + +/** + * Variable: prefHozEdgeSep + * + * The preferred horizontal distance between edges exiting a vertex + */ +mxCoordinateAssignment.prototype.prefHozEdgeSep = 5; + +/** + * Variable: prefVertEdgeOff + * + * The preferred vertical offset between edges exiting a vertex + */ +mxCoordinateAssignment.prototype.prefVertEdgeOff = 2; + +/** + * Variable: minEdgeJetty + * + * The minimum distance for an edge jetty from a vertex + */ +mxCoordinateAssignment.prototype.minEdgeJetty = 12; + +/** + * Variable: channelBuffer + * + * The size of the vertical buffer in the center of inter-rank channels + * where edge control points should not be placed + */ +mxCoordinateAssignment.prototype.channelBuffer = 4; + +/** + * Variable: jettyPositions + * + * 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 + */ +mxCoordinateAssignment.prototype.jettyPositions = null; + +/** + * Variable: orientation + * + * The position of the root ( start ) node(s) relative to the rest of the + * laid out graph. Default is . + */ +mxCoordinateAssignment.prototype.orientation = mxConstants.DIRECTION_NORTH; + +/** + * Variable: initialX + * + * The minimum x position node placement starts at + */ +mxCoordinateAssignment.prototype.initialX = null; + +/** + * Variable: limitX + * + * The maximum x value this positioning lays up to + */ +mxCoordinateAssignment.prototype.limitX = null; + +/** + * Variable: currentXDelta + * + * The sum of x-displacements for the current iteration + */ +mxCoordinateAssignment.prototype.currentXDelta = null; + +/** + * Variable: widestRank + * + * The rank that has the widest x position + */ +mxCoordinateAssignment.prototype.widestRank = null; + +/** + * Variable: rankTopY + * + * Internal cache of top-most values of Y for each rank + */ +mxCoordinateAssignment.prototype.rankTopY = null; + +/** + * Variable: rankBottomY + * + * Internal cache of bottom-most value of Y for each rank + */ +mxCoordinateAssignment.prototype.rankBottomY = null; + +/** + * Variable: widestRankValue + * + * The X-coordinate of the edge of the widest rank + */ +mxCoordinateAssignment.prototype.widestRankValue = null; + +/** + * Variable: rankWidths + * + * The width of all the ranks + */ +mxCoordinateAssignment.prototype.rankWidths = null; + +/** + * Variable: rankY + * + * The Y-coordinate of all the ranks + */ +mxCoordinateAssignment.prototype.rankY = null; + +/** + * Variable: fineTuning + * + * Whether or not to perform local optimisations and iterate multiple times + * through the algorithm. Default is true. + */ +mxCoordinateAssignment.prototype.fineTuning = true; + +/** + * Variable: nextLayerConnectedCache + * + * A store of connections to the layer above for speed + */ +mxCoordinateAssignment.prototype.nextLayerConnectedCache = null; + +/** + * Variable: previousLayerConnectedCache + * + * A store of connections to the layer below for speed + */ +mxCoordinateAssignment.prototype.previousLayerConnectedCache = null; + +/** + * Variable: groupPadding + * + * Padding added to resized parents + */ +mxCoordinateAssignment.prototype.groupPadding = 10; + +/** + * Utility method to display current positions + */ +mxCoordinateAssignment.prototype.printStatus = function() +{ + var model = this.layout.getModel(); + mxLog.show(); + + mxLog.writeln('======Coord assignment debug======='); + + for (var j = 0; j < model.ranks.length; j++) + { + mxLog.write('Rank ', j, ' : ' ); + var rank = model.ranks[j]; + + for (var k = 0; k < rank.length; k++) + { + var cell = rank[k]; + + mxLog.write(cell.getGeneralPurposeVariable(j), ' '); + } + mxLog.writeln(); + } + + mxLog.writeln('===================================='); +}; + +/** + * Function: execute + * + * A basic horizontal coordinate assignment algorithm + */ +mxCoordinateAssignment.prototype.execute = function(parent) +{ + this.jettyPositions = Object(); + var model = this.layout.getModel(); + this.currentXDelta = 0.0; + + this.initialCoords(this.layout.getGraph(), model); + +// this.printStatus(); + + if (this.fineTuning) + { + this.minNode(model); + } + + var bestXDelta = 100000000.0; + + if (this.fineTuning) + { + for (var i = 0; i < this.maxIterations; i++) + { +// this.printStatus(); + + // Median Heuristic + if (i != 0) + { + this.medianPos(i, model); + this.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 (this.currentXDelta < bestXDelta) + { + for (var j = 0; j < model.ranks.length; j++) + { + var rank = model.ranks[j]; + + for (var k = 0; k < rank.length; k++) + { + var cell = rank[k]; + cell.setX(j, cell.getGeneralPurposeVariable(j)); + } + } + + bestXDelta = this.currentXDelta; + } + else + { + // Restore the best positions + for (var j = 0; j < model.ranks.length; j++) + { + var rank = model.ranks[j]; + + for (var k = 0; k < rank.length; k++) + { + var cell = rank[k]; + cell.setGeneralPurposeVariable(j, cell.getX(j)); + } + } + } + + this.minPath(this.layout.getGraph(), model); + + this.currentXDelta = 0; + } + } + + this.setCellLocations(this.layout.getGraph(), model); +}; + +/** + * Function: minNode + * + * Performs one median positioning sweep in both directions + */ +mxCoordinateAssignment.prototype.minNode = function(model) +{ + // Queue all nodes + var nodeList = []; + + // Need to be able to map from cell to cellWrapper + var map = new mxDictionary(); + var rank = []; + + for (var i = 0; i <= model.maxRank; i++) + { + rank[i] = model.ranks[i]; + + for (var 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 + var node = rank[i][j]; + var nodeWrapper = new WeightedCellSorter(node, i); + nodeWrapper.rankIndex = j; + nodeWrapper.visited = true; + nodeList.push(nodeWrapper); + + map.put(node, nodeWrapper); + } + } + + // Set a limit of the maximum number of times we will access the queue + // in case a loop appears + var maxTries = nodeList.length * 10; + var count = 0; + + // Don't move cell within this value of their median + var tolerance = 1; + + while (nodeList.length > 0 && count <= maxTries) + { + var cellWrapper = nodeList.shift(); + var cell = cellWrapper.cell; + + var rankValue = cellWrapper.weightedValue; + var rankIndex = parseInt(cellWrapper.rankIndex); + + var nextLayerConnectedCells = cell.getNextLayerConnectedCells(rankValue); + var previousLayerConnectedCells = cell.getPreviousLayerConnectedCells(rankValue); + + var numNextLayerConnected = nextLayerConnectedCells.length; + var numPreviousLayerConnected = previousLayerConnectedCells.length; + + var medianNextLevel = this.medianXValue(nextLayerConnectedCells, + rankValue + 1); + var medianPreviousLevel = this.medianXValue(previousLayerConnectedCells, + rankValue - 1); + + var numConnectedNeighbours = numNextLayerConnected + + numPreviousLayerConnected; + var currentPosition = cell.getGeneralPurposeVariable(rankValue); + var cellMedian = currentPosition; + + if (numConnectedNeighbours > 0) + { + cellMedian = (medianNextLevel * numNextLayerConnected + medianPreviousLevel + * numPreviousLayerConnected) + / numConnectedNeighbours; + } + + // Flag storing whether or not position has changed + var positionChanged = false; + + if (cellMedian < currentPosition - tolerance) + { + if (rankIndex == 0) + { + cell.setGeneralPurposeVariable(rankValue, cellMedian); + positionChanged = true; + } + else + { + var leftCell = rank[rankValue][rankIndex - 1]; + var leftLimit = leftCell + .getGeneralPurposeVariable(rankValue); + leftLimit = leftLimit + leftCell.width / 2 + + this.intraCellSpacing + cell.width / 2; + + if (leftLimit < cellMedian) + { + cell.setGeneralPurposeVariable(rankValue, cellMedian); + positionChanged = true; + } + else if (leftLimit < cell + .getGeneralPurposeVariable(rankValue) + - tolerance) + { + cell.setGeneralPurposeVariable(rankValue, leftLimit); + positionChanged = true; + } + } + } + else if (cellMedian > currentPosition + tolerance) + { + var rankSize = rank[rankValue].length; + + if (rankIndex == rankSize - 1) + { + cell.setGeneralPurposeVariable(rankValue, cellMedian); + positionChanged = true; + } + else + { + var rightCell = rank[rankValue][rankIndex + 1]; + var rightLimit = rightCell + .getGeneralPurposeVariable(rankValue); + rightLimit = rightLimit - rightCell.width / 2 + - this.intraCellSpacing - cell.width / 2; + + if (rightLimit > cellMedian) + { + cell.setGeneralPurposeVariable(rankValue, 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 (var i = 0; i < nextLayerConnectedCells.length; i++) + { + var connectedCell = nextLayerConnectedCells[i]; + var connectedCellWrapper = map.get(connectedCell); + + if (connectedCellWrapper != null) + { + if (connectedCellWrapper.visited == false) + { + connectedCellWrapper.visited = true; + nodeList.push(connectedCellWrapper); + } + } + } + + // Add connected nodes to map and list + for (var i = 0; i < previousLayerConnectedCells.length; i++) + { + var connectedCell = previousLayerConnectedCells[i]; + var connectedCellWrapper = map.get(connectedCell); + + if (connectedCellWrapper != null) + { + if (connectedCellWrapper.visited == false) + { + connectedCellWrapper.visited = true; + nodeList.push(connectedCellWrapper); + } + } + } + } + + cellWrapper.visited = false; + count++; + } +}; + +/** + * Function: medianPos + * + * Performs one median positioning sweep in one direction + * + * Parameters: + * + * i - the iteration of the whole process + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.medianPos = function(i, model) +{ + // Reverse sweep direction each time through this method + var downwardSweep = (i % 2 == 0); + + if (downwardSweep) + { + for (var j = model.maxRank; j > 0; j--) + { + this.rankMedianPosition(j - 1, model, j); + } + } + else + { + for (var j = 0; j < model.maxRank - 1; j++) + { + this.rankMedianPosition(j + 1, model, j); + } + } +}; + +/** + * Function: rankMedianPosition + * + * Performs median minimisation over one rank. + * + * Parameters: + * + * rankValue - the layer number of this rank + * model - an internal model of the hierarchical layout + * nextRankValue - the layer number whose connected cels are to be laid out + * relative to + */ +mxCoordinateAssignment.prototype.rankMedianPosition = function(rankValue, model, nextRankValue) +{ + var rank = model.ranks[rankValue]; + + // 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 traveling up or down the hierarchy. + var weightedValues = []; + var cellMap = new Object(); + + for (var i = 0; i < rank.length; i++) + { + var currentCell = rank[i]; + weightedValues[i] = new WeightedCellSorter(); + weightedValues[i].cell = currentCell; + weightedValues[i].rankIndex = i; + cellMap[currentCell.id] = weightedValues[i]; + var 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 = this.calculatedWeightedValue( + currentCell, nextLayerConnectedCells); + } + + weightedValues.sort(WeightedCellSorter.prototype.compare); + + // Set the new position of each node within the rank using + // its temp variable + + for (var i = 0; i < weightedValues.length; i++) + { + var numConnectionsNextLevel = 0; + var cell = weightedValues[i].cell; + var nextLayerConnectedCells = null; + var medianNextLevel = 0; + + if (nextRankValue < rankValue) + { + nextLayerConnectedCells = cell.getPreviousLayerConnectedCells( + rankValue).slice(); + } + else + { + nextLayerConnectedCells = cell.getNextLayerConnectedCells( + rankValue).slice(); + } + + if (nextLayerConnectedCells != null) + { + numConnectionsNextLevel = nextLayerConnectedCells.length; + + if (numConnectionsNextLevel > 0) + { + medianNextLevel = this.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); + } + } + + var leftBuffer = 0.0; + var leftLimit = -100000000.0; + + for (var j = weightedValues[i].rankIndex - 1; j >= 0;) + { + var weightedValue = cellMap[rank[j].id]; + + if (weightedValue != null) + { + var 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 + + this.intraCellSpacing + + leftBuffer + cell.width / 2.0; + j = -1; + } + else + { + leftBuffer += leftCell.width + this.intraCellSpacing; + j--; + } + } + } + + var rightBuffer = 0.0; + var rightLimit = 100000000.0; + + for (var j = weightedValues[i].rankIndex + 1; j < weightedValues.length;) + { + var weightedValue = cellMap[rank[j].id]; + + if (weightedValue != null) + { + var 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 + - this.intraCellSpacing + - rightBuffer - cell.width / 2.0; + j = weightedValues.length; + } + else + { + rightBuffer += rightCell.width + this.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, leftLimit); + this.currentXDelta += leftLimit - medianNextLevel; + } + else if (medianNextLevel > rightLimit) + { + // Couldn't place at median value, place as close to that + // value as possible + cell.setGeneralPurposeVariable(rankValue, rightLimit); + this.currentXDelta += medianNextLevel - rightLimit; + } + + weightedValues[i].visited = true; + } +}; + +/** + * Function: calculatedWeightedValue + * + * 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 + * + * Parameters: + * + * currentCell - the cell whose weight is to be calculated + * collection - the cells the specified cell is connected to + */ +mxCoordinateAssignment.prototype.calculatedWeightedValue = function(currentCell, collection) +{ + var totalWeight = 0; + + for (var i = 0; i < collection.length; i++) + { + var cell = collection[i]; + + if (currentCell.isVertex() && cell.isVertex()) + { + totalWeight++; + } + else if (currentCell.isEdge() && cell.isEdge()) + { + totalWeight += 8; + } + else + { + totalWeight += 2; + } + } + + return totalWeight; +}; + +/** + * Function: medianXValue + * + * Calculates the median position of the connected cell on the specified + * rank + * + * Parameters: + * + * connectedCells - the cells the candidate connects to on this level + * rankValue - the layer number of this rank + */ +mxCoordinateAssignment.prototype.medianXValue = function(connectedCells, rankValue) +{ + if (connectedCells.length == 0) + { + return 0; + } + + var medianValues = []; + + for (var i = 0; i < connectedCells.length; i++) + { + medianValues[i] = connectedCells[i].getGeneralPurposeVariable(rankValue); + } + + medianValues.sort(function(a,b){return a - b;}); + + if (connectedCells.length % 2 == 1) + { + // For odd numbers of adjacent vertices return the median + return medianValues[Math.floor(connectedCells.length / 2)]; + } + else + { + var medianPoint = connectedCells.length / 2; + var leftMedian = medianValues[medianPoint - 1]; + var rightMedian = medianValues[medianPoint]; + + return ((leftMedian + rightMedian) / 2); + } +}; + +/** + * Function: initialCoords + * + * 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. + * + * Parameters: + * + * facade - the facade describing the input graph + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.initialCoords = function(facade, model) +{ + this.calculateWidestRank(facade, model); + + // Sweep up and down from the widest rank + for (var i = this.widestRank; i >= 0; i--) + { + if (i < model.maxRank) + { + this.rankCoordinates(i, facade, model); + } + } + + for (var i = this.widestRank+1; i <= model.maxRank; i++) + { + if (i > 0) + { + this.rankCoordinates(i, facade, model); + } + } +}; + +/** + * Function: rankCoordinates + * + * 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. + * + * Parameters: + * + * rankValue - the current rank being processed + * graph - the facade describing the input graph + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.rankCoordinates = function(rankValue, graph, model) +{ + var rank = model.ranks[rankValue]; + var maxY = 0.0; + var localX = this.initialX + (this.widestRankValue - this.rankWidths[rankValue]) + / 2; + + // Store whether or not any of the cells' bounds were unavailable so + // to only issue the warning once for all cells + var boundsWarning = false; + + for (var i = 0; i < rank.length; i++) + { + var node = rank[i]; + + if (node.isVertex()) + { + var bounds = this.layout.getVertexBounds(node.cell); + + if (bounds != null) + { + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + node.width = bounds.width; + node.height = bounds.height; + } + else + { + node.width = bounds.height; + node.height = bounds.width; + } + } + else + { + boundsWarning = true; + } + + maxY = Math.max(maxY, node.height); + } + else if (node.isEdge()) + { + // The width is the number of additional parallel edges + // time the parallel edge spacing + var numEdges = 1; + + if (node.edges != null) + { + numEdges = node.edges.length; + } + else + { + mxLog.warn('edge.edges is null'); + } + + node.width = (numEdges - 1) * this.parallelEdgeSpacing; + } + + // Set the initial x-value as being the best result so far + localX += node.width / 2.0; + node.setX(rankValue, localX); + node.setGeneralPurposeVariable(rankValue, localX); + localX += node.width / 2.0; + localX += this.intraCellSpacing; + } + + if (boundsWarning == true) + { + mxLog.warn('At least one cell has no bounds'); + } +}; + +/** + * Function: calculateWidestRank + * + * Calculates the width rank in the hierarchy. Also set the y value of each + * rank whilst performing the calculation + * + * Parameters: + * + * graph - the facade describing the input graph + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.calculateWidestRank = function(graph, model) +{ + // Starting y co-ordinate + var y = -this.interRankCellSpacing; + + // Track the widest cell on the last rank since the y + // difference depends on it + var lastRankMaxCellHeight = 0.0; + this.rankWidths = []; + this.rankY = []; + + for (var rankValue = model.maxRank; rankValue >= 0; rankValue--) + { + // Keep track of the widest cell on this rank + var maxCellHeight = 0.0; + var rank = model.ranks[rankValue]; + var localX = this.initialX; + + // Store whether or not any of the cells' bounds were unavailable so + // to only issue the warning once for all cells + var boundsWarning = false; + + for (var i = 0; i < rank.length; i++) + { + var node = rank[i]; + + if (node.isVertex()) + { + var bounds = this.layout.getVertexBounds(node.cell); + + if (bounds != null) + { + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + node.width = bounds.width; + node.height = bounds.height; + } + else + { + node.width = bounds.height; + node.height = bounds.width; + } + } + else + { + boundsWarning = true; + } + + maxCellHeight = Math.max(maxCellHeight, node.height); + } + else if (node.isEdge()) + { + // The width is the number of additional parallel edges + // time the parallel edge spacing + var numEdges = 1; + + if (node.edges != null) + { + numEdges = node.edges.length; + } + else + { + mxLog.warn('edge.edges is null'); + } + + node.width = (numEdges - 1) * this.parallelEdgeSpacing; + } + + // Set the initial x-value as being the best result so far + localX += node.width / 2.0; + node.setX(rankValue, localX); + node.setGeneralPurposeVariable(rankValue, localX); + localX += node.width / 2.0; + localX += this.intraCellSpacing; + + if (localX > this.widestRankValue) + { + this.widestRankValue = localX; + this.widestRank = rankValue; + } + + this.rankWidths[rankValue] = localX; + } + + if (boundsWarning == true) + { + mxLog.warn('At least one cell has no bounds'); + } + + this.rankY[rankValue] = y; + var distanceToNextRank = maxCellHeight / 2.0 + + lastRankMaxCellHeight / 2.0 + this.interRankCellSpacing; + lastRankMaxCellHeight = maxCellHeight; + + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_WEST) + { + y += distanceToNextRank; + } + else + { + y -= distanceToNextRank; + } + + for (var i = 0; i < rank.length; i++) + { + var cell = rank[i]; + cell.setY(rankValue, y); + } + } +}; + +/** + * Function: minPath + * + * Straightens out chains of virtual nodes where possibleacade to those stored after this layout + * processing step has completed. + * + * Parameters: + * + * graph - the facade describing the input graph + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.minPath = function(graph, 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 + var edges = model.edgeMapper.getValues(); + + for (var j = 0; j < edges.length; j++) + { + var cell = edges[j]; + + if (cell.maxRank - cell.minRank - 1 < 1) + { + continue; + } + + // At least two virtual nodes in the edge + // Check first whether the edge is already straight + var referenceX = cell + .getGeneralPurposeVariable(cell.minRank + 1); + var edgeStraight = true; + var refSegCount = 0; + + for (var i = cell.minRank + 2; i < cell.maxRank; i++) + { + var x = cell.getGeneralPurposeVariable(i); + + if (referenceX != x) + { + edgeStraight = false; + referenceX = x; + } + else + { + refSegCount++; + } + } + + if (!edgeStraight) + { + var upSegCount = 0; + var downSegCount = 0; + var upXPositions = []; + var downXPositions = []; + + var currentX = cell.getGeneralPurposeVariable(cell.minRank + 1); + + for (var 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. + var nextX = cell.getX(i + 1); + + if (currentX == nextX) + { + upXPositions[i - cell.minRank - 1] = currentX; + upSegCount++; + } + else if (this.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(i); + + for (var 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. + var nextX = cell.getX(i - 1); + + if (currentX == nextX) + { + downXPositions[i - cell.minRank - 2] = currentX; + downSegCount++; + } + else if (this.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) + { + if (downSegCount >= upSegCount) + { + // Apply down calculation values + for (var i = cell.maxRank - 2; i > cell.minRank; i--) + { + cell.setX(i, downXPositions[i - cell.minRank - 1]); + } + } + else if (upSegCount > downSegCount) + { + // Apply up calculation values + for (var i = cell.minRank + 2; i < cell.maxRank; i++) + { + cell.setX(i, 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. + } + } + } + } +}; + +/** + * Function: repositionValid + * + * Determines whether or not a node may be moved to the specified x + * position on the specified rank + * + * Parameters: + * + * model - the layout model + * cell - the cell being analysed + * rank - the layer of the cell + * position - the x position being sought + */ +mxCoordinateAssignment.prototype.repositionValid = function(model, cell, rank, position) +{ + var rankArray = model.ranks[rank]; + var rankIndex = -1; + + for (var i = 0; i < rankArray.length; i++) + { + if (cell == rankArray[i]) + { + rankIndex = i; + break; + } + } + + if (rankIndex < 0) + { + return false; + } + + var 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; + } + + var leftCell = rankArray[rankIndex - 1]; + var leftLimit = leftCell.getGeneralPurposeVariable(rank); + leftLimit = leftLimit + leftCell.width / 2 + + this.intraCellSpacing + 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; + } + + var rightCell = rankArray[rankIndex + 1]; + var rightLimit = rightCell.getGeneralPurposeVariable(rank); + rightLimit = rightLimit - rightCell.width / 2 + - this.intraCellSpacing - cell.width / 2; + + if (rightLimit >= position) + { + return true; + } + else + { + return false; + } + } + + return true; +}; + +/** + * Function: setCellLocations + * + * Sets the cell locations in the facade to those stored after this layout + * processing step has completed. + * + * Parameters: + * + * graph - the input graph + * model - the layout model + */ +mxCoordinateAssignment.prototype.setCellLocations = function(graph, model) +{ + this.rankTopY = []; + this.rankBottomY = []; + + for (var i = 0; i < model.ranks.length; i++) + { + this.rankTopY[i] = Number.MAX_VALUE; + this.rankBottomY[i] = -Number.MAX_VALUE; + } + + var vertices = model.vertexMapper.getValues(); + + // 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 (var i = 0; i < vertices.length; i++) + { + this.setVertexLocation(vertices[i]); + } + + // Post process edge styles. Needs the vertex locations set for initial + // values of the top and bottoms of each rank + if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.ORTHOGONAL + || this.layout.edgeStyle == mxHierarchicalEdgeStyle.POLYLINE + || this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE) + { + this.localEdgeProcessing(model); + } + + var edges = model.edgeMapper.getValues(); + + for (var i = 0; i < edges.length; i++) + { + this.setEdgePosition(edges[i]); + } +}; + +/** + * Function: localEdgeProcessing + * + * Separates the x position of edges as they connect to vertices + * + * Parameters: + * + * model - the layout model + */ +mxCoordinateAssignment.prototype.localEdgeProcessing = function(model) +{ + // Iterate through each vertex, look at the edges connected in + // both directions. + for (var rankIndex = 0; rankIndex < model.ranks.length; rankIndex++) + { + var rank = model.ranks[rankIndex]; + + for (var cellIndex = 0; cellIndex < rank.length; cellIndex++) + { + var cell = rank[cellIndex]; + + if (cell.isVertex()) + { + var currentCells = cell.getPreviousLayerConnectedCells(rankIndex); + + var currentRank = rankIndex - 1; + + // Two loops, last connected cells, and next + for (var k = 0; k < 2; k++) + { + if (currentRank > -1 + && currentRank < model.ranks.length + && currentCells != null + && currentCells.length > 0) + { + var sortedCells = []; + + for (var j = 0; j < currentCells.length; j++) + { + var sorter = new WeightedCellSorter( + currentCells[j], currentCells[j].getX(currentRank)); + sortedCells.push(sorter); + } + + sortedCells.sort(WeightedCellSorter.prototype.compare); + + var leftLimit = cell.x[0] - cell.width / 2; + var rightLimit = leftLimit + cell.width; + + // Connected edge count starts at 1 to allow for buffer + // with edge of vertex + var connectedEdgeCount = 0; + var connectedEdgeGroupCount = 0; + var connectedEdges = []; + // Calculate width requirements for all connected edges + for (var j = 0; j < sortedCells.length; j++) + { + var innerCell = sortedCells[j].cell; + var connections; + + if (innerCell.isVertex()) + { + // Get the connecting edge + if (k == 0) + { + connections = cell.connectsAsSource; + + } + else + { + connections = cell.connectsAsTarget; + } + + for (var connIndex = 0; connIndex < connections.length; connIndex++) + { + if (connections[connIndex].source == innerCell + || connections[connIndex].target == innerCell) + { + connectedEdgeCount += connections[connIndex].edges + .length; + connectedEdgeGroupCount++; + + connectedEdges.push(connections[connIndex]); + } + } + } + else + { + connectedEdgeCount += innerCell.edges.length; + connectedEdgeGroupCount++; + connectedEdges.push(innerCell); + } + } + + var requiredWidth = (connectedEdgeCount + 1) + * this.prefHozEdgeSep; + + // Add a buffer on the edges of the vertex if the edge count allows + if (cell.width > requiredWidth + + (2 * this.prefHozEdgeSep)) + { + leftLimit += this.prefHozEdgeSep; + rightLimit -= this.prefHozEdgeSep; + } + + var availableWidth = rightLimit - leftLimit; + var edgeSpacing = availableWidth / connectedEdgeCount; + + var currentX = leftLimit + edgeSpacing / 2.0; + var currentYOffset = this.minEdgeJetty - this.prefVertEdgeOff; + var maxYOffset = 0; + + for (var j = 0; j < connectedEdges.length; j++) + { + var numActualEdges = connectedEdges[j].edges + .length; + var pos = this.jettyPositions[connectedEdges[j].ids[0]]; + + if (pos == null) + { + pos = []; + this.jettyPositions[connectedEdges[j].ids[0]] = pos; + } + + if (j < connectedEdgeCount / 2) + { + currentYOffset += this.prefVertEdgeOff; + } + else if (j > connectedEdgeCount / 2) + { + currentYOffset -= this.prefVertEdgeOff; + } + // Ignore the case if equals, this means the second of 2 + // jettys with the same y (even number of edges) + + for (var 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(rankIndex); + + currentRank = rankIndex + 1; + } + } + } + } +}; + +/** + * Function: setEdgePosition + * + * Fixes the control points + */ +mxCoordinateAssignment.prototype.setEdgePosition = function(cell) +{ + // For parallel edges we need to seperate out the points a + // little + var offsetX = 0; + // Only set the edge control points once + + if (cell.temp[0] != 101207) + { + var maxRank = cell.maxRank; + var minRank = cell.minRank; + + if (maxRank == minRank) + { + maxRank = cell.source.maxRank; + minRank = cell.target.minRank; + } + + var parallelEdgeCount = 0; + var jettys = this.jettyPositions[cell.ids[0]]; + + var source = cell.isReversed ? cell.target.cell : cell.source.cell; + var graph = this.layout.graph; + var layoutReversed = this.orientation == mxConstants.DIRECTION_EAST + || this.orientation == mxConstants.DIRECTION_SOUTH; + + for (var i = 0; i < cell.edges.length; i++) + { + var realEdge = cell.edges[i]; + var realSource = this.layout.getVisibleTerminal(realEdge, true); + + //List oldPoints = graph.getPoints(realEdge); + var newPoints = []; + + // 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 + var reversed = cell.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) + { + var arrayOffset = reversed ? 2 : 0; + var y = reversed ? + (layoutReversed ? this.rankBottomY[minRank] : this.rankTopY[minRank]) : + (layoutReversed ? this.rankTopY[maxRank] : this.rankBottomY[maxRank]); + var jetty = jettys[parallelEdgeCount * 4 + 1 + arrayOffset]; + + if (reversed != layoutReversed) + { + jetty = -jetty; + } + + y += jetty; + var x = jettys[parallelEdgeCount * 4 + arrayOffset]; + + var modelSource = graph.model.getTerminal(realEdge, true); + + if (this.layout.isPort(modelSource) && graph.model.getParent(modelSource) == realSource) + { + var state = graph.view.getState(modelSource); + + if (state != null) + { + x = state.x; + } + else + { + x = realSource.geometry.x + cell.source.width * modelSource.geometry.x; + } + } + + if (this.orientation == mxConstants.DIRECTION_NORTH + || this.orientation == mxConstants.DIRECTION_SOUTH) + { + newPoints.push(new mxPoint(x, y)); + + if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE) + { + newPoints.push(new mxPoint(x, y + jetty)); + } + } + else + { + newPoints.push(new mxPoint(y, x)); + + if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE) + { + newPoints.push(new mxPoint(y + jetty, x)); + } + } + } + + // Declare variables to define loop through edge points and + // change direction if edge is reversed + + var loopStart = cell.x.length - 1; + var loopLimit = -1; + var loopDelta = -1; + var currentRank = cell.maxRank - 1; + + if (reversed) + { + loopStart = 0; + loopLimit = cell.x.length; + loopDelta = 1; + currentRank = cell.minRank + 1; + } + // Reversed edges need the points inserted in + // reverse order + for (var j = loopStart; (cell.maxRank != cell.minRank) && j != loopLimit; j += loopDelta) + { + // The horizontal position in a vertical layout + var positionX = cell.x[j] + offsetX; + + // Work out the vertical positions in a vertical layout + // in the edge buffer channels above and below this rank + var topChannelY = (this.rankTopY[currentRank] + this.rankBottomY[currentRank + 1]) / 2.0; + var bottomChannelY = (this.rankTopY[currentRank - 1] + this.rankBottomY[currentRank]) / 2.0; + + if (reversed) + { + var tmp = topChannelY; + topChannelY = bottomChannelY; + bottomChannelY = tmp; + } + + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + newPoints.push(new mxPoint(positionX, topChannelY)); + newPoints.push(new mxPoint(positionX, bottomChannelY)); + } + else + { + newPoints.push(new mxPoint(topChannelY, positionX)); + newPoints.push(new mxPoint(bottomChannelY, positionX)); + } + + this.limitX = Math.max(this.limitX, positionX); + currentRank += loopDelta; + } + + // Second jetty of edge + if (jettys != null) + { + var arrayOffset = reversed ? 2 : 0; + var rankY = reversed ? + (layoutReversed ? this.rankTopY[maxRank] : this.rankBottomY[maxRank]) : + (layoutReversed ? this.rankBottomY[minRank] : this.rankTopY[minRank]); + var jetty = jettys[parallelEdgeCount * 4 + 3 - arrayOffset]; + + if (reversed != layoutReversed) + { + jetty = -jetty; + } + var y = rankY - jetty; + var x = jettys[parallelEdgeCount * 4 + 2 - arrayOffset]; + + var modelTarget = graph.model.getTerminal(realEdge, false); + var realTarget = this.layout.getVisibleTerminal(realEdge, false); + + if (this.layout.isPort(modelTarget) && graph.model.getParent(modelTarget) == realTarget) + { + var state = graph.view.getState(modelTarget); + + if (state != null) + { + x = state.x; + } + else + { + x = realTarget.geometry.x + cell.target.width * modelTarget.geometry.x; + } + } + + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE) + { + newPoints.push(new mxPoint(x, y - jetty)); + } + + newPoints.push(new mxPoint(x, y)); + } + else + { + if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE) + { + newPoints.push(new mxPoint(y - jetty, x)); + } + + newPoints.push(new mxPoint(y, x)); + } + } + + if (cell.isReversed) + { + this.processReversedEdge(cell, realEdge); + } + + this.layout.setEdgePoints(realEdge, newPoints); + + // Increase offset so next edge is drawn next to + // this one + if (offsetX == 0.0) + { + offsetX = this.parallelEdgeSpacing; + } + else if (offsetX > 0) + { + offsetX = -offsetX; + } + else + { + offsetX = -offsetX + this.parallelEdgeSpacing; + } + + parallelEdgeCount++; + } + + cell.temp[0] = 101207; + } +}; + + +/** + * Function: setVertexLocation + * + * Fixes the position of the specified vertex. + * + * Parameters: + * + * cell - the vertex to position + */ +mxCoordinateAssignment.prototype.setVertexLocation = function(cell) +{ + var realCell = cell.cell; + var positionX = cell.x[0] - cell.width / 2; + var positionY = cell.y[0] - cell.height / 2; + + this.rankTopY[cell.minRank] = Math.min(this.rankTopY[cell.minRank], positionY); + this.rankBottomY[cell.minRank] = Math.max(this.rankBottomY[cell.minRank], + positionY + cell.height); + + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + this.layout.setVertexLocation(realCell, positionX, positionY); + } + else + { + this.layout.setVertexLocation(realCell, positionY, positionX); + } + + this.limitX = Math.max(this.limitX, positionX + cell.width); +}; + +/** + * Function: processReversedEdge + * + * Hook to add additional processing + * + * Parameters: + * + * edge - the hierarchical model edge + * realEdge - the real edge in the graph + */ +mxCoordinateAssignment.prototype.processReversedEdge = function(graph, model) +{ + // hook for subclassers +}; + +/** + * Class: WeightedCellSorter + * + * 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)) + * + * Constructor: WeightedCellSorter + * + * Constructs a new weighted cell sorted for the given cell and weight. + */ +function WeightedCellSorter(cell, weightedValue) +{ + this.cell = cell; + this.weightedValue = weightedValue; +}; + +/** + * Variable: weightedValue + * + * The weighted value of the cell stored. + */ +WeightedCellSorter.prototype.weightedValue = 0; + +/** + * Variable: nudge + * + * Whether or not to flip equal weight values. + */ +WeightedCellSorter.prototype.nudge = false; + +/** + * Variable: visited + * + * Whether or not this cell has been visited in the current assignment. + */ +WeightedCellSorter.prototype.visited = false; + +/** + * Variable: rankIndex + * + * The index this cell is in the model rank. + */ +WeightedCellSorter.prototype.rankIndex = null; + +/** + * Variable: cell + * + * The cell whose median value is being calculated. + */ +WeightedCellSorter.prototype.cell = null; + +/** + * Function: compare + * + * Compares two WeightedCellSorters. + */ +WeightedCellSorter.prototype.compare = function(a, b) +{ + if (a != null && b != null) + { + if (b.weightedValue > a.weightedValue) + { + return -1; + } + else if (b.weightedValue < a.weightedValue) + { + return 1; + } + else + { + if (b.nudge) + { + return -1; + } + else + { + return 1; + } + } + } + else + { + return 0; + } +};