Mon 21 Jul 22:43:21 CEST 2025
This commit is contained in:
parent
c922220a8b
commit
1335e0c438
|
@ -0,0 +1,675 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user