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.
672 lines
18 KiB
672 lines
18 KiB
var LayoutConstants = require('./LayoutConstants');
|
|
var LGraphManager = require('./LGraphManager');
|
|
var LNode = require('./LNode');
|
|
var LEdge = require('./LEdge');
|
|
var LGraph = require('./LGraph');
|
|
var PointD = require('./util/PointD');
|
|
var Transform = require('./util/Transform');
|
|
var Emitter = require('./util/Emitter');
|
|
|
|
function Layout(isRemoteUse) {
|
|
Emitter.call( this );
|
|
|
|
//Layout Quality: 0:draft, 1:default, 2:proof
|
|
this.layoutQuality = LayoutConstants.QUALITY;
|
|
//Whether layout should create bendpoints as needed or not
|
|
this.createBendsAsNeeded =
|
|
LayoutConstants.DEFAULT_CREATE_BENDS_AS_NEEDED;
|
|
//Whether layout should be incremental or not
|
|
this.incremental = LayoutConstants.DEFAULT_INCREMENTAL;
|
|
//Whether we animate from before to after layout node positions
|
|
this.animationOnLayout =
|
|
LayoutConstants.DEFAULT_ANIMATION_ON_LAYOUT;
|
|
//Whether we animate the layout process or not
|
|
this.animationDuringLayout = LayoutConstants.DEFAULT_ANIMATION_DURING_LAYOUT;
|
|
//Number iterations that should be done between two successive animations
|
|
this.animationPeriod = LayoutConstants.DEFAULT_ANIMATION_PERIOD;
|
|
/**
|
|
* Whether or not leaf nodes (non-compound nodes) are of uniform sizes. When
|
|
* they are, both spring and repulsion forces between two leaf nodes can be
|
|
* calculated without the expensive clipping point calculations, resulting
|
|
* in major speed-up.
|
|
*/
|
|
this.uniformLeafNodeSizes =
|
|
LayoutConstants.DEFAULT_UNIFORM_LEAF_NODE_SIZES;
|
|
/**
|
|
* This is used for creation of bendpoints by using dummy nodes and edges.
|
|
* Maps an LEdge to its dummy bendpoint path.
|
|
*/
|
|
this.edgeToDummyNodes = new Map();
|
|
this.graphManager = new LGraphManager(this);
|
|
this.isLayoutFinished = false;
|
|
this.isSubLayout = false;
|
|
this.isRemoteUse = false;
|
|
|
|
if (isRemoteUse != null) {
|
|
this.isRemoteUse = isRemoteUse;
|
|
}
|
|
}
|
|
|
|
Layout.RANDOM_SEED = 1;
|
|
|
|
Layout.prototype = Object.create( Emitter.prototype );
|
|
|
|
Layout.prototype.getGraphManager = function () {
|
|
return this.graphManager;
|
|
};
|
|
|
|
Layout.prototype.getAllNodes = function () {
|
|
return this.graphManager.getAllNodes();
|
|
};
|
|
|
|
Layout.prototype.getAllEdges = function () {
|
|
return this.graphManager.getAllEdges();
|
|
};
|
|
|
|
Layout.prototype.getAllNodesToApplyGravitation = function () {
|
|
return this.graphManager.getAllNodesToApplyGravitation();
|
|
};
|
|
|
|
Layout.prototype.newGraphManager = function () {
|
|
var gm = new LGraphManager(this);
|
|
this.graphManager = gm;
|
|
return gm;
|
|
};
|
|
|
|
Layout.prototype.newGraph = function (vGraph)
|
|
{
|
|
return new LGraph(null, this.graphManager, vGraph);
|
|
};
|
|
|
|
Layout.prototype.newNode = function (vNode)
|
|
{
|
|
return new LNode(this.graphManager, vNode);
|
|
};
|
|
|
|
Layout.prototype.newEdge = function (vEdge)
|
|
{
|
|
return new LEdge(null, null, vEdge);
|
|
};
|
|
|
|
Layout.prototype.checkLayoutSuccess = function() {
|
|
return (this.graphManager.getRoot() == null)
|
|
|| this.graphManager.getRoot().getNodes().length == 0
|
|
|| this.graphManager.includesInvalidEdge();
|
|
};
|
|
|
|
Layout.prototype.runLayout = function ()
|
|
{
|
|
this.isLayoutFinished = false;
|
|
|
|
if (this.tilingPreLayout) {
|
|
this.tilingPreLayout();
|
|
}
|
|
|
|
this.initParameters();
|
|
var isLayoutSuccessfull;
|
|
|
|
if (this.checkLayoutSuccess())
|
|
{
|
|
isLayoutSuccessfull = false;
|
|
}
|
|
else
|
|
{
|
|
isLayoutSuccessfull = this.layout();
|
|
}
|
|
|
|
if (LayoutConstants.ANIMATE === 'during') {
|
|
// If this is a 'during' layout animation. Layout is not finished yet.
|
|
// We need to perform these in index.js when layout is really finished.
|
|
return false;
|
|
}
|
|
|
|
if (isLayoutSuccessfull)
|
|
{
|
|
if (!this.isSubLayout)
|
|
{
|
|
this.doPostLayout();
|
|
}
|
|
}
|
|
|
|
if (this.tilingPostLayout) {
|
|
this.tilingPostLayout();
|
|
}
|
|
|
|
this.isLayoutFinished = true;
|
|
|
|
return isLayoutSuccessfull;
|
|
};
|
|
|
|
/**
|
|
* This method performs the operations required after layout.
|
|
*/
|
|
Layout.prototype.doPostLayout = function ()
|
|
{
|
|
//assert !isSubLayout : "Should not be called on sub-layout!";
|
|
// Propagate geometric changes to v-level objects
|
|
if(!this.incremental){
|
|
this.transform();
|
|
}
|
|
this.update();
|
|
};
|
|
|
|
/**
|
|
* This method updates the geometry of the target graph according to
|
|
* calculated layout.
|
|
*/
|
|
Layout.prototype.update2 = function () {
|
|
// update bend points
|
|
if (this.createBendsAsNeeded)
|
|
{
|
|
this.createBendpointsFromDummyNodes();
|
|
|
|
// reset all edges, since the topology has changed
|
|
this.graphManager.resetAllEdges();
|
|
}
|
|
|
|
// perform edge, node and root updates if layout is not called
|
|
// remotely
|
|
if (!this.isRemoteUse)
|
|
{
|
|
// update all edges
|
|
var edge;
|
|
var allEdges = this.graphManager.getAllEdges();
|
|
for (var i = 0; i < allEdges.length; i++)
|
|
{
|
|
edge = allEdges[i];
|
|
// this.update(edge);
|
|
}
|
|
|
|
// recursively update nodes
|
|
var node;
|
|
var nodes = this.graphManager.getRoot().getNodes();
|
|
for (var i = 0; i < nodes.length; i++)
|
|
{
|
|
node = nodes[i];
|
|
// this.update(node);
|
|
}
|
|
|
|
// update root graph
|
|
this.update(this.graphManager.getRoot());
|
|
}
|
|
};
|
|
|
|
Layout.prototype.update = function (obj) {
|
|
if (obj == null) {
|
|
this.update2();
|
|
}
|
|
else if (obj instanceof LNode) {
|
|
var node = obj;
|
|
if (node.getChild() != null)
|
|
{
|
|
// since node is compound, recursively update child nodes
|
|
var nodes = node.getChild().getNodes();
|
|
for (var i = 0; i < nodes.length; i++)
|
|
{
|
|
update(nodes[i]);
|
|
}
|
|
}
|
|
|
|
// if the l-level node is associated with a v-level graph object,
|
|
// then it is assumed that the v-level node implements the
|
|
// interface Updatable.
|
|
if (node.vGraphObject != null)
|
|
{
|
|
// cast to Updatable without any type check
|
|
var vNode = node.vGraphObject;
|
|
|
|
// call the update method of the interface
|
|
vNode.update(node);
|
|
}
|
|
}
|
|
else if (obj instanceof LEdge) {
|
|
var edge = obj;
|
|
// if the l-level edge is associated with a v-level graph object,
|
|
// then it is assumed that the v-level edge implements the
|
|
// interface Updatable.
|
|
|
|
if (edge.vGraphObject != null)
|
|
{
|
|
// cast to Updatable without any type check
|
|
var vEdge = edge.vGraphObject;
|
|
|
|
// call the update method of the interface
|
|
vEdge.update(edge);
|
|
}
|
|
}
|
|
else if (obj instanceof LGraph) {
|
|
var graph = obj;
|
|
// if the l-level graph is associated with a v-level graph object,
|
|
// then it is assumed that the v-level object implements the
|
|
// interface Updatable.
|
|
|
|
if (graph.vGraphObject != null)
|
|
{
|
|
// cast to Updatable without any type check
|
|
var vGraph = graph.vGraphObject;
|
|
|
|
// call the update method of the interface
|
|
vGraph.update(graph);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method is used to set all layout parameters to default values
|
|
* determined at compile time.
|
|
*/
|
|
Layout.prototype.initParameters = function () {
|
|
if (!this.isSubLayout)
|
|
{
|
|
this.layoutQuality = LayoutConstants.QUALITY;
|
|
this.animationDuringLayout = LayoutConstants.DEFAULT_ANIMATION_DURING_LAYOUT;
|
|
this.animationPeriod = LayoutConstants.DEFAULT_ANIMATION_PERIOD;
|
|
this.animationOnLayout = LayoutConstants.DEFAULT_ANIMATION_ON_LAYOUT;
|
|
this.incremental = LayoutConstants.DEFAULT_INCREMENTAL;
|
|
this.createBendsAsNeeded = LayoutConstants.DEFAULT_CREATE_BENDS_AS_NEEDED;
|
|
this.uniformLeafNodeSizes = LayoutConstants.DEFAULT_UNIFORM_LEAF_NODE_SIZES;
|
|
}
|
|
|
|
if (this.animationDuringLayout)
|
|
{
|
|
this.animationOnLayout = false;
|
|
}
|
|
};
|
|
|
|
Layout.prototype.transform = function (newLeftTop) {
|
|
if (newLeftTop == undefined) {
|
|
this.transform(new PointD(0, 0));
|
|
}
|
|
else {
|
|
// create a transformation object (from Eclipse to layout). When an
|
|
// inverse transform is applied, we get upper-left coordinate of the
|
|
// drawing or the root graph at given input coordinate (some margins
|
|
// already included in calculation of left-top).
|
|
|
|
var trans = new Transform();
|
|
var leftTop = this.graphManager.getRoot().updateLeftTop();
|
|
|
|
if (leftTop != null)
|
|
{
|
|
trans.setWorldOrgX(newLeftTop.x);
|
|
trans.setWorldOrgY(newLeftTop.y);
|
|
|
|
trans.setDeviceOrgX(leftTop.x);
|
|
trans.setDeviceOrgY(leftTop.y);
|
|
|
|
var nodes = this.getAllNodes();
|
|
var node;
|
|
|
|
for (var i = 0; i < nodes.length; i++)
|
|
{
|
|
node = nodes[i];
|
|
node.transform(trans);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Layout.prototype.positionNodesRandomly = function (graph) {
|
|
|
|
if (graph == undefined) {
|
|
//assert !this.incremental;
|
|
this.positionNodesRandomly(this.getGraphManager().getRoot());
|
|
this.getGraphManager().getRoot().updateBounds(true);
|
|
}
|
|
else {
|
|
var lNode;
|
|
var childGraph;
|
|
|
|
var nodes = graph.getNodes();
|
|
for (var i = 0; i < nodes.length; i++)
|
|
{
|
|
lNode = nodes[i];
|
|
childGraph = lNode.getChild();
|
|
|
|
if (childGraph == null)
|
|
{
|
|
lNode.scatter();
|
|
}
|
|
else if (childGraph.getNodes().length == 0)
|
|
{
|
|
lNode.scatter();
|
|
}
|
|
else
|
|
{
|
|
this.positionNodesRandomly(childGraph);
|
|
lNode.updateBounds();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method returns a list of trees where each tree is represented as a
|
|
* list of l-nodes. The method returns a list of size 0 when:
|
|
* - The graph is not flat or
|
|
* - One of the component(s) of the graph is not a tree.
|
|
*/
|
|
Layout.prototype.getFlatForest = function ()
|
|
{
|
|
var flatForest = [];
|
|
var isForest = true;
|
|
|
|
// Quick reference for all nodes in the graph manager associated with
|
|
// this layout. The list should not be changed.
|
|
var allNodes = this.graphManager.getRoot().getNodes();
|
|
|
|
// First be sure that the graph is flat
|
|
var isFlat = true;
|
|
|
|
for (var i = 0; i < allNodes.length; i++)
|
|
{
|
|
if (allNodes[i].getChild() != null)
|
|
{
|
|
isFlat = false;
|
|
}
|
|
}
|
|
|
|
// Return empty forest if the graph is not flat.
|
|
if (!isFlat)
|
|
{
|
|
return flatForest;
|
|
}
|
|
|
|
// Run BFS for each component of the graph.
|
|
|
|
var visited = new Set();
|
|
var toBeVisited = [];
|
|
var parents = new Map();
|
|
var unProcessedNodes = [];
|
|
|
|
unProcessedNodes = unProcessedNodes.concat(allNodes);
|
|
|
|
// Each iteration of this loop finds a component of the graph and
|
|
// decides whether it is a tree or not. If it is a tree, adds it to the
|
|
// forest and continued with the next component.
|
|
|
|
while (unProcessedNodes.length > 0 && isForest)
|
|
{
|
|
toBeVisited.push(unProcessedNodes[0]);
|
|
|
|
// Start the BFS. Each iteration of this loop visits a node in a
|
|
// BFS manner.
|
|
while (toBeVisited.length > 0 && isForest)
|
|
{
|
|
//pool operation
|
|
var currentNode = toBeVisited[0];
|
|
toBeVisited.splice(0, 1);
|
|
visited.add(currentNode);
|
|
|
|
// Traverse all neighbors of this node
|
|
var neighborEdges = currentNode.getEdges();
|
|
|
|
for (var i = 0; i < neighborEdges.length; i++)
|
|
{
|
|
var currentNeighbor =
|
|
neighborEdges[i].getOtherEnd(currentNode);
|
|
|
|
// If BFS is not growing from this neighbor.
|
|
if (parents.get(currentNode) != currentNeighbor)
|
|
{
|
|
// We haven't previously visited this neighbor.
|
|
if (!visited.has(currentNeighbor))
|
|
{
|
|
toBeVisited.push(currentNeighbor);
|
|
parents.set(currentNeighbor, currentNode);
|
|
}
|
|
// Since we have previously visited this neighbor and
|
|
// this neighbor is not parent of currentNode, given
|
|
// graph contains a component that is not tree, hence
|
|
// it is not a forest.
|
|
else
|
|
{
|
|
isForest = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The graph contains a component that is not a tree. Empty
|
|
// previously found trees. The method will end.
|
|
if (!isForest)
|
|
{
|
|
flatForest = [];
|
|
}
|
|
// Save currently visited nodes as a tree in our forest. Reset
|
|
// visited and parents lists. Continue with the next component of
|
|
// the graph, if any.
|
|
else
|
|
{
|
|
var temp = [...visited];
|
|
flatForest.push(temp);
|
|
//flatForest = flatForest.concat(temp);
|
|
//unProcessedNodes.removeAll(visited);
|
|
for (var i = 0; i < temp.length; i++) {
|
|
var value = temp[i];
|
|
var index = unProcessedNodes.indexOf(value);
|
|
if (index > -1) {
|
|
unProcessedNodes.splice(index, 1);
|
|
}
|
|
}
|
|
visited = new Set();
|
|
parents = new Map();
|
|
}
|
|
}
|
|
|
|
return flatForest;
|
|
};
|
|
|
|
/**
|
|
* This method creates dummy nodes (an l-level node with minimal dimensions)
|
|
* for the given edge (one per bendpoint). The existing l-level structure
|
|
* is updated accordingly.
|
|
*/
|
|
Layout.prototype.createDummyNodesForBendpoints = function (edge)
|
|
{
|
|
var dummyNodes = [];
|
|
var prev = edge.source;
|
|
|
|
var graph = this.graphManager.calcLowestCommonAncestor(edge.source, edge.target);
|
|
|
|
for (var i = 0; i < edge.bendpoints.length; i++)
|
|
{
|
|
// create new dummy node
|
|
var dummyNode = this.newNode(null);
|
|
dummyNode.setRect(new Point(0, 0), new Dimension(1, 1));
|
|
|
|
graph.add(dummyNode);
|
|
|
|
// create new dummy edge between prev and dummy node
|
|
var dummyEdge = this.newEdge(null);
|
|
this.graphManager.add(dummyEdge, prev, dummyNode);
|
|
|
|
dummyNodes.add(dummyNode);
|
|
prev = dummyNode;
|
|
}
|
|
|
|
var dummyEdge = this.newEdge(null);
|
|
this.graphManager.add(dummyEdge, prev, edge.target);
|
|
|
|
this.edgeToDummyNodes.set(edge, dummyNodes);
|
|
|
|
// remove real edge from graph manager if it is inter-graph
|
|
if (edge.isInterGraph())
|
|
{
|
|
this.graphManager.remove(edge);
|
|
}
|
|
// else, remove the edge from the current graph
|
|
else
|
|
{
|
|
graph.remove(edge);
|
|
}
|
|
|
|
return dummyNodes;
|
|
};
|
|
|
|
/**
|
|
* This method creates bendpoints for edges from the dummy nodes
|
|
* at l-level.
|
|
*/
|
|
Layout.prototype.createBendpointsFromDummyNodes = function ()
|
|
{
|
|
var edges = [];
|
|
edges = edges.concat(this.graphManager.getAllEdges());
|
|
edges = [...this.edgeToDummyNodes.keys()].concat(edges);
|
|
|
|
for (var k = 0; k < edges.length; k++)
|
|
{
|
|
var lEdge = edges[k];
|
|
|
|
if (lEdge.bendpoints.length > 0)
|
|
{
|
|
var path = this.edgeToDummyNodes.get(lEdge);
|
|
|
|
for (var i = 0; i < path.length; i++)
|
|
{
|
|
var dummyNode = path[i];
|
|
var p = new PointD(dummyNode.getCenterX(),
|
|
dummyNode.getCenterY());
|
|
|
|
// update bendpoint's location according to dummy node
|
|
var ebp = lEdge.bendpoints.get(i);
|
|
ebp.x = p.x;
|
|
ebp.y = p.y;
|
|
|
|
// remove the dummy node, dummy edges incident with this
|
|
// dummy node is also removed (within the remove method)
|
|
dummyNode.getOwner().remove(dummyNode);
|
|
}
|
|
|
|
// add the real edge to graph
|
|
this.graphManager.add(lEdge, lEdge.source, lEdge.target);
|
|
}
|
|
}
|
|
};
|
|
|
|
Layout.transform = function (sliderValue, defaultValue, minDiv, maxMul) {
|
|
if (minDiv != undefined && maxMul != undefined) {
|
|
var value = defaultValue;
|
|
|
|
if (sliderValue <= 50)
|
|
{
|
|
var minValue = defaultValue / minDiv;
|
|
value -= ((defaultValue - minValue) / 50) * (50 - sliderValue);
|
|
}
|
|
else
|
|
{
|
|
var maxValue = defaultValue * maxMul;
|
|
value += ((maxValue - defaultValue) / 50) * (sliderValue - 50);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
else {
|
|
var a, b;
|
|
|
|
if (sliderValue <= 50)
|
|
{
|
|
a = 9.0 * defaultValue / 500.0;
|
|
b = defaultValue / 10.0;
|
|
}
|
|
else
|
|
{
|
|
a = 9.0 * defaultValue / 50.0;
|
|
b = -8 * defaultValue;
|
|
}
|
|
|
|
return (a * sliderValue + b);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method finds and returns the center of the given nodes, assuming
|
|
* that the given nodes form a tree in themselves.
|
|
*/
|
|
Layout.findCenterOfTree = function (nodes)
|
|
{
|
|
var list = [];
|
|
list = list.concat(nodes);
|
|
|
|
var removedNodes = [];
|
|
var remainingDegrees = new Map();
|
|
var foundCenter = false;
|
|
var centerNode = null;
|
|
|
|
if (list.length == 1 || list.length == 2)
|
|
{
|
|
foundCenter = true;
|
|
centerNode = list[0];
|
|
}
|
|
|
|
for (var i = 0; i < list.length; i++)
|
|
{
|
|
var node = list[i];
|
|
var degree = node.getNeighborsList().size;
|
|
remainingDegrees.set(node, node.getNeighborsList().size);
|
|
|
|
if (degree == 1)
|
|
{
|
|
removedNodes.push(node);
|
|
}
|
|
}
|
|
|
|
var tempList = [];
|
|
tempList = tempList.concat(removedNodes);
|
|
|
|
while (!foundCenter)
|
|
{
|
|
var tempList2 = [];
|
|
tempList2 = tempList2.concat(tempList);
|
|
tempList = [];
|
|
|
|
for (var i = 0; i < list.length; i++)
|
|
{
|
|
var node = list[i];
|
|
|
|
var index = list.indexOf(node);
|
|
if (index >= 0) {
|
|
list.splice(index, 1);
|
|
}
|
|
|
|
var neighbours = node.getNeighborsList();
|
|
|
|
neighbours.forEach(function(neighbour) {
|
|
if (removedNodes.indexOf(neighbour) < 0)
|
|
{
|
|
var otherDegree = remainingDegrees.get(neighbour);
|
|
var newDegree = otherDegree - 1;
|
|
|
|
if (newDegree == 1)
|
|
{
|
|
tempList.push(neighbour);
|
|
}
|
|
|
|
remainingDegrees.set(neighbour, newDegree);
|
|
}
|
|
});
|
|
}
|
|
|
|
removedNodes = removedNodes.concat(tempList);
|
|
|
|
if (list.length == 1 || list.length == 2)
|
|
{
|
|
foundCenter = true;
|
|
centerNode = list[0];
|
|
}
|
|
}
|
|
|
|
return centerNode;
|
|
};
|
|
|
|
/**
|
|
* During the coarsening process, this layout may be referenced by two graph managers
|
|
* this setter function grants access to change the currently being used graph manager
|
|
*/
|
|
Layout.prototype.setGraphManager = function (gm)
|
|
{
|
|
this.graphManager = gm;
|
|
};
|
|
|
|
module.exports = Layout;
|
|
|