blob: 139079d9209669293b4ba0acb22f61052ac404db [file] [log] [blame]
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxMedianHybridCrossingReduction
*
* 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.
*
* Constructor: mxMedianHybridCrossingReduction
*
* 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 mxMedianHybridCrossingReduction(layout)
{
this.layout = layout;
};
/**
* Extends mxMedianHybridCrossingReduction.
*/
mxMedianHybridCrossingReduction.prototype = new mxHierarchicalLayoutStage();
mxMedianHybridCrossingReduction.prototype.constructor = mxMedianHybridCrossingReduction;
/**
* Variable: layout
*
* Reference to the enclosing <mxHierarchicalLayout>.
*/
mxMedianHybridCrossingReduction.prototype.layout = null;
/**
* Variable: maxIterations
*
* The maximum number of iterations to perform whilst reducing edge
* crossings. Default is 24.
*/
mxMedianHybridCrossingReduction.prototype.maxIterations = 24;
/**
* Variable: nestedBestRanks
*
* Stores each rank as a collection of cells in the best order found for
* each layer so far
*/
mxMedianHybridCrossingReduction.prototype.nestedBestRanks = null;
/**
* Variable: currentBestCrossings
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.currentBestCrossings = 0;
/**
* Variable: iterationsWithoutImprovement
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.iterationsWithoutImprovement = 0;
/**
* Variable: maxNoImprovementIterations
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.maxNoImprovementIterations = 2;
/**
* Function: execute
*
* Performs a vertex ordering within ranks as described by Gansner et al
* 1993
*/
mxMedianHybridCrossingReduction.prototype.execute = function(parent)
{
var model = this.layout.getModel();
// Stores initial ordering as being the best one found so far
this.nestedBestRanks = [];
for (var i = 0; i < model.ranks.length; i++)
{
this.nestedBestRanks[i] = model.ranks[i].slice();
}
var iterationsWithoutImprovement = 0;
var currentBestCrossings = this.calculateCrossings(model);
for (var i = 0; i < this.maxIterations &&
iterationsWithoutImprovement < this.maxNoImprovementIterations; i++)
{
this.weightedMedian(i, model);
this.transpose(i, model);
var candidateCrossings = this.calculateCrossings(model);
if (candidateCrossings < currentBestCrossings)
{
currentBestCrossings = candidateCrossings;
iterationsWithoutImprovement = 0;
// Store the current rankings as the best ones
for (var j = 0; j < this.nestedBestRanks.length; j++)
{
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
this.nestedBestRanks[j][cell.getGeneralPurposeVariable(j)] = cell;
}
}
}
else
{
// Increase count of iterations where we haven't improved the
// layout
iterationsWithoutImprovement++;
// Restore the best values to the cells
for (var j = 0; j < this.nestedBestRanks.length; j++)
{
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
cell.setGeneralPurposeVariable(j, k);
}
}
}
if (currentBestCrossings == 0)
{
// Do nothing further
break;
}
}
// Store the best rankings but in the model
var ranks = [];
var rankList = [];
for (var i = 0; i < model.maxRank + 1; i++)
{
rankList[i] = [];
ranks[i] = rankList[i];
}
for (var i = 0; i < this.nestedBestRanks.length; i++)
{
for (var j = 0; j < this.nestedBestRanks[i].length; j++)
{
rankList[i].push(this.nestedBestRanks[i][j]);
}
}
model.ranks = ranks;
};
/**
* Function: calculateCrossings
*
* Calculates the total number of edge crossing in the current graph.
* Returns the current number of edge crossings in the hierarchy graph
* model in the current candidate layout
*
* Parameters:
*
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.calculateCrossings = function(model)
{
var numRanks = model.ranks.length;
var totalCrossings = 0;
for (var i = 1; i < numRanks; i++)
{
totalCrossings += this.calculateRankCrossing(i, model);
}
return totalCrossings;
};
/**
* Function: calculateRankCrossing
*
* Calculates the number of edges crossings between the specified rank and
* the rank below it. Returns the number of edges crossings with the rank
* beneath
*
* Parameters:
*
* i - the topmost rank of the pair ( higher rank value )
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.calculateRankCrossing = function(i, model)
{
var totalCrossings = 0;
var rank = model.ranks[i];
var previousRank = model.ranks[i - 1];
var tmpIndices = [];
// Iterate over the top rank and fill in the connection information
for (var j = 0; j < rank.length; j++)
{
var node = rank[j];
var rankPosition = node.getGeneralPurposeVariable(i);
var connectedCells = node.getPreviousLayerConnectedCells(i);
var nodeIndices = [];
for (var k = 0; k < connectedCells.length; k++)
{
var connectedNode = connectedCells[k];
var otherCellRankPosition = connectedNode.getGeneralPurposeVariable(i - 1);
nodeIndices.push(otherCellRankPosition);
}
nodeIndices.sort(function(x, y) { return x - y; });
tmpIndices[rankPosition] = nodeIndices;
}
var indices = [];
for (var j = 0; j < tmpIndices.length; j++)
{
indices = indices.concat(tmpIndices[j]);
}
var firstIndex = 1;
while (firstIndex < previousRank.length)
{
firstIndex <<= 1;
}
var treeSize = 2 * firstIndex - 1;
firstIndex -= 1;
var tree = [];
for (var j = 0; j < treeSize; ++j)
{
tree[j] = 0;
}
for (var j = 0; j < indices.length; j++)
{
var index = indices[j];
var treeIndex = index + firstIndex;
++tree[treeIndex];
while (treeIndex > 0)
{
if (treeIndex % 2)
{
totalCrossings += tree[treeIndex + 1];
}
treeIndex = (treeIndex - 1) >> 1;
++tree[treeIndex];
}
}
return totalCrossings;
};
/**
* Function: transpose
*
* Takes each possible adjacent cell pair on each rank and checks if
* swapping them around reduces the number of crossing
*
* Parameters:
*
* mainLoopIteration - the iteration number of the main loop
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.transpose = function(mainLoopIteration, model)
{
var improved = true;
// Track the number of iterations in case of looping
var count = 0;
var maxCount = 10;
while (improved && count++ < maxCount)
{
// On certain iterations allow allow swapping of cell pairs with
// equal edge crossings switched or not switched. This help to
// nudge a stuck layout into a lower crossing total.
var nudge = mainLoopIteration % 2 == 1 && count % 2 == 1;
improved = false;
for (var i = 0; i < model.ranks.length; i++)
{
var rank = model.ranks[i];
var orderedCells = [];
for (var j = 0; j < rank.length; j++)
{
var cell = rank[j];
var tempRank = cell.getGeneralPurposeVariable(i);
// FIXME: Workaround to avoid negative tempRanks
if (tempRank < 0)
{
tempRank = j;
}
orderedCells[tempRank] = cell;
}
var leftCellAboveConnections = null;
var leftCellBelowConnections = null;
var rightCellAboveConnections = null;
var rightCellBelowConnections = null;
var leftAbovePositions = null;
var leftBelowPositions = null;
var rightAbovePositions = null;
var rightBelowPositions = null;
var leftCell = null;
var rightCell = null;
for (var j = 0; j < (rank.length - 1); j++)
{
// For each intra-rank adjacent pair of cells
// see if swapping them around would reduce the
// number of edges crossing they cause in total
// On every cell pair except the first on each rank, we
// can save processing using the previous values for the
// right cell on the new left cell
if (j == 0)
{
leftCell = orderedCells[j];
leftCellAboveConnections = leftCell
.getNextLayerConnectedCells(i);
leftCellBelowConnections = leftCell
.getPreviousLayerConnectedCells(i);
leftAbovePositions = [];
leftBelowPositions = [];
for (var k = 0; k < leftCellAboveConnections.length; k++)
{
leftAbovePositions[k] = leftCellAboveConnections[k].getGeneralPurposeVariable(i + 1);
}
for (var k = 0; k < leftCellBelowConnections.length; k++)
{
leftBelowPositions[k] = leftCellBelowConnections[k].getGeneralPurposeVariable(i - 1);
}
}
else
{
leftCellAboveConnections = rightCellAboveConnections;
leftCellBelowConnections = rightCellBelowConnections;
leftAbovePositions = rightAbovePositions;
leftBelowPositions = rightBelowPositions;
leftCell = rightCell;
}
rightCell = orderedCells[j + 1];
rightCellAboveConnections = rightCell
.getNextLayerConnectedCells(i);
rightCellBelowConnections = rightCell
.getPreviousLayerConnectedCells(i);
rightAbovePositions = [];
rightBelowPositions = [];
for (var k = 0; k < rightCellAboveConnections.length; k++)
{
rightAbovePositions[k] = rightCellAboveConnections[k].getGeneralPurposeVariable(i + 1);
}
for (var k = 0; k < rightCellBelowConnections.length; k++)
{
rightBelowPositions[k] = rightCellBelowConnections[k].getGeneralPurposeVariable(i - 1);
}
var totalCurrentCrossings = 0;
var totalSwitchedCrossings = 0;
for (var k = 0; k < leftAbovePositions.length; k++)
{
for (var ik = 0; ik < rightAbovePositions.length; ik++)
{
if (leftAbovePositions[k] > rightAbovePositions[ik])
{
totalCurrentCrossings++;
}
if (leftAbovePositions[k] < rightAbovePositions[ik])
{
totalSwitchedCrossings++;
}
}
}
for (var k = 0; k < leftBelowPositions.length; k++)
{
for (var ik = 0; ik < rightBelowPositions.length; ik++)
{
if (leftBelowPositions[k] > rightBelowPositions[ik])
{
totalCurrentCrossings++;
}
if (leftBelowPositions[k] < rightBelowPositions[ik])
{
totalSwitchedCrossings++;
}
}
}
if ((totalSwitchedCrossings < totalCurrentCrossings) ||
(totalSwitchedCrossings == totalCurrentCrossings &&
nudge))
{
var temp = leftCell.getGeneralPurposeVariable(i);
leftCell.setGeneralPurposeVariable(i, rightCell
.getGeneralPurposeVariable(i));
rightCell.setGeneralPurposeVariable(i, temp);
// With this pair exchanged we have to switch all of
// values for the left cell to the right cell so the
// next iteration for this rank uses it as the left
// cell again
rightCellAboveConnections = leftCellAboveConnections;
rightCellBelowConnections = leftCellBelowConnections;
rightAbovePositions = leftAbovePositions;
rightBelowPositions = leftBelowPositions;
rightCell = leftCell;
if (!nudge)
{
// Don't count nudges as improvement or we'll end
// up stuck in two combinations and not finishing
// as early as we should
improved = true;
}
}
}
}
}
};
/**
* Function: weightedMedian
*
* Sweeps up or down the layout attempting to minimise the median placement
* of connected cells on adjacent ranks
*
* Parameters:
*
* iteration - the iteration number of the main loop
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.weightedMedian = function(iteration, model)
{
// Reverse sweep direction each time through this method
var downwardSweep = (iteration % 2 == 0);
if (downwardSweep)
{
for (var j = model.maxRank - 1; j >= 0; j--)
{
this.medianRank(j, downwardSweep);
}
}
else
{
for (var j = 1; j < model.maxRank; j++)
{
this.medianRank(j, downwardSweep);
}
}
};
/**
* Function: medianRank
*
* Attempts to minimise the median placement of connected cells on this rank
* and one of the adjacent ranks
*
* Parameters:
*
* rankValue - the layer number of this rank
* downwardSweep - whether or not this is a downward sweep through the graph
*/
mxMedianHybridCrossingReduction.prototype.medianRank = function(rankValue, downwardSweep)
{
var numCellsForRank = this.nestedBestRanks[rankValue].length;
var medianValues = [];
var reservedPositions = [];
for (var i = 0; i < numCellsForRank; i++)
{
var cell = this.nestedBestRanks[rankValue][i];
var sorterEntry = new MedianCellSorter();
sorterEntry.cell = cell;
// Flip whether or not equal medians are flipped on up and down
// sweeps
// TODO re-implement some kind of nudge
// medianValues[i].nudge = !downwardSweep;
var nextLevelConnectedCells;
if (downwardSweep)
{
nextLevelConnectedCells = cell
.getNextLayerConnectedCells(rankValue);
}
else
{
nextLevelConnectedCells = cell
.getPreviousLayerConnectedCells(rankValue);
}
var nextRankValue;
if (downwardSweep)
{
nextRankValue = rankValue + 1;
}
else
{
nextRankValue = rankValue - 1;
}
if (nextLevelConnectedCells != null
&& nextLevelConnectedCells.length != 0)
{
sorterEntry.medianValue = this.medianValue(
nextLevelConnectedCells, nextRankValue);
medianValues.push(sorterEntry);
}
else
{
// Nodes with no adjacent vertices are flagged in the reserved array
// to indicate they should be left in their current position.
reservedPositions[cell.getGeneralPurposeVariable(rankValue)] = true;
}
}
medianValues.sort(MedianCellSorter.prototype.compare);
// Set the new position of each node within the rank using
// its temp variable
for (var i = 0; i < numCellsForRank; i++)
{
if (reservedPositions[i] == null)
{
var cell = medianValues.shift().cell;
cell.setGeneralPurposeVariable(rankValue, i);
}
}
};
/**
* Function: medianValue
*
* Calculates the median rank order positioning for the specified cell using
* the connected cells on the specified rank. Returns the median rank
* ordering value of the connected cells
*
* Parameters:
*
* connectedCells - the cells on the specified rank connected to the
* specified cell
* rankValue - the rank that the connected cell lie upon
*/
mxMedianHybridCrossingReduction.prototype.medianValue = function(connectedCells, rankValue)
{
var medianValues = [];
var arrayCount = 0;
for (var i = 0; i < connectedCells.length; i++)
{
var cell = connectedCells[i];
medianValues[arrayCount++] = cell.getGeneralPurposeVariable(rankValue);
}
// Sort() sorts lexicographically by default (i.e. 11 before 9) so force
// numerical order sort
medianValues.sort(function(a,b){return a - b;});
if (arrayCount % 2 == 1)
{
// For odd numbers of adjacent vertices return the median
return medianValues[Math.floor(arrayCount / 2)];
}
else if (arrayCount == 2)
{
return ((medianValues[0] + medianValues[1]) / 2.0);
}
else
{
var medianPoint = arrayCount / 2;
var leftMedian = medianValues[medianPoint - 1] - medianValues[0];
var rightMedian = medianValues[arrayCount - 1]
- medianValues[medianPoint];
return (medianValues[medianPoint - 1] * rightMedian + medianValues[medianPoint]
* leftMedian)
/ (leftMedian + rightMedian);
}
};
/**
* Class: MedianCellSorter
*
* A utility class used to track cells whilst sorting occurs on the median
* values. Does not violate (x.compareTo(y)==0) == (x.equals(y))
*
* Constructor: MedianCellSorter
*
* Constructs a new median cell sorter.
*/
function MedianCellSorter()
{
// empty
};
/**
* Variable: medianValue
*
* The weighted value of the cell stored.
*/
MedianCellSorter.prototype.medianValue = 0;
/**
* Variable: cell
*
* The cell whose median value is being calculated
*/
MedianCellSorter.prototype.cell = false;
/**
* Function: compare
*
* Compares two MedianCellSorters.
*/
MedianCellSorter.prototype.compare = function(a, b)
{
if (a != null && b != null)
{
if (b.medianValue > a.medianValue)
{
return -1;
}
else if (b.medianValue < a.medianValue)
{
return 1;
}
else
{
return 0;
}
}
else
{
return 0;
}
};