/** * Copyright (c) 2006-2018, JGraph Ltd * Copyright (c) 2006-2018, Gaudenz Alder */ /** * Class: mxHierarchicalLayout * * A hierarchical layout algorithm. * * Constructor: mxHierarchicalLayout * * Constructs a new hierarchical layout algorithm. * * Arguments: * * graph - Reference to the enclosing . * orientation - Optional constant that defines the orientation of this * layout. * deterministic - Optional boolean that specifies if this layout should be * deterministic. Default is true. */ function mxHierarchicalLayout(graph, orientation, deterministic) { mxGraphLayout.call(this, graph); this.orientation = (orientation != null) ? orientation : mxConstants.DIRECTION_NORTH; this.deterministic = (deterministic != null) ? deterministic : true; }; var mxHierarchicalEdgeStyle = { ORTHOGONAL: 1, POLYLINE: 2, STRAIGHT: 3, CURVE: 4 }; /** * Extends mxGraphLayout. */ mxHierarchicalLayout.prototype = new mxGraphLayout(); mxHierarchicalLayout.prototype.constructor = mxHierarchicalLayout; /** * Variable: roots * * Holds the array of that this layout contains. */ mxHierarchicalLayout.prototype.roots = null; /** * Variable: resizeParent * * Specifies if the parent should be resized after the layout so that it * contains all the child cells. Default is false. See also . */ mxHierarchicalLayout.prototype.resizeParent = false; /** * Variable: maintainParentLocation * * Specifies if the parent location should be maintained, so that the * top, left corner stays the same before and after execution of * the layout. Default is false for backwards compatibility. */ mxHierarchicalLayout.prototype.maintainParentLocation = false; /** * Variable: moveParent * * Specifies if the parent should be moved if is enabled. * Default is false. */ mxHierarchicalLayout.prototype.moveParent = false; /** * Variable: parentBorder * * The border to be added around the children if the parent is to be * resized using . Default is 0. */ mxHierarchicalLayout.prototype.parentBorder = 0; /** * Variable: intraCellSpacing * * The spacing buffer added between cells on the same layer. Default is 30. */ mxHierarchicalLayout.prototype.intraCellSpacing = 30; /** * Variable: interRankCellSpacing * * The spacing buffer added between cell on adjacent layers. Default is 100. */ mxHierarchicalLayout.prototype.interRankCellSpacing = 100; /** * Variable: interHierarchySpacing * * The spacing buffer between unconnected hierarchies. Default is 60. */ mxHierarchicalLayout.prototype.interHierarchySpacing = 60; /** * Variable: parallelEdgeSpacing * * The distance between each parallel edge on each ranks for long edges. * Default is 10. */ mxHierarchicalLayout.prototype.parallelEdgeSpacing = 10; /** * Variable: orientation * * The position of the root node(s) relative to the laid out graph in. * Default is . */ mxHierarchicalLayout.prototype.orientation = mxConstants.DIRECTION_NORTH; /** * Variable: fineTuning * * Whether or not to perform local optimisations and iterate multiple times * through the algorithm. Default is true. */ mxHierarchicalLayout.prototype.fineTuning = true; /** * * Variable: tightenToSource * * Whether or not to tighten the assigned ranks of vertices up towards * the source cells. Default is true. */ mxHierarchicalLayout.prototype.tightenToSource = true; /** * Variable: disableEdgeStyle * * Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are * modified by the result. Default is true. */ mxHierarchicalLayout.prototype.disableEdgeStyle = true; /** * Variable: traverseAncestors * * Whether or not to drill into child cells and layout in reverse * group order. This also cause the layout to navigate edges whose * terminal vertices have different parents but are in the same * ancestry chain. Default is true. */ mxHierarchicalLayout.prototype.traverseAncestors = true; /** * Variable: model * * The internal formed of the layout. */ mxHierarchicalLayout.prototype.model = null; /** * Variable: edgesSet * * A cache of edges whose source terminal is the key */ mxHierarchicalLayout.prototype.edgesCache = null; /** * Variable: edgesSet * * A cache of edges whose source terminal is the key */ mxHierarchicalLayout.prototype.edgeSourceTermCache = null; /** * Variable: edgesSet * * A cache of edges whose source terminal is the key */ mxHierarchicalLayout.prototype.edgesTargetTermCache = null; /** * Variable: edgeStyle * * The style to apply between cell layers to edge segments. * Default is . */ mxHierarchicalLayout.prototype.edgeStyle = mxHierarchicalEdgeStyle.POLYLINE; /** * Function: getModel * * Returns the internal for this layout algorithm. */ mxHierarchicalLayout.prototype.getModel = function() { return this.model; }; /** * Function: execute * * Executes the layout for the children of the specified parent. * * Parameters: * * parent - Parent that contains the children to be laid out. * roots - Optional starting roots of the layout. */ mxHierarchicalLayout.prototype.execute = function(parent, roots) { this.parent = parent; var model = this.graph.model; this.edgesCache = new mxDictionary(); this.edgeSourceTermCache = new mxDictionary(); this.edgesTargetTermCache = new mxDictionary(); if (roots != null && !(roots instanceof Array)) { roots = [roots]; } // If the roots are set and the parent is set, only // use the roots that are some dependent of the that // parent. // If just the root are set, use them as-is // If just the parent is set use it's immediate // children as the initial set if (roots == null && parent == null) { // TODO indicate the problem return; } // Maintaining parent location this.parentX = null; this.parentY = null; if (parent != this.root && model.isVertex(parent) != null && this.maintainParentLocation) { var geo = this.graph.getCellGeometry(parent); if (geo != null) { this.parentX = geo.x; this.parentY = geo.y; } } if (roots != null) { var rootsCopy = []; for (var i = 0; i < roots.length; i++) { var ancestor = parent != null ? model.isAncestor(parent, roots[i]) : true; if (ancestor && model.isVertex(roots[i])) { rootsCopy.push(roots[i]); } } this.roots = rootsCopy; } model.beginUpdate(); try { this.run(parent); if (this.resizeParent && !this.graph.isCellCollapsed(parent)) { this.graph.updateGroupBounds([parent], this.parentBorder, this.moveParent); } // Maintaining parent location if (this.parentX != null && this.parentY != null) { var geo = this.graph.getCellGeometry(parent); if (geo != null) { geo = geo.clone(); geo.x = this.parentX; geo.y = this.parentY; model.setGeometry(parent, geo); } } } finally { model.endUpdate(); } }; /** * Function: findRoots * * Returns all visible children in the given parent which do not have * incoming edges. If the result is empty then the children with the * maximum difference between incoming and outgoing edges are returned. * This takes into account edges that are being promoted to the given * root due to invisible children or collapsed cells. * * Parameters: * * parent - whose children should be checked. * vertices - array of vertices to limit search to */ mxHierarchicalLayout.prototype.findRoots = function(parent, vertices) { var roots = []; if (parent != null && vertices != null) { var model = this.graph.model; var best = null; var maxDiff = -100000; for (var i in vertices) { var cell = vertices[i]; if (model.isVertex(cell) && this.graph.isCellVisible(cell)) { var conns = this.getEdges(cell); var fanOut = 0; var fanIn = 0; for (var k = 0; k < conns.length; k++) { var src = this.getVisibleTerminal(conns[k], true); if (src == cell) { fanOut++; } else { fanIn++; } } if (fanIn == 0 && fanOut > 0) { roots.push(cell); } var diff = fanOut - fanIn; if (diff > maxDiff) { maxDiff = diff; best = cell; } } } if (roots.length == 0 && best != null) { roots.push(best); } } return roots; }; /** * Function: getEdges * * Returns the connected edges for the given cell. * * Parameters: * * cell - whose edges should be returned. */ mxHierarchicalLayout.prototype.getEdges = function(cell) { var cachedEdges = this.edgesCache.get(cell); if (cachedEdges != null) { return cachedEdges; } var model = this.graph.model; var edges = []; var isCollapsed = this.graph.isCellCollapsed(cell); var childCount = model.getChildCount(cell); for (var i = 0; i < childCount; i++) { var child = model.getChildAt(cell, i); if (this.isPort(child)) { edges = edges.concat(model.getEdges(child, true, true)); } else if (isCollapsed || !this.graph.isCellVisible(child)) { edges = edges.concat(model.getEdges(child, true, true)); } } edges = edges.concat(model.getEdges(cell, true, true)); var result = []; for (var i = 0; i < edges.length; i++) { var source = this.getVisibleTerminal(edges[i], true); var target = this.getVisibleTerminal(edges[i], false); if ((source == target) || ((source != target) && ((target == cell && (this.parent == null || this.isAncestor(this.parent, source, this.traverseAncestors))) || (source == cell && (this.parent == null || this.isAncestor(this.parent, target, this.traverseAncestors)))))) { result.push(edges[i]); } } this.edgesCache.put(cell, result); return result; }; /** * Function: getVisibleTerminal * * Helper function to return visible terminal for edge allowing for ports * * Parameters: * * edge - whose edges should be returned. * source - Boolean that specifies whether the source or target terminal is to be returned */ mxHierarchicalLayout.prototype.getVisibleTerminal = function(edge, source) { var terminalCache = this.edgesTargetTermCache; if (source) { terminalCache = this.edgeSourceTermCache; } var term = terminalCache.get(edge); if (term != null) { return term; } var state = this.graph.view.getState(edge); var terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source); if (terminal == null) { terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source); } if (terminal != null) { if (this.isPort(terminal)) { terminal = this.graph.model.getParent(terminal); } terminalCache.put(edge, terminal); } return terminal; }; /** * Function: run * * The API method used to exercise the layout upon the graph description * and produce a separate description of the vertex position and edge * routing changes made. It runs each stage of the layout that has been * created. */ mxHierarchicalLayout.prototype.run = function(parent) { // Separate out unconnected hierarchies var hierarchyVertices = []; var allVertexSet = []; if (this.roots == null && parent != null) { var filledVertexSet = Object(); this.filterDescendants(parent, filledVertexSet); this.roots = []; var filledVertexSetEmpty = true; // Poor man's isSetEmpty for (var key in filledVertexSet) { if (filledVertexSet[key] != null) { filledVertexSetEmpty = false; break; } } while (!filledVertexSetEmpty) { var candidateRoots = this.findRoots(parent, filledVertexSet); // If the candidate root is an unconnected group cell, remove it from // the layout. We may need a custom set that holds such groups and forces // them to be processed for resizing and/or moving. for (var i = 0; i < candidateRoots.length; i++) { var vertexSet = Object(); hierarchyVertices.push(vertexSet); this.traverse(candidateRoots[i], true, null, allVertexSet, vertexSet, hierarchyVertices, filledVertexSet); } for (var i = 0; i < candidateRoots.length; i++) { this.roots.push(candidateRoots[i]); } filledVertexSetEmpty = true; // Poor man's isSetEmpty for (var key in filledVertexSet) { if (filledVertexSet[key] != null) { filledVertexSetEmpty = false; break; } } } } else { // Find vertex set as directed traversal from roots for (var i = 0; i < this.roots.length; i++) { var vertexSet = Object(); hierarchyVertices.push(vertexSet); this.traverse(this.roots[i], true, null, allVertexSet, vertexSet, hierarchyVertices, null); } } // Iterate through the result removing parents who have children in this layout // Perform a layout for each seperate hierarchy // Track initial coordinate x-positioning var initialX = 0; for (var i = 0; i < hierarchyVertices.length; i++) { var vertexSet = hierarchyVertices[i]; var tmp = []; for (var key in vertexSet) { tmp.push(vertexSet[key]); } this.model = new mxGraphHierarchyModel(this, tmp, this.roots, parent, this.tightenToSource); this.cycleStage(parent); this.layeringStage(); this.crossingStage(parent); initialX = this.placementStage(initialX, parent); } }; /** * Function: filterDescendants * * Creates an array of descendant cells */ mxHierarchicalLayout.prototype.filterDescendants = function(cell, result) { var model = this.graph.model; if (model.isVertex(cell) && cell != this.parent && this.graph.isCellVisible(cell)) { result[mxObjectIdentity.get(cell)] = cell; } if (this.traverseAncestors || cell == this.parent && this.graph.isCellVisible(cell)) { var childCount = model.getChildCount(cell); for (var i = 0; i < childCount; i++) { var child = model.getChildAt(cell, i); // Ignore ports in the layout vertex list, they are dealt with // in the traversal mechanisms if (!this.isPort(child)) { this.filterDescendants(child, result); } } } }; /** * Function: isPort * * Returns true if the given cell is a "port", that is, when connecting to * it, its parent is the connecting vertex in terms of graph traversal * * Parameters: * * cell - that represents the port. */ mxHierarchicalLayout.prototype.isPort = function(cell) { if (cell != null && cell.geometry != null) { return cell.geometry.relative; } else { return false; } }; /** * Function: getEdgesBetween * * Returns the edges between the given source and target. This takes into * account collapsed and invisible cells and ports. * * Parameters: * * source - * target - * directed - */ mxHierarchicalLayout.prototype.getEdgesBetween = function(source, target, directed) { directed = (directed != null) ? directed : false; var edges = this.getEdges(source); var result = []; // Checks if the edge is connected to the correct // cell and returns the first match for (var i = 0; i < edges.length; i++) { var src = this.getVisibleTerminal(edges[i], true); var trg = this.getVisibleTerminal(edges[i], false); if ((src == source && trg == target) || (!directed && src == target && trg == source)) { result.push(edges[i]); } } return result; }; /** * Traverses the (directed) graph invoking the given function for each * visited vertex and edge. The function is invoked with the current vertex * and the incoming edge as a parameter. This implementation makes sure * each vertex is only visited once. The function may return false if the * traversal should stop at the given vertex. * * Parameters: * * vertex - that represents the vertex where the traversal starts. * directed - boolean indicating if edges should only be traversed * from source to target. Default is true. * edge - Optional that represents the incoming edge. This is * null for the first step of the traversal. * allVertices - Array of cell paths for the visited cells. */ mxHierarchicalLayout.prototype.traverse = function(vertex, directed, edge, allVertices, currentComp, hierarchyVertices, filledVertexSet) { if (vertex != null && allVertices != null) { // Has this vertex been seen before in any traversal // And if the filled vertex set is populated, only // process vertices in that it contains var vertexID = mxObjectIdentity.get(vertex); if ((allVertices[vertexID] == null) && (filledVertexSet == null ? true : filledVertexSet[vertexID] != null)) { if (currentComp[vertexID] == null) { currentComp[vertexID] = vertex; } if (allVertices[vertexID] == null) { allVertices[vertexID] = vertex; } if (filledVertexSet !== null) { delete filledVertexSet[vertexID]; } var edges = this.getEdges(vertex); var edgeIsSource = []; for (var i = 0; i < edges.length; i++) { edgeIsSource[i] = (this.getVisibleTerminal(edges[i], true) == vertex); } for (var i = 0; i < edges.length; i++) { if (!directed || edgeIsSource[i]) { var next = this.getVisibleTerminal(edges[i], !edgeIsSource[i]); // Check whether there are more edges incoming from the target vertex than outgoing // The hierarchical model treats bi-directional parallel edges as being sourced // from the more "sourced" terminal. If the directions are equal in number, the direction // is that of the natural direction from the roots of the layout. // The checks below are slightly more verbose than need be for performance reasons var netCount = 1; for (var j = 0; j < edges.length; j++) { if (j == i) { continue; } else { var isSource2 = edgeIsSource[j]; var otherTerm = this.getVisibleTerminal(edges[j], !isSource2); if (otherTerm == next) { if (isSource2) { netCount++; } else { netCount--; } } } } if (netCount >= 0) { currentComp = this.traverse(next, directed, edges[i], allVertices, currentComp, hierarchyVertices, filledVertexSet); } } } } else { if (currentComp[vertexID] == null) { // We've seen this vertex before, but not in the current component // This component and the one it's in need to be merged for (var i = 0; i < hierarchyVertices.length; i++) { var comp = hierarchyVertices[i]; if (comp[vertexID] != null) { for (var key in comp) { currentComp[key] = comp[key]; } // Remove the current component from the hierarchy set hierarchyVertices.splice(i, 1); return currentComp; } } } } } return currentComp; }; /** * Function: cycleStage * * Executes the cycle stage using mxMinimumCycleRemover. */ mxHierarchicalLayout.prototype.cycleStage = function(parent) { var cycleStage = new mxMinimumCycleRemover(this); cycleStage.execute(parent); }; /** * Function: layeringStage * * Implements first stage of a Sugiyama layout. */ mxHierarchicalLayout.prototype.layeringStage = function() { this.model.initialRank(); this.model.fixRanks(); }; /** * Function: crossingStage * * Executes the crossing stage using mxMedianHybridCrossingReduction. */ mxHierarchicalLayout.prototype.crossingStage = function(parent) { var crossingStage = new mxMedianHybridCrossingReduction(this); crossingStage.execute(parent); }; /** * Function: placementStage * * Executes the placement stage using mxCoordinateAssignment. */ mxHierarchicalLayout.prototype.placementStage = function(initialX, parent) { var placementStage = new mxCoordinateAssignment(this, this.intraCellSpacing, this.interRankCellSpacing, this.orientation, initialX, this.parallelEdgeSpacing); placementStage.fineTuning = this.fineTuning; placementStage.execute(parent); return placementStage.limitX + this.interHierarchySpacing; };