|
var Layout = require('../Layout'); |
|
var FDLayoutConstants = require('./FDLayoutConstants'); |
|
var LayoutConstants = require('../LayoutConstants'); |
|
var IGeometry = require('../util/IGeometry'); |
|
var IMath = require('../util/IMath'); |
|
|
|
function FDLayout() { |
|
Layout.call(this); |
|
|
|
this.useSmartIdealEdgeLengthCalculation = FDLayoutConstants.DEFAULT_USE_SMART_IDEAL_EDGE_LENGTH_CALCULATION; |
|
this.idealEdgeLength = FDLayoutConstants.DEFAULT_EDGE_LENGTH; |
|
this.springConstant = FDLayoutConstants.DEFAULT_SPRING_STRENGTH; |
|
this.repulsionConstant = FDLayoutConstants.DEFAULT_REPULSION_STRENGTH; |
|
this.gravityConstant = FDLayoutConstants.DEFAULT_GRAVITY_STRENGTH; |
|
this.compoundGravityConstant = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH; |
|
this.gravityRangeFactor = FDLayoutConstants.DEFAULT_GRAVITY_RANGE_FACTOR; |
|
this.compoundGravityRangeFactor = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR; |
|
this.displacementThresholdPerNode = (3.0 * FDLayoutConstants.DEFAULT_EDGE_LENGTH) / 100; |
|
this.coolingFactor = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL; |
|
this.initialCoolingFactor = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL; |
|
this.totalDisplacement = 0.0; |
|
this.oldTotalDisplacement = 0.0; |
|
this.maxIterations = FDLayoutConstants.MAX_ITERATIONS; |
|
} |
|
|
|
FDLayout.prototype = Object.create(Layout.prototype); |
|
|
|
for (var prop in Layout) { |
|
FDLayout[prop] = Layout[prop]; |
|
} |
|
|
|
FDLayout.prototype.initParameters = function () { |
|
Layout.prototype.initParameters.call(this, arguments); |
|
|
|
this.totalIterations = 0; |
|
this.notAnimatedIterations = 0; |
|
|
|
this.useFRGridVariant = FDLayoutConstants.DEFAULT_USE_SMART_REPULSION_RANGE_CALCULATION; |
|
|
|
this.grid = []; |
|
}; |
|
|
|
FDLayout.prototype.calcIdealEdgeLengths = function () { |
|
var edge; |
|
var lcaDepth; |
|
var source; |
|
var target; |
|
var sizeOfSourceInLca; |
|
var sizeOfTargetInLca; |
|
|
|
var allEdges = this.getGraphManager().getAllEdges(); |
|
for (var i = 0; i < allEdges.length; i++) |
|
{ |
|
edge = allEdges[i]; |
|
|
|
edge.idealLength = this.idealEdgeLength; |
|
|
|
if (edge.isInterGraph) |
|
{ |
|
source = edge.getSource(); |
|
target = edge.getTarget(); |
|
|
|
sizeOfSourceInLca = edge.getSourceInLca().getEstimatedSize(); |
|
sizeOfTargetInLca = edge.getTargetInLca().getEstimatedSize(); |
|
|
|
if (this.useSmartIdealEdgeLengthCalculation) |
|
{ |
|
edge.idealLength += sizeOfSourceInLca + sizeOfTargetInLca - |
|
2 * LayoutConstants.SIMPLE_NODE_SIZE; |
|
} |
|
|
|
lcaDepth = edge.getLca().getInclusionTreeDepth(); |
|
|
|
edge.idealLength += FDLayoutConstants.DEFAULT_EDGE_LENGTH * |
|
FDLayoutConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR * |
|
(source.getInclusionTreeDepth() + |
|
target.getInclusionTreeDepth() - 2 * lcaDepth); |
|
} |
|
} |
|
}; |
|
|
|
FDLayout.prototype.initSpringEmbedder = function () { |
|
|
|
var s = this.getAllNodes().length; |
|
if (this.incremental) { |
|
if(s > FDLayoutConstants.ADAPTATION_LOWER_NODE_LIMIT){ |
|
this.coolingFactor = Math.max(this.coolingFactor*FDLayoutConstants.COOLING_ADAPTATION_FACTOR, this.coolingFactor - |
|
(s-FDLayoutConstants.ADAPTATION_LOWER_NODE_LIMIT)/(FDLayoutConstants.ADAPTATION_UPPER_NODE_LIMIT-FDLayoutConstants.ADAPTATION_LOWER_NODE_LIMIT)*this.coolingFactor*(1-FDLayoutConstants.COOLING_ADAPTATION_FACTOR)); |
|
} |
|
this.maxNodeDisplacement = FDLayoutConstants.MAX_NODE_DISPLACEMENT_INCREMENTAL; |
|
} |
|
else { |
|
if(s > FDLayoutConstants.ADAPTATION_LOWER_NODE_LIMIT){ |
|
this.coolingFactor = Math.max(FDLayoutConstants.COOLING_ADAPTATION_FACTOR, 1.0 - |
|
(s-FDLayoutConstants.ADAPTATION_LOWER_NODE_LIMIT)/(FDLayoutConstants.ADAPTATION_UPPER_NODE_LIMIT-FDLayoutConstants.ADAPTATION_LOWER_NODE_LIMIT)*(1-FDLayoutConstants.COOLING_ADAPTATION_FACTOR)); |
|
} |
|
else { |
|
this.coolingFactor = 1.0; |
|
} |
|
this.initialCoolingFactor = this.coolingFactor; |
|
this.maxNodeDisplacement = FDLayoutConstants.MAX_NODE_DISPLACEMENT; |
|
} |
|
|
|
this.maxIterations = |
|
Math.max(this.getAllNodes().length * 5, this.maxIterations); |
|
|
|
this.totalDisplacementThreshold = |
|
this.displacementThresholdPerNode * this.getAllNodes().length; |
|
|
|
this.repulsionRange = this.calcRepulsionRange(); |
|
}; |
|
|
|
FDLayout.prototype.calcSpringForces = function () { |
|
var lEdges = this.getAllEdges(); |
|
var edge; |
|
|
|
for (var i = 0; i < lEdges.length; i++) |
|
{ |
|
edge = lEdges[i]; |
|
|
|
this.calcSpringForce(edge, edge.idealLength); |
|
} |
|
}; |
|
|
|
FDLayout.prototype.calcRepulsionForces = function (gridUpdateAllowed = true, forceToNodeSurroundingUpdate = false) { |
|
var i, j; |
|
var nodeA, nodeB; |
|
var lNodes = this.getAllNodes(); |
|
var processedNodeSet; |
|
|
|
if (this.useFRGridVariant) |
|
{ |
|
if ((this.totalIterations % FDLayoutConstants.GRID_CALCULATION_CHECK_PERIOD == 1 && gridUpdateAllowed)) |
|
{ |
|
this.updateGrid(); |
|
} |
|
|
|
processedNodeSet = new Set(); |
|
|
|
|
|
for (i = 0; i < lNodes.length; i++) |
|
{ |
|
nodeA = lNodes[i]; |
|
this.calculateRepulsionForceOfANode(nodeA, processedNodeSet, gridUpdateAllowed, forceToNodeSurroundingUpdate); |
|
processedNodeSet.add(nodeA); |
|
} |
|
} |
|
else |
|
{ |
|
for (i = 0; i < lNodes.length; i++) |
|
{ |
|
nodeA = lNodes[i]; |
|
|
|
for (j = i + 1; j < lNodes.length; j++) |
|
{ |
|
nodeB = lNodes[j]; |
|
|
|
|
|
if (nodeA.getOwner() != nodeB.getOwner()) |
|
{ |
|
continue; |
|
} |
|
|
|
this.calcRepulsionForce(nodeA, nodeB); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
FDLayout.prototype.calcGravitationalForces = function () { |
|
var node; |
|
var lNodes = this.getAllNodesToApplyGravitation(); |
|
|
|
for (var i = 0; i < lNodes.length; i++) |
|
{ |
|
node = lNodes[i]; |
|
this.calcGravitationalForce(node); |
|
} |
|
}; |
|
|
|
FDLayout.prototype.moveNodes = function () { |
|
var lNodes = this.getAllNodes(); |
|
var node; |
|
|
|
for (var i = 0; i < lNodes.length; i++) |
|
{ |
|
node = lNodes[i]; |
|
node.move(); |
|
} |
|
} |
|
|
|
FDLayout.prototype.calcSpringForce = function (edge, idealLength) { |
|
var sourceNode = edge.getSource(); |
|
var targetNode = edge.getTarget(); |
|
|
|
var length; |
|
var springForce; |
|
var springForceX; |
|
var springForceY; |
|
|
|
|
|
if (this.uniformLeafNodeSizes && |
|
sourceNode.getChild() == null && targetNode.getChild() == null) |
|
{ |
|
edge.updateLengthSimple(); |
|
} |
|
else |
|
{ |
|
edge.updateLength(); |
|
|
|
if (edge.isOverlapingSourceAndTarget) |
|
{ |
|
return; |
|
} |
|
} |
|
|
|
length = edge.getLength(); |
|
|
|
if(length == 0) |
|
return; |
|
|
|
|
|
springForce = this.springConstant * (length - idealLength); |
|
|
|
|
|
springForceX = springForce * (edge.lengthX / length); |
|
springForceY = springForce * (edge.lengthY / length); |
|
|
|
|
|
sourceNode.springForceX += springForceX; |
|
sourceNode.springForceY += springForceY; |
|
targetNode.springForceX -= springForceX; |
|
targetNode.springForceY -= springForceY; |
|
}; |
|
|
|
FDLayout.prototype.calcRepulsionForce = function (nodeA, nodeB) { |
|
var rectA = nodeA.getRect(); |
|
var rectB = nodeB.getRect(); |
|
var overlapAmount = new Array(2); |
|
var clipPoints = new Array(4); |
|
var distanceX; |
|
var distanceY; |
|
var distanceSquared; |
|
var distance; |
|
var repulsionForce; |
|
var repulsionForceX; |
|
var repulsionForceY; |
|
|
|
if (rectA.intersects(rectB)) |
|
{ |
|
|
|
IGeometry.calcSeparationAmount(rectA, |
|
rectB, |
|
overlapAmount, |
|
FDLayoutConstants.DEFAULT_EDGE_LENGTH / 2.0); |
|
|
|
repulsionForceX = 2 * overlapAmount[0]; |
|
repulsionForceY = 2 * overlapAmount[1]; |
|
|
|
var childrenConstant = nodeA.noOfChildren * nodeB.noOfChildren / (nodeA.noOfChildren + nodeB.noOfChildren); |
|
|
|
|
|
nodeA.repulsionForceX -= childrenConstant * repulsionForceX; |
|
nodeA.repulsionForceY -= childrenConstant * repulsionForceY; |
|
nodeB.repulsionForceX += childrenConstant * repulsionForceX; |
|
nodeB.repulsionForceY += childrenConstant * repulsionForceY; |
|
} |
|
else |
|
{ |
|
|
|
|
|
if (this.uniformLeafNodeSizes && |
|
nodeA.getChild() == null && nodeB.getChild() == null) |
|
{ |
|
distanceX = rectB.getCenterX() - rectA.getCenterX(); |
|
distanceY = rectB.getCenterY() - rectA.getCenterY(); |
|
} |
|
else |
|
{ |
|
IGeometry.getIntersection(rectA, rectB, clipPoints); |
|
|
|
distanceX = clipPoints[2] - clipPoints[0]; |
|
distanceY = clipPoints[3] - clipPoints[1]; |
|
} |
|
|
|
|
|
if (Math.abs(distanceX) < FDLayoutConstants.MIN_REPULSION_DIST) |
|
{ |
|
distanceX = IMath.sign(distanceX) * |
|
FDLayoutConstants.MIN_REPULSION_DIST; |
|
} |
|
|
|
if (Math.abs(distanceY) < FDLayoutConstants.MIN_REPULSION_DIST) |
|
{ |
|
distanceY = IMath.sign(distanceY) * |
|
FDLayoutConstants.MIN_REPULSION_DIST; |
|
} |
|
|
|
distanceSquared = distanceX * distanceX + distanceY * distanceY; |
|
distance = Math.sqrt(distanceSquared); |
|
|
|
repulsionForce = this.repulsionConstant * nodeA.noOfChildren * nodeB.noOfChildren / distanceSquared; |
|
|
|
|
|
repulsionForceX = repulsionForce * distanceX / distance; |
|
repulsionForceY = repulsionForce * distanceY / distance; |
|
|
|
|
|
nodeA.repulsionForceX -= repulsionForceX; |
|
nodeA.repulsionForceY -= repulsionForceY; |
|
nodeB.repulsionForceX += repulsionForceX; |
|
nodeB.repulsionForceY += repulsionForceY; |
|
} |
|
}; |
|
|
|
FDLayout.prototype.calcGravitationalForce = function (node) { |
|
var ownerGraph; |
|
var ownerCenterX; |
|
var ownerCenterY; |
|
var distanceX; |
|
var distanceY; |
|
var absDistanceX; |
|
var absDistanceY; |
|
var estimatedSize; |
|
ownerGraph = node.getOwner(); |
|
|
|
ownerCenterX = (ownerGraph.getRight() + ownerGraph.getLeft()) / 2; |
|
ownerCenterY = (ownerGraph.getTop() + ownerGraph.getBottom()) / 2; |
|
distanceX = node.getCenterX() - ownerCenterX; |
|
distanceY = node.getCenterY() - ownerCenterY; |
|
absDistanceX = Math.abs(distanceX) + node.getWidth() / 2; |
|
absDistanceY = Math.abs(distanceY) + node.getHeight() / 2; |
|
|
|
if (node.getOwner() == this.graphManager.getRoot()) |
|
{ |
|
estimatedSize = ownerGraph.getEstimatedSize() * this.gravityRangeFactor; |
|
|
|
if (absDistanceX > estimatedSize || absDistanceY > estimatedSize) |
|
{ |
|
node.gravitationForceX = -this.gravityConstant * distanceX; |
|
node.gravitationForceY = -this.gravityConstant * distanceY; |
|
} |
|
} |
|
else |
|
{ |
|
estimatedSize = ownerGraph.getEstimatedSize() * this.compoundGravityRangeFactor; |
|
|
|
if (absDistanceX > estimatedSize || absDistanceY > estimatedSize) |
|
{ |
|
node.gravitationForceX = -this.gravityConstant * distanceX * |
|
this.compoundGravityConstant; |
|
node.gravitationForceY = -this.gravityConstant * distanceY * |
|
this.compoundGravityConstant; |
|
} |
|
} |
|
}; |
|
|
|
FDLayout.prototype.isConverged = function () { |
|
var converged; |
|
var oscilating = false; |
|
|
|
if (this.totalIterations > this.maxIterations / 3) |
|
{ |
|
oscilating = |
|
Math.abs(this.totalDisplacement - this.oldTotalDisplacement) < 2; |
|
} |
|
|
|
converged = this.totalDisplacement < this.totalDisplacementThreshold; |
|
|
|
this.oldTotalDisplacement = this.totalDisplacement; |
|
|
|
return converged || oscilating; |
|
}; |
|
|
|
FDLayout.prototype.animate = function () { |
|
if (this.animationDuringLayout && !this.isSubLayout) |
|
{ |
|
if (this.notAnimatedIterations == this.animationPeriod) |
|
{ |
|
this.update(); |
|
this.notAnimatedIterations = 0; |
|
} |
|
else |
|
{ |
|
this.notAnimatedIterations++; |
|
} |
|
} |
|
}; |
|
|
|
|
|
FDLayout.prototype.calcNoOfChildrenForAllNodes = function () |
|
{ |
|
var node; |
|
var allNodes = this.graphManager.getAllNodes(); |
|
|
|
for(var i = 0; i < allNodes.length; i++) |
|
{ |
|
node = allNodes[i]; |
|
node.noOfChildren = node.getNoOfChildren(); |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
FDLayout.prototype.calcGrid = function (graph){ |
|
|
|
var sizeX = 0; |
|
var sizeY = 0; |
|
|
|
sizeX = parseInt(Math.ceil((graph.getRight() - graph.getLeft()) / this.repulsionRange)); |
|
sizeY = parseInt(Math.ceil((graph.getBottom() - graph.getTop()) / this.repulsionRange)); |
|
|
|
var grid = new Array(sizeX); |
|
|
|
for(var i = 0; i < sizeX; i++){ |
|
grid[i] = new Array(sizeY); |
|
} |
|
|
|
for(var i = 0; i < sizeX; i++){ |
|
for(var j = 0; j < sizeY; j++){ |
|
grid[i][j] = new Array(); |
|
} |
|
} |
|
|
|
return grid; |
|
}; |
|
|
|
FDLayout.prototype.addNodeToGrid = function (v, left, top){ |
|
|
|
var startX = 0; |
|
var finishX = 0; |
|
var startY = 0; |
|
var finishY = 0; |
|
|
|
startX = parseInt(Math.floor((v.getRect().x - left) / this.repulsionRange)); |
|
finishX = parseInt(Math.floor((v.getRect().width + v.getRect().x - left) / this.repulsionRange)); |
|
startY = parseInt(Math.floor((v.getRect().y - top) / this.repulsionRange)); |
|
finishY = parseInt(Math.floor((v.getRect().height + v.getRect().y - top) / this.repulsionRange)); |
|
|
|
for (var i = startX; i <= finishX; i++) |
|
{ |
|
for (var j = startY; j <= finishY; j++) |
|
{ |
|
this.grid[i][j].push(v); |
|
v.setGridCoordinates(startX, finishX, startY, finishY); |
|
} |
|
} |
|
|
|
}; |
|
|
|
FDLayout.prototype.updateGrid = function() { |
|
var i; |
|
var nodeA; |
|
var lNodes = this.getAllNodes(); |
|
|
|
this.grid = this.calcGrid(this.graphManager.getRoot()); |
|
|
|
|
|
for (i = 0; i < lNodes.length; i++) |
|
{ |
|
nodeA = lNodes[i]; |
|
this.addNodeToGrid(nodeA, this.graphManager.getRoot().getLeft(), this.graphManager.getRoot().getTop()); |
|
} |
|
|
|
}; |
|
|
|
FDLayout.prototype.calculateRepulsionForceOfANode = function (nodeA, processedNodeSet, gridUpdateAllowed, forceToNodeSurroundingUpdate){ |
|
|
|
if ((this.totalIterations % FDLayoutConstants.GRID_CALCULATION_CHECK_PERIOD == 1 && gridUpdateAllowed) || forceToNodeSurroundingUpdate) |
|
{ |
|
var surrounding = new Set(); |
|
nodeA.surrounding = new Array(); |
|
var nodeB; |
|
var grid = this.grid; |
|
|
|
for (var i = (nodeA.startX - 1); i < (nodeA.finishX + 2); i++) |
|
{ |
|
for (var j = (nodeA.startY - 1); j < (nodeA.finishY + 2); j++) |
|
{ |
|
if (!((i < 0) || (j < 0) || (i >= grid.length) || (j >= grid[0].length))) |
|
{ |
|
for (var k = 0; k < grid[i][j].length; k++) { |
|
nodeB = grid[i][j][k]; |
|
|
|
|
|
|
|
if ((nodeA.getOwner() != nodeB.getOwner()) || (nodeA == nodeB)) |
|
{ |
|
continue; |
|
} |
|
|
|
|
|
|
|
if (!processedNodeSet.has(nodeB) && !surrounding.has(nodeB)) |
|
{ |
|
var distanceX = Math.abs(nodeA.getCenterX()-nodeB.getCenterX()) - |
|
((nodeA.getWidth()/2) + (nodeB.getWidth()/2)); |
|
var distanceY = Math.abs(nodeA.getCenterY()-nodeB.getCenterY()) - |
|
((nodeA.getHeight()/2) + (nodeB.getHeight()/2)); |
|
|
|
|
|
|
|
if ((distanceX <= this.repulsionRange) && (distanceY <= this.repulsionRange)) |
|
{ |
|
|
|
surrounding.add(nodeB); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
nodeA.surrounding = [...surrounding]; |
|
|
|
} |
|
for (i = 0; i < nodeA.surrounding.length; i++) |
|
{ |
|
this.calcRepulsionForce(nodeA, nodeA.surrounding[i]); |
|
} |
|
}; |
|
|
|
FDLayout.prototype.calcRepulsionRange = function () { |
|
return 0.0; |
|
}; |
|
|
|
module.exports = FDLayout; |
|
|