mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1243 lines
37 KiB
1243 lines
37 KiB
var FDLayout = require('layout-base').FDLayout;
|
|
var CoSEGraphManager = require('./CoSEGraphManager');
|
|
var CoSEGraph = require('./CoSEGraph');
|
|
var CoSENode = require('./CoSENode');
|
|
var CoSEEdge = require('./CoSEEdge');
|
|
var CoSEConstants = require('./CoSEConstants');
|
|
var FDLayoutConstants = require('layout-base').FDLayoutConstants;
|
|
var LayoutConstants = require('layout-base').LayoutConstants;
|
|
var Point = require('layout-base').Point;
|
|
var PointD = require('layout-base').PointD;
|
|
var Layout = require('layout-base').Layout;
|
|
var Integer = require('layout-base').Integer;
|
|
var IGeometry = require('layout-base').IGeometry;
|
|
var LGraph = require('layout-base').LGraph;
|
|
var Transform = require('layout-base').Transform;
|
|
|
|
function CoSELayout() {
|
|
FDLayout.call(this);
|
|
|
|
this.toBeTiled = {}; // Memorize if a node is to be tiled or is tiled
|
|
}
|
|
|
|
CoSELayout.prototype = Object.create(FDLayout.prototype);
|
|
|
|
for (var prop in FDLayout) {
|
|
CoSELayout[prop] = FDLayout[prop];
|
|
}
|
|
|
|
CoSELayout.prototype.newGraphManager = function () {
|
|
var gm = new CoSEGraphManager(this);
|
|
this.graphManager = gm;
|
|
return gm;
|
|
};
|
|
|
|
CoSELayout.prototype.newGraph = function (vGraph) {
|
|
return new CoSEGraph(null, this.graphManager, vGraph);
|
|
};
|
|
|
|
CoSELayout.prototype.newNode = function (vNode) {
|
|
return new CoSENode(this.graphManager, vNode);
|
|
};
|
|
|
|
CoSELayout.prototype.newEdge = function (vEdge) {
|
|
return new CoSEEdge(null, null, vEdge);
|
|
};
|
|
|
|
CoSELayout.prototype.initParameters = function () {
|
|
FDLayout.prototype.initParameters.call(this, arguments);
|
|
if (!this.isSubLayout) {
|
|
if (CoSEConstants.DEFAULT_EDGE_LENGTH < 10)
|
|
{
|
|
this.idealEdgeLength = 10;
|
|
}
|
|
else
|
|
{
|
|
this.idealEdgeLength = CoSEConstants.DEFAULT_EDGE_LENGTH;
|
|
}
|
|
|
|
this.useSmartIdealEdgeLengthCalculation =
|
|
CoSEConstants.DEFAULT_USE_SMART_IDEAL_EDGE_LENGTH_CALCULATION;
|
|
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;
|
|
|
|
// variables for tree reduction support
|
|
this.prunedNodesAll = [];
|
|
this.growTreeIterations = 0;
|
|
this.afterGrowthIterations = 0;
|
|
this.isTreeGrowing = false;
|
|
this.isGrowthFinished = false;
|
|
|
|
// variables for cooling
|
|
this.coolingCycle = 0;
|
|
this.maxCoolingCycle = this.maxIterations/FDLayoutConstants.CONVERGENCE_CHECK_PERIOD;
|
|
this.finalTemperature = FDLayoutConstants.CONVERGENCE_CHECK_PERIOD/this.maxIterations;
|
|
this.coolingAdjuster = 1;
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.layout = function () {
|
|
var createBendsAsNeeded = LayoutConstants.DEFAULT_CREATE_BENDS_AS_NEEDED;
|
|
if (createBendsAsNeeded)
|
|
{
|
|
this.createBendpoints();
|
|
this.graphManager.resetAllEdges();
|
|
}
|
|
|
|
this.level = 0;
|
|
return this.classicLayout();
|
|
};
|
|
|
|
CoSELayout.prototype.classicLayout = function () {
|
|
this.nodesWithGravity = this.calculateNodesToApplyGravitationTo();
|
|
this.graphManager.setAllNodesToApplyGravitation(this.nodesWithGravity);
|
|
this.calcNoOfChildrenForAllNodes();
|
|
this.graphManager.calcLowestCommonAncestors();
|
|
this.graphManager.calcInclusionTreeDepths();
|
|
this.graphManager.getRoot().calcEstimatedSize();
|
|
this.calcIdealEdgeLengths();
|
|
|
|
if (!this.incremental)
|
|
{
|
|
var forest = this.getFlatForest();
|
|
|
|
// The graph associated with this layout is flat and a forest
|
|
if (forest.length > 0)
|
|
{
|
|
this.positionNodesRadially(forest);
|
|
}
|
|
// The graph associated with this layout is not flat or a forest
|
|
else
|
|
{
|
|
// Reduce the trees when incremental mode is not enabled and graph is not a forest
|
|
this.reduceTrees();
|
|
// Update nodes that gravity will be applied
|
|
this.graphManager.resetAllNodesToApplyGravitation();
|
|
var allNodes = new Set(this.getAllNodes());
|
|
var intersection = this.nodesWithGravity.filter(x => allNodes.has(x));
|
|
this.graphManager.setAllNodesToApplyGravitation(intersection);
|
|
|
|
this.positionNodesRandomly();
|
|
}
|
|
}
|
|
else {
|
|
if(CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL){
|
|
// Reduce the trees in incremental mode if only this constant is set to true
|
|
this.reduceTrees();
|
|
// Update nodes that gravity will be applied
|
|
this.graphManager.resetAllNodesToApplyGravitation();
|
|
var allNodes = new Set(this.getAllNodes());
|
|
var intersection = this.nodesWithGravity.filter(x => allNodes.has(x));
|
|
this.graphManager.setAllNodesToApplyGravitation(intersection);
|
|
}
|
|
}
|
|
|
|
this.initSpringEmbedder();
|
|
this.runSpringEmbedder();
|
|
|
|
return true;
|
|
};
|
|
|
|
CoSELayout.prototype.tick = function() {
|
|
this.totalIterations++;
|
|
|
|
if (this.totalIterations === this.maxIterations && !this.isTreeGrowing && !this.isGrowthFinished) {
|
|
if(this.prunedNodesAll.length > 0){
|
|
this.isTreeGrowing = true;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (this.totalIterations % FDLayoutConstants.CONVERGENCE_CHECK_PERIOD == 0 && !this.isTreeGrowing && !this.isGrowthFinished)
|
|
{
|
|
if (this.isConverged())
|
|
{
|
|
if(this.prunedNodesAll.length > 0){
|
|
this.isTreeGrowing = true;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
this.coolingCycle++;
|
|
|
|
if(this.layoutQuality == 0) {
|
|
// quality - "draft"
|
|
this.coolingAdjuster = this.coolingCycle;
|
|
}
|
|
else if(this.layoutQuality == 1) {
|
|
// quality - "default"
|
|
this.coolingAdjuster = this.coolingCycle / 3;
|
|
}
|
|
|
|
// cooling schedule is based on http://www.btluke.com/simanf1.html -> cooling schedule 3
|
|
this.coolingFactor = Math.max(this.initialCoolingFactor - Math.pow(this.coolingCycle, Math.log(100 * (this.initialCoolingFactor - this.finalTemperature)) / Math.log(this.maxCoolingCycle))/100 * this.coolingAdjuster, this.finalTemperature);
|
|
this.animationPeriod = Math.ceil(this.initialAnimationPeriod * Math.sqrt(this.coolingFactor));
|
|
}
|
|
// Operations while tree is growing again
|
|
if(this.isTreeGrowing){
|
|
if(this.growTreeIterations % 10 == 0){
|
|
if(this.prunedNodesAll.length > 0) {
|
|
this.graphManager.updateBounds();
|
|
this.updateGrid();
|
|
this.growTree(this.prunedNodesAll);
|
|
// Update nodes that gravity will be applied
|
|
this.graphManager.resetAllNodesToApplyGravitation();
|
|
var allNodes = new Set(this.getAllNodes());
|
|
var intersection = this.nodesWithGravity.filter(x => allNodes.has(x));
|
|
this.graphManager.setAllNodesToApplyGravitation(intersection);
|
|
|
|
this.graphManager.updateBounds();
|
|
this.updateGrid();
|
|
this.coolingFactor = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL;
|
|
}
|
|
else {
|
|
this.isTreeGrowing = false;
|
|
this.isGrowthFinished = true;
|
|
}
|
|
}
|
|
this.growTreeIterations++;
|
|
}
|
|
// Operations after growth is finished
|
|
if(this.isGrowthFinished){
|
|
if (this.isConverged())
|
|
{
|
|
return true;
|
|
}
|
|
if(this.afterGrowthIterations % 10 == 0){
|
|
this.graphManager.updateBounds();
|
|
this.updateGrid();
|
|
}
|
|
this.coolingFactor = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL * ((100 - this.afterGrowthIterations) / 100);
|
|
this.afterGrowthIterations++;
|
|
}
|
|
|
|
var gridUpdateAllowed = !this.isTreeGrowing && !this.isGrowthFinished;
|
|
var forceToNodeSurroundingUpdate = (this.growTreeIterations % 10 == 1 && this.isTreeGrowing) || (this.afterGrowthIterations % 10 == 1 && this.isGrowthFinished);
|
|
|
|
this.totalDisplacement = 0;
|
|
this.graphManager.updateBounds();
|
|
this.calcSpringForces();
|
|
this.calcRepulsionForces(gridUpdateAllowed, forceToNodeSurroundingUpdate);
|
|
this.calcGravitationalForces();
|
|
this.moveNodes();
|
|
this.animate();
|
|
|
|
return false; // Layout is not ended yet return false
|
|
};
|
|
|
|
CoSELayout.prototype.getPositionsData = function() {
|
|
var allNodes = this.graphManager.getAllNodes();
|
|
var pData = {};
|
|
for (var i = 0; i < allNodes.length; i++) {
|
|
var rect = allNodes[i].rect;
|
|
var id = allNodes[i].id;
|
|
pData[id] = {
|
|
id: id,
|
|
x: rect.getCenterX(),
|
|
y: rect.getCenterY(),
|
|
w: rect.width,
|
|
h: rect.height
|
|
};
|
|
}
|
|
|
|
return pData;
|
|
};
|
|
|
|
CoSELayout.prototype.runSpringEmbedder = function () {
|
|
this.initialAnimationPeriod = 25;
|
|
this.animationPeriod = this.initialAnimationPeriod;
|
|
var layoutEnded = false;
|
|
|
|
// If aminate option is 'during' signal that layout is supposed to start iterating
|
|
if ( FDLayoutConstants.ANIMATE === 'during' ) {
|
|
this.emit('layoutstarted');
|
|
}
|
|
else {
|
|
// If aminate option is 'during' tick() function will be called on index.js
|
|
while (!layoutEnded) {
|
|
layoutEnded = this.tick();
|
|
}
|
|
|
|
this.graphManager.updateBounds();
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.calculateNodesToApplyGravitationTo = function () {
|
|
var nodeList = [];
|
|
var graph;
|
|
|
|
var graphs = this.graphManager.getGraphs();
|
|
var size = graphs.length;
|
|
var i;
|
|
for (i = 0; i < size; i++)
|
|
{
|
|
graph = graphs[i];
|
|
|
|
graph.updateConnected();
|
|
|
|
if (!graph.isConnected)
|
|
{
|
|
nodeList = nodeList.concat(graph.getNodes());
|
|
}
|
|
}
|
|
|
|
return nodeList;
|
|
};
|
|
|
|
CoSELayout.prototype.createBendpoints = function () {
|
|
var edges = [];
|
|
edges = edges.concat(this.graphManager.getAllEdges());
|
|
var visited = new Set();
|
|
var i;
|
|
for (i = 0; i < edges.length; i++)
|
|
{
|
|
var edge = edges[i];
|
|
|
|
if (!visited.has(edge))
|
|
{
|
|
var source = edge.getSource();
|
|
var target = edge.getTarget();
|
|
|
|
if (source == target)
|
|
{
|
|
edge.getBendpoints().push(new PointD());
|
|
edge.getBendpoints().push(new PointD());
|
|
this.createDummyNodesForBendpoints(edge);
|
|
visited.add(edge);
|
|
}
|
|
else
|
|
{
|
|
var edgeList = [];
|
|
|
|
edgeList = edgeList.concat(source.getEdgeListToNode(target));
|
|
edgeList = edgeList.concat(target.getEdgeListToNode(source));
|
|
|
|
if (!visited.has(edgeList[0]))
|
|
{
|
|
if (edgeList.length > 1)
|
|
{
|
|
var k;
|
|
for (k = 0; k < edgeList.length; k++)
|
|
{
|
|
var multiEdge = edgeList[k];
|
|
multiEdge.getBendpoints().push(new PointD());
|
|
this.createDummyNodesForBendpoints(multiEdge);
|
|
}
|
|
}
|
|
edgeList.forEach(function(edge){
|
|
visited.add(edge);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (visited.size == edges.length)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.positionNodesRadially = function (forest) {
|
|
// We tile the trees to a grid row by row; first tree starts at (0,0)
|
|
var currentStartingPoint = new Point(0, 0);
|
|
var numberOfColumns = Math.ceil(Math.sqrt(forest.length));
|
|
var height = 0;
|
|
var currentY = 0;
|
|
var currentX = 0;
|
|
var point = new PointD(0, 0);
|
|
|
|
for (var i = 0; i < forest.length; i++)
|
|
{
|
|
if (i % numberOfColumns == 0)
|
|
{
|
|
// Start of a new row, make the x coordinate 0, increment the
|
|
// y coordinate with the max height of the previous row
|
|
currentX = 0;
|
|
currentY = height;
|
|
|
|
if (i != 0)
|
|
{
|
|
currentY += CoSEConstants.DEFAULT_COMPONENT_SEPERATION;
|
|
}
|
|
|
|
height = 0;
|
|
}
|
|
|
|
var tree = forest[i];
|
|
|
|
// Find the center of the tree
|
|
var centerNode = Layout.findCenterOfTree(tree);
|
|
|
|
// Set the staring point of the next tree
|
|
currentStartingPoint.x = currentX;
|
|
currentStartingPoint.y = currentY;
|
|
|
|
// Do a radial layout starting with the center
|
|
point =
|
|
CoSELayout.radialLayout(tree, centerNode, currentStartingPoint);
|
|
|
|
if (point.y > height)
|
|
{
|
|
height = Math.floor(point.y);
|
|
}
|
|
|
|
currentX = Math.floor(point.x + CoSEConstants.DEFAULT_COMPONENT_SEPERATION);
|
|
}
|
|
|
|
this.transform(
|
|
new PointD(LayoutConstants.WORLD_CENTER_X - point.x / 2,
|
|
LayoutConstants.WORLD_CENTER_Y - point.y / 2));
|
|
};
|
|
|
|
CoSELayout.radialLayout = function (tree, centerNode, startingPoint) {
|
|
var radialSep = Math.max(this.maxDiagonalInTree(tree),
|
|
CoSEConstants.DEFAULT_RADIAL_SEPARATION);
|
|
CoSELayout.branchRadialLayout(centerNode, null, 0, 359, 0, radialSep);
|
|
var bounds = LGraph.calculateBounds(tree);
|
|
|
|
var transform = new Transform();
|
|
transform.setDeviceOrgX(bounds.getMinX());
|
|
transform.setDeviceOrgY(bounds.getMinY());
|
|
transform.setWorldOrgX(startingPoint.x);
|
|
transform.setWorldOrgY(startingPoint.y);
|
|
|
|
for (var i = 0; i < tree.length; i++)
|
|
{
|
|
var node = tree[i];
|
|
node.transform(transform);
|
|
}
|
|
|
|
var bottomRight =
|
|
new PointD(bounds.getMaxX(), bounds.getMaxY());
|
|
|
|
return transform.inverseTransformPoint(bottomRight);
|
|
};
|
|
|
|
CoSELayout.branchRadialLayout = function (node, parentOfNode, startAngle, endAngle, distance, radialSeparation) {
|
|
// First, position this node by finding its angle.
|
|
var halfInterval = ((endAngle - startAngle) + 1) / 2;
|
|
|
|
if (halfInterval < 0)
|
|
{
|
|
halfInterval += 180;
|
|
}
|
|
|
|
var nodeAngle = (halfInterval + startAngle) % 360;
|
|
var teta = (nodeAngle * IGeometry.TWO_PI) / 360;
|
|
|
|
// Make polar to java cordinate conversion.
|
|
var cos_teta = Math.cos(teta);
|
|
var x_ = distance * Math.cos(teta);
|
|
var y_ = distance * Math.sin(teta);
|
|
|
|
node.setCenter(x_, y_);
|
|
|
|
// Traverse all neighbors of this node and recursively call this
|
|
// function.
|
|
var neighborEdges = [];
|
|
neighborEdges = neighborEdges.concat(node.getEdges());
|
|
var childCount = neighborEdges.length;
|
|
|
|
if (parentOfNode != null)
|
|
{
|
|
childCount--;
|
|
}
|
|
|
|
var branchCount = 0;
|
|
|
|
var incEdgesCount = neighborEdges.length;
|
|
var startIndex;
|
|
|
|
var edges = node.getEdgesBetween(parentOfNode);
|
|
|
|
// If there are multiple edges, prune them until there remains only one
|
|
// edge.
|
|
while (edges.length > 1)
|
|
{
|
|
//neighborEdges.remove(edges.remove(0));
|
|
var temp = edges[0];
|
|
edges.splice(0, 1);
|
|
var index = neighborEdges.indexOf(temp);
|
|
if (index >= 0) {
|
|
neighborEdges.splice(index, 1);
|
|
}
|
|
incEdgesCount--;
|
|
childCount--;
|
|
}
|
|
|
|
if (parentOfNode != null)
|
|
{
|
|
//assert edges.length == 1;
|
|
startIndex = (neighborEdges.indexOf(edges[0]) + 1) % incEdgesCount;
|
|
}
|
|
else
|
|
{
|
|
startIndex = 0;
|
|
}
|
|
|
|
var stepAngle = Math.abs(endAngle - startAngle) / childCount;
|
|
|
|
for (var i = startIndex;
|
|
branchCount != childCount;
|
|
i = (++i) % incEdgesCount)
|
|
{
|
|
var currentNeighbor =
|
|
neighborEdges[i].getOtherEnd(node);
|
|
|
|
// Don't back traverse to root node in current tree.
|
|
if (currentNeighbor == parentOfNode)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var childStartAngle =
|
|
(startAngle + branchCount * stepAngle) % 360;
|
|
var childEndAngle = (childStartAngle + stepAngle) % 360;
|
|
|
|
CoSELayout.branchRadialLayout(currentNeighbor,
|
|
node,
|
|
childStartAngle, childEndAngle,
|
|
distance + radialSeparation, radialSeparation);
|
|
|
|
branchCount++;
|
|
}
|
|
};
|
|
|
|
CoSELayout.maxDiagonalInTree = function (tree) {
|
|
var maxDiagonal = Integer.MIN_VALUE;
|
|
|
|
for (var i = 0; i < tree.length; i++)
|
|
{
|
|
var node = tree[i];
|
|
var diagonal = node.getDiagonal();
|
|
|
|
if (diagonal > maxDiagonal)
|
|
{
|
|
maxDiagonal = diagonal;
|
|
}
|
|
}
|
|
|
|
return maxDiagonal;
|
|
};
|
|
|
|
CoSELayout.prototype.calcRepulsionRange = function () {
|
|
// formula is 2 x (level + 1) x idealEdgeLength
|
|
return (2 * (this.level + 1) * this.idealEdgeLength);
|
|
};
|
|
|
|
// Tiling methods
|
|
|
|
// Group zero degree members whose parents are not to be tiled, create dummy parents where needed and fill memberGroups by their dummp parent id's
|
|
CoSELayout.prototype.groupZeroDegreeMembers = function () {
|
|
var self = this;
|
|
// array of [parent_id x oneDegreeNode_id]
|
|
var tempMemberGroups = {}; // A temporary map of parent node and its zero degree members
|
|
this.memberGroups = {}; // A map of dummy parent node and its zero degree members whose parents are not to be tiled
|
|
this.idToDummyNode = {}; // A map of id to dummy node
|
|
|
|
var zeroDegree = []; // List of zero degree nodes whose parents are not to be tiled
|
|
var allNodes = this.graphManager.getAllNodes();
|
|
|
|
// Fill zero degree list
|
|
for (var i = 0; i < allNodes.length; i++) {
|
|
var node = allNodes[i];
|
|
var parent = node.getParent();
|
|
// If a node has zero degree and its parent is not to be tiled if exists add that node to zeroDegres list
|
|
if (this.getNodeDegreeWithChildren(node) === 0 && ( parent.id == undefined || !this.getToBeTiled(parent) ) ) {
|
|
zeroDegree.push(node);
|
|
}
|
|
}
|
|
|
|
// Create a map of parent node and its zero degree members
|
|
for (var i = 0; i < zeroDegree.length; i++)
|
|
{
|
|
var node = zeroDegree[i]; // Zero degree node itself
|
|
var p_id = node.getParent().id; // Parent id
|
|
|
|
if (typeof tempMemberGroups[p_id] === "undefined")
|
|
tempMemberGroups[p_id] = [];
|
|
|
|
tempMemberGroups[p_id] = tempMemberGroups[p_id].concat(node); // Push node to the list belongs to its parent in tempMemberGroups
|
|
}
|
|
|
|
// If there are at least two nodes at a level, create a dummy compound for them
|
|
Object.keys(tempMemberGroups).forEach(function(p_id) {
|
|
if (tempMemberGroups[p_id].length > 1) {
|
|
var dummyCompoundId = "DummyCompound_" + p_id; // The id of dummy compound which will be created soon
|
|
self.memberGroups[dummyCompoundId] = tempMemberGroups[p_id]; // Add dummy compound to memberGroups
|
|
|
|
var parent = tempMemberGroups[p_id][0].getParent(); // The parent of zero degree nodes will be the parent of new dummy compound
|
|
|
|
// Create a dummy compound with calculated id
|
|
var dummyCompound = new CoSENode(self.graphManager);
|
|
dummyCompound.id = dummyCompoundId;
|
|
dummyCompound.paddingLeft = parent.paddingLeft || 0;
|
|
dummyCompound.paddingRight = parent.paddingRight || 0;
|
|
dummyCompound.paddingBottom = parent.paddingBottom || 0;
|
|
dummyCompound.paddingTop = parent.paddingTop || 0;
|
|
|
|
self.idToDummyNode[dummyCompoundId] = dummyCompound;
|
|
|
|
var dummyParentGraph = self.getGraphManager().add(self.newGraph(), dummyCompound);
|
|
var parentGraph = parent.getChild();
|
|
|
|
// Add dummy compound to parent the graph
|
|
parentGraph.add(dummyCompound);
|
|
|
|
// For each zero degree node in this level remove it from its parent graph and add it to the graph of dummy parent
|
|
for (var i = 0; i < tempMemberGroups[p_id].length; i++) {
|
|
var node = tempMemberGroups[p_id][i];
|
|
|
|
parentGraph.remove(node);
|
|
dummyParentGraph.add(node);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
CoSELayout.prototype.clearCompounds = function () {
|
|
var childGraphMap = {};
|
|
var idToNode = {};
|
|
|
|
// Get compound ordering by finding the inner one first
|
|
this.performDFSOnCompounds();
|
|
|
|
for (var i = 0; i < this.compoundOrder.length; i++) {
|
|
|
|
idToNode[this.compoundOrder[i].id] = this.compoundOrder[i];
|
|
childGraphMap[this.compoundOrder[i].id] = [].concat(this.compoundOrder[i].getChild().getNodes());
|
|
|
|
// Remove children of compounds
|
|
this.graphManager.remove(this.compoundOrder[i].getChild());
|
|
this.compoundOrder[i].child = null;
|
|
}
|
|
|
|
this.graphManager.resetAllNodes();
|
|
|
|
// Tile the removed children
|
|
this.tileCompoundMembers(childGraphMap, idToNode);
|
|
};
|
|
|
|
CoSELayout.prototype.clearZeroDegreeMembers = function () {
|
|
var self = this;
|
|
var tiledZeroDegreePack = this.tiledZeroDegreePack = [];
|
|
|
|
Object.keys(this.memberGroups).forEach(function(id) {
|
|
var compoundNode = self.idToDummyNode[id]; // Get the dummy compound
|
|
|
|
tiledZeroDegreePack[id] = self.tileNodes(self.memberGroups[id], compoundNode.paddingLeft + compoundNode.paddingRight);
|
|
|
|
// Set the width and height of the dummy compound as calculated
|
|
compoundNode.rect.width = tiledZeroDegreePack[id].width;
|
|
compoundNode.rect.height = tiledZeroDegreePack[id].height;
|
|
});
|
|
};
|
|
|
|
CoSELayout.prototype.repopulateCompounds = function () {
|
|
for (var i = this.compoundOrder.length - 1; i >= 0; i--) {
|
|
var lCompoundNode = this.compoundOrder[i];
|
|
var id = lCompoundNode.id;
|
|
var horizontalMargin = lCompoundNode.paddingLeft;
|
|
var verticalMargin = lCompoundNode.paddingTop;
|
|
|
|
this.adjustLocations(this.tiledMemberPack[id], lCompoundNode.rect.x, lCompoundNode.rect.y, horizontalMargin, verticalMargin);
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.repopulateZeroDegreeMembers = function () {
|
|
var self = this;
|
|
var tiledPack = this.tiledZeroDegreePack;
|
|
|
|
Object.keys(tiledPack).forEach(function(id) {
|
|
var compoundNode = self.idToDummyNode[id]; // Get the dummy compound by its id
|
|
var horizontalMargin = compoundNode.paddingLeft;
|
|
var verticalMargin = compoundNode.paddingTop;
|
|
|
|
// Adjust the positions of nodes wrt its compound
|
|
self.adjustLocations(tiledPack[id], compoundNode.rect.x, compoundNode.rect.y, horizontalMargin, verticalMargin);
|
|
});
|
|
};
|
|
|
|
CoSELayout.prototype.getToBeTiled = function (node) {
|
|
var id = node.id;
|
|
//firstly check the previous results
|
|
if (this.toBeTiled[id] != null) {
|
|
return this.toBeTiled[id];
|
|
}
|
|
|
|
//only compound nodes are to be tiled
|
|
var childGraph = node.getChild();
|
|
if (childGraph == null) {
|
|
this.toBeTiled[id] = false;
|
|
return false;
|
|
}
|
|
|
|
var children = childGraph.getNodes(); // Get the children nodes
|
|
|
|
//a compound node is not to be tiled if all of its compound children are not to be tiled
|
|
for (var i = 0; i < children.length; i++) {
|
|
var theChild = children[i];
|
|
|
|
if (this.getNodeDegree(theChild) > 0) {
|
|
this.toBeTiled[id] = false;
|
|
return false;
|
|
}
|
|
|
|
//pass the children not having the compound structure
|
|
if (theChild.getChild() == null) {
|
|
this.toBeTiled[theChild.id] = false;
|
|
continue;
|
|
}
|
|
|
|
if (!this.getToBeTiled(theChild)) {
|
|
this.toBeTiled[id] = false;
|
|
return false;
|
|
}
|
|
}
|
|
this.toBeTiled[id] = true;
|
|
return true;
|
|
};
|
|
|
|
// Get degree of a node depending of its edges and independent of its children
|
|
CoSELayout.prototype.getNodeDegree = function (node) {
|
|
var id = node.id;
|
|
var edges = node.getEdges();
|
|
var degree = 0;
|
|
|
|
// For the edges connected
|
|
for (var i = 0; i < edges.length; i++) {
|
|
var edge = edges[i];
|
|
if (edge.getSource().id !== edge.getTarget().id) {
|
|
degree = degree + 1;
|
|
}
|
|
}
|
|
return degree;
|
|
};
|
|
|
|
// Get degree of a node with its children
|
|
CoSELayout.prototype.getNodeDegreeWithChildren = function (node) {
|
|
var degree = this.getNodeDegree(node);
|
|
if (node.getChild() == null) {
|
|
return degree;
|
|
}
|
|
var children = node.getChild().getNodes();
|
|
for (var i = 0; i < children.length; i++) {
|
|
var child = children[i];
|
|
degree += this.getNodeDegreeWithChildren(child);
|
|
}
|
|
return degree;
|
|
};
|
|
|
|
CoSELayout.prototype.performDFSOnCompounds = function () {
|
|
this.compoundOrder = [];
|
|
this.fillCompexOrderByDFS(this.graphManager.getRoot().getNodes());
|
|
};
|
|
|
|
CoSELayout.prototype.fillCompexOrderByDFS = function (children) {
|
|
for (var i = 0; i < children.length; i++) {
|
|
var child = children[i];
|
|
if (child.getChild() != null) {
|
|
this.fillCompexOrderByDFS(child.getChild().getNodes());
|
|
}
|
|
if (this.getToBeTiled(child)) {
|
|
this.compoundOrder.push(child);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method places each zero degree member wrt given (x,y) coordinates (top left).
|
|
*/
|
|
CoSELayout.prototype.adjustLocations = function (organization, x, y, compoundHorizontalMargin, compoundVerticalMargin) {
|
|
x += compoundHorizontalMargin;
|
|
y += compoundVerticalMargin;
|
|
|
|
var left = x;
|
|
|
|
for (var i = 0; i < organization.rows.length; i++) {
|
|
var row = organization.rows[i];
|
|
x = left;
|
|
var maxHeight = 0;
|
|
|
|
for (var j = 0; j < row.length; j++) {
|
|
var lnode = row[j];
|
|
|
|
lnode.rect.x = x;// + lnode.rect.width / 2;
|
|
lnode.rect.y = y;// + lnode.rect.height / 2;
|
|
|
|
x += lnode.rect.width + organization.horizontalPadding;
|
|
|
|
if (lnode.rect.height > maxHeight)
|
|
maxHeight = lnode.rect.height;
|
|
}
|
|
|
|
y += maxHeight + organization.verticalPadding;
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.tileCompoundMembers = function (childGraphMap, idToNode) {
|
|
var self = this;
|
|
this.tiledMemberPack = [];
|
|
|
|
Object.keys(childGraphMap).forEach(function(id) {
|
|
// Get the compound node
|
|
var compoundNode = idToNode[id];
|
|
|
|
self.tiledMemberPack[id] = self.tileNodes(childGraphMap[id], compoundNode.paddingLeft + compoundNode.paddingRight);
|
|
|
|
compoundNode.rect.width = self.tiledMemberPack[id].width;
|
|
compoundNode.rect.height = self.tiledMemberPack[id].height;
|
|
});
|
|
};
|
|
|
|
CoSELayout.prototype.tileNodes = function (nodes, minWidth) {
|
|
var verticalPadding = CoSEConstants.TILING_PADDING_VERTICAL;
|
|
var horizontalPadding = CoSEConstants.TILING_PADDING_HORIZONTAL;
|
|
var organization = {
|
|
rows: [],
|
|
rowWidth: [],
|
|
rowHeight: [],
|
|
width: 0,
|
|
height: minWidth, // assume minHeight equals to minWidth
|
|
verticalPadding: verticalPadding,
|
|
horizontalPadding: horizontalPadding
|
|
};
|
|
|
|
// Sort the nodes in ascending order of their areas
|
|
nodes.sort(function (n1, n2) {
|
|
if (n1.rect.width * n1.rect.height > n2.rect.width * n2.rect.height)
|
|
return -1;
|
|
if (n1.rect.width * n1.rect.height < n2.rect.width * n2.rect.height)
|
|
return 1;
|
|
return 0;
|
|
});
|
|
|
|
// Create the organization -> tile members
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var lNode = nodes[i];
|
|
|
|
if (organization.rows.length == 0) {
|
|
this.insertNodeToRow(organization, lNode, 0, minWidth);
|
|
}
|
|
else if (this.canAddHorizontal(organization, lNode.rect.width, lNode.rect.height)) {
|
|
this.insertNodeToRow(organization, lNode, this.getShortestRowIndex(organization), minWidth);
|
|
}
|
|
else {
|
|
this.insertNodeToRow(organization, lNode, organization.rows.length, minWidth);
|
|
}
|
|
|
|
this.shiftToLastRow(organization);
|
|
}
|
|
|
|
return organization;
|
|
};
|
|
|
|
CoSELayout.prototype.insertNodeToRow = function (organization, node, rowIndex, minWidth) {
|
|
var minCompoundSize = minWidth;
|
|
|
|
// Add new row if needed
|
|
if (rowIndex == organization.rows.length) {
|
|
var secondDimension = [];
|
|
|
|
organization.rows.push(secondDimension);
|
|
organization.rowWidth.push(minCompoundSize);
|
|
organization.rowHeight.push(0);
|
|
}
|
|
|
|
// Update row width
|
|
var w = organization.rowWidth[rowIndex] + node.rect.width;
|
|
|
|
if (organization.rows[rowIndex].length > 0) {
|
|
w += organization.horizontalPadding;
|
|
}
|
|
|
|
organization.rowWidth[rowIndex] = w;
|
|
// Update compound width
|
|
if (organization.width < w) {
|
|
organization.width = w;
|
|
}
|
|
|
|
// Update height
|
|
var h = node.rect.height;
|
|
if (rowIndex > 0)
|
|
h += organization.verticalPadding;
|
|
|
|
var extraHeight = 0;
|
|
if (h > organization.rowHeight[rowIndex]) {
|
|
extraHeight = organization.rowHeight[rowIndex];
|
|
organization.rowHeight[rowIndex] = h;
|
|
extraHeight = organization.rowHeight[rowIndex] - extraHeight;
|
|
}
|
|
|
|
organization.height += extraHeight;
|
|
|
|
// Insert node
|
|
organization.rows[rowIndex].push(node);
|
|
};
|
|
|
|
//Scans the rows of an organization and returns the one with the min width
|
|
CoSELayout.prototype.getShortestRowIndex = function (organization) {
|
|
var r = -1;
|
|
var min = Number.MAX_VALUE;
|
|
|
|
for (var i = 0; i < organization.rows.length; i++) {
|
|
if (organization.rowWidth[i] < min) {
|
|
r = i;
|
|
min = organization.rowWidth[i];
|
|
}
|
|
}
|
|
return r;
|
|
};
|
|
|
|
//Scans the rows of an organization and returns the one with the max width
|
|
CoSELayout.prototype.getLongestRowIndex = function (organization) {
|
|
var r = -1;
|
|
var max = Number.MIN_VALUE;
|
|
|
|
for (var i = 0; i < organization.rows.length; i++) {
|
|
|
|
if (organization.rowWidth[i] > max) {
|
|
r = i;
|
|
max = organization.rowWidth[i];
|
|
}
|
|
}
|
|
|
|
return r;
|
|
};
|
|
|
|
/**
|
|
* This method checks whether adding extra width to the organization violates
|
|
* the aspect ratio(1) or not.
|
|
*/
|
|
CoSELayout.prototype.canAddHorizontal = function (organization, extraWidth, extraHeight) {
|
|
|
|
var sri = this.getShortestRowIndex(organization);
|
|
|
|
if (sri < 0) {
|
|
return true;
|
|
}
|
|
|
|
var min = organization.rowWidth[sri];
|
|
|
|
if (min + organization.horizontalPadding + extraWidth <= organization.width)
|
|
return true;
|
|
|
|
var hDiff = 0;
|
|
|
|
// Adding to an existing row
|
|
if (organization.rowHeight[sri] < extraHeight) {
|
|
if (sri > 0)
|
|
hDiff = extraHeight + organization.verticalPadding - organization.rowHeight[sri];
|
|
}
|
|
|
|
var add_to_row_ratio;
|
|
if (organization.width - min >= extraWidth + organization.horizontalPadding) {
|
|
add_to_row_ratio = (organization.height + hDiff) / (min + extraWidth + organization.horizontalPadding);
|
|
} else {
|
|
add_to_row_ratio = (organization.height + hDiff) / organization.width;
|
|
}
|
|
|
|
// Adding a new row for this node
|
|
hDiff = extraHeight + organization.verticalPadding;
|
|
var add_new_row_ratio;
|
|
if (organization.width < extraWidth) {
|
|
add_new_row_ratio = (organization.height + hDiff) / extraWidth;
|
|
} else {
|
|
add_new_row_ratio = (organization.height + hDiff) / organization.width;
|
|
}
|
|
|
|
if (add_new_row_ratio < 1)
|
|
add_new_row_ratio = 1 / add_new_row_ratio;
|
|
|
|
if (add_to_row_ratio < 1)
|
|
add_to_row_ratio = 1 / add_to_row_ratio;
|
|
|
|
return add_to_row_ratio < add_new_row_ratio;
|
|
};
|
|
|
|
//If moving the last node from the longest row and adding it to the last
|
|
//row makes the bounding box smaller, do it.
|
|
CoSELayout.prototype.shiftToLastRow = function (organization) {
|
|
var longest = this.getLongestRowIndex(organization);
|
|
var last = organization.rowWidth.length - 1;
|
|
var row = organization.rows[longest];
|
|
var node = row[row.length - 1];
|
|
|
|
var diff = node.width + organization.horizontalPadding;
|
|
|
|
// Check if there is enough space on the last row
|
|
if (organization.width - organization.rowWidth[last] > diff && longest != last) {
|
|
// Remove the last element of the longest row
|
|
row.splice(-1, 1);
|
|
|
|
// Push it to the last row
|
|
organization.rows[last].push(node);
|
|
|
|
organization.rowWidth[longest] = organization.rowWidth[longest] - diff;
|
|
organization.rowWidth[last] = organization.rowWidth[last] + diff;
|
|
organization.width = organization.rowWidth[instance.getLongestRowIndex(organization)];
|
|
|
|
// Update heights of the organization
|
|
var maxHeight = Number.MIN_VALUE;
|
|
for (var i = 0; i < row.length; i++) {
|
|
if (row[i].height > maxHeight)
|
|
maxHeight = row[i].height;
|
|
}
|
|
if (longest > 0)
|
|
maxHeight += organization.verticalPadding;
|
|
|
|
var prevTotal = organization.rowHeight[longest] + organization.rowHeight[last];
|
|
|
|
organization.rowHeight[longest] = maxHeight;
|
|
if (organization.rowHeight[last] < node.height + organization.verticalPadding)
|
|
organization.rowHeight[last] = node.height + organization.verticalPadding;
|
|
|
|
var finalTotal = organization.rowHeight[longest] + organization.rowHeight[last];
|
|
organization.height += (finalTotal - prevTotal);
|
|
|
|
this.shiftToLastRow(organization);
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.tilingPreLayout = function() {
|
|
if (CoSEConstants.TILE) {
|
|
// Find zero degree nodes and create a compound for each level
|
|
this.groupZeroDegreeMembers();
|
|
// Tile and clear children of each compound
|
|
this.clearCompounds();
|
|
// Separately tile and clear zero degree nodes for each level
|
|
this.clearZeroDegreeMembers();
|
|
}
|
|
};
|
|
|
|
CoSELayout.prototype.tilingPostLayout = function() {
|
|
if (CoSEConstants.TILE) {
|
|
this.repopulateZeroDegreeMembers();
|
|
this.repopulateCompounds();
|
|
}
|
|
};
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Section: Tree Reduction methods
|
|
// -----------------------------------------------------------------------------
|
|
// Reduce trees
|
|
CoSELayout.prototype.reduceTrees = function ()
|
|
{
|
|
var prunedNodesAll = [];
|
|
var containsLeaf = true;
|
|
var node;
|
|
|
|
while(containsLeaf) {
|
|
var allNodes = this.graphManager.getAllNodes();
|
|
var prunedNodesInStepTemp = [];
|
|
containsLeaf = false;
|
|
|
|
for (var i = 0; i < allNodes.length; i++) {
|
|
node = allNodes[i];
|
|
if(node.getEdges().length == 1 && !node.getEdges()[0].isInterGraph && node.getChild() == null){
|
|
prunedNodesInStepTemp.push([node, node.getEdges()[0], node.getOwner()]);
|
|
containsLeaf = true;
|
|
}
|
|
}
|
|
if(containsLeaf == true){
|
|
var prunedNodesInStep = [];
|
|
for(var j = 0; j < prunedNodesInStepTemp.length; j++){
|
|
if(prunedNodesInStepTemp[j][0].getEdges().length == 1){
|
|
prunedNodesInStep.push(prunedNodesInStepTemp[j]);
|
|
prunedNodesInStepTemp[j][0].getOwner().remove(prunedNodesInStepTemp[j][0]);
|
|
}
|
|
}
|
|
prunedNodesAll.push(prunedNodesInStep);
|
|
this.graphManager.resetAllNodes();
|
|
this.graphManager.resetAllEdges();
|
|
}
|
|
}
|
|
this.prunedNodesAll = prunedNodesAll;
|
|
};
|
|
|
|
// Grow tree one step
|
|
CoSELayout.prototype.growTree = function(prunedNodesAll)
|
|
{
|
|
var lengthOfPrunedNodesInStep = prunedNodesAll.length;
|
|
var prunedNodesInStep = prunedNodesAll[lengthOfPrunedNodesInStep - 1];
|
|
|
|
var nodeData;
|
|
for(var i = 0; i < prunedNodesInStep.length; i++){
|
|
nodeData = prunedNodesInStep[i];
|
|
|
|
this.findPlaceforPrunedNode(nodeData);
|
|
|
|
nodeData[2].add(nodeData[0]);
|
|
nodeData[2].add(nodeData[1], nodeData[1].source, nodeData[1].target);
|
|
}
|
|
|
|
prunedNodesAll.splice(prunedNodesAll.length-1, 1);
|
|
this.graphManager.resetAllNodes();
|
|
this.graphManager.resetAllEdges();
|
|
};
|
|
|
|
// Find an appropriate position to replace pruned node, this method can be improved
|
|
CoSELayout.prototype.findPlaceforPrunedNode = function(nodeData){
|
|
|
|
var gridForPrunedNode;
|
|
var nodeToConnect;
|
|
var prunedNode = nodeData[0];
|
|
if(prunedNode == nodeData[1].source){
|
|
nodeToConnect = nodeData[1].target;
|
|
}
|
|
else {
|
|
nodeToConnect = nodeData[1].source;
|
|
}
|
|
var startGridX = nodeToConnect.startX;
|
|
var finishGridX = nodeToConnect.finishX;
|
|
var startGridY = nodeToConnect.startY;
|
|
var finishGridY = nodeToConnect.finishY;
|
|
|
|
var upNodeCount = 0;
|
|
var downNodeCount = 0;
|
|
var rightNodeCount = 0;
|
|
var leftNodeCount = 0;
|
|
var controlRegions = [upNodeCount, rightNodeCount, downNodeCount, leftNodeCount]
|
|
|
|
if(startGridY > 0){
|
|
for(var i = startGridX; i <= finishGridX; i++ ){
|
|
controlRegions[0] += (this.grid[i][startGridY - 1].length + this.grid[i][startGridY].length - 1);
|
|
}
|
|
}
|
|
if(finishGridX < this.grid.length - 1){
|
|
for(var i = startGridY; i <= finishGridY; i++ ){
|
|
controlRegions[1] += (this.grid[finishGridX + 1][i].length + this.grid[finishGridX][i].length - 1);
|
|
}
|
|
}
|
|
if(finishGridY < this.grid[0].length - 1){
|
|
for(var i = startGridX; i <= finishGridX; i++ ){
|
|
controlRegions[2] += (this.grid[i][finishGridY + 1].length + this.grid[i][finishGridY].length - 1);
|
|
}
|
|
}
|
|
if(startGridX > 0){
|
|
for(var i = startGridY; i <= finishGridY; i++ ){
|
|
controlRegions[3] += (this.grid[startGridX - 1][i].length + this.grid[startGridX][i].length - 1);
|
|
}
|
|
}
|
|
var min = Integer.MAX_VALUE;
|
|
var minCount;
|
|
var minIndex;
|
|
for(var j = 0; j < controlRegions.length; j++){
|
|
if(controlRegions[j] < min){
|
|
min = controlRegions[j];
|
|
minCount = 1;
|
|
minIndex = j;
|
|
}
|
|
else if(controlRegions[j] == min){
|
|
minCount++;
|
|
}
|
|
}
|
|
|
|
if(minCount == 3 && min == 0){
|
|
if(controlRegions[0] == 0 && controlRegions[1] == 0 && controlRegions[2] == 0){
|
|
gridForPrunedNode = 1;
|
|
}
|
|
else if(controlRegions[0] == 0 && controlRegions[1] == 0 && controlRegions[3] == 0){
|
|
gridForPrunedNode = 0;
|
|
}
|
|
else if(controlRegions[0] == 0 && controlRegions[2] == 0 && controlRegions[3] == 0){
|
|
gridForPrunedNode = 3;
|
|
}
|
|
else if(controlRegions[1] == 0 && controlRegions[2] == 0 && controlRegions[3] == 0){
|
|
gridForPrunedNode = 2;
|
|
}
|
|
}
|
|
else if(minCount == 2 && min == 0){
|
|
var random = Math.floor(Math.random() * 2);
|
|
if(controlRegions[0] == 0 && controlRegions[1] == 0){;
|
|
if(random == 0){
|
|
gridForPrunedNode = 0;
|
|
}
|
|
else{
|
|
gridForPrunedNode = 1;
|
|
}
|
|
}
|
|
else if(controlRegions[0] == 0 && controlRegions[2] == 0){
|
|
if(random == 0){
|
|
gridForPrunedNode = 0;
|
|
}
|
|
else{
|
|
gridForPrunedNode = 2;
|
|
}
|
|
}
|
|
else if(controlRegions[0] == 0 && controlRegions[3] == 0){
|
|
if(random == 0){
|
|
gridForPrunedNode = 0;
|
|
}
|
|
else{
|
|
gridForPrunedNode = 3;
|
|
}
|
|
}
|
|
else if(controlRegions[1] == 0 && controlRegions[2] == 0){
|
|
if(random == 0){
|
|
gridForPrunedNode = 1;
|
|
}
|
|
else{
|
|
gridForPrunedNode = 2;
|
|
}
|
|
}
|
|
else if(controlRegions[1] == 0 && controlRegions[3] == 0){
|
|
if(random == 0){
|
|
gridForPrunedNode = 1;
|
|
}
|
|
else{
|
|
gridForPrunedNode = 3;
|
|
}
|
|
}
|
|
else {
|
|
if(random == 0){
|
|
gridForPrunedNode = 2;
|
|
}
|
|
else{
|
|
gridForPrunedNode = 3;
|
|
}
|
|
}
|
|
}
|
|
else if(minCount == 4 && min == 0){
|
|
var random = Math.floor(Math.random() * 4);
|
|
gridForPrunedNode = random;
|
|
}
|
|
else {
|
|
gridForPrunedNode = minIndex;
|
|
}
|
|
|
|
if(gridForPrunedNode == 0) {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX(),
|
|
nodeToConnect.getCenterY() - nodeToConnect.getHeight()/2 - FDLayoutConstants.DEFAULT_EDGE_LENGTH - prunedNode.getHeight()/2);
|
|
}
|
|
else if(gridForPrunedNode == 1) {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX() + nodeToConnect.getWidth()/2 + FDLayoutConstants.DEFAULT_EDGE_LENGTH + prunedNode.getWidth()/2,
|
|
nodeToConnect.getCenterY());
|
|
}
|
|
else if(gridForPrunedNode == 2) {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX(),
|
|
nodeToConnect.getCenterY() + nodeToConnect.getHeight()/2 + FDLayoutConstants.DEFAULT_EDGE_LENGTH + prunedNode.getHeight()/2);
|
|
}
|
|
else {
|
|
prunedNode.setCenter(nodeToConnect.getCenterX() - nodeToConnect.getWidth()/2 - FDLayoutConstants.DEFAULT_EDGE_LENGTH - prunedNode.getWidth()/2,
|
|
nodeToConnect.getCenterY());
|
|
}
|
|
|
|
};
|
|
|
|
module.exports = CoSELayout;
|
|
|