| /** |
| * 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 <mxHierarchicalLayout>. |
| */ |
| 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 <mxConstants.DIRECTION_NORTH>. |
| */ |
| 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; |
| } |
| }; |