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;