/** * Copyright (c) 2006-2018, JGraph Ltd * Copyright (c) 2006-2018, Gaudenz Alder */ /** * Class: mxSwimlaneModel * * Internal model of a hierarchical graph. This model stores nodes and edges * equivalent to the real graph nodes and edges, but also stores the rank of the * cells, the order within the ranks and the new candidate locations of cells. * The internal model also reverses edge direction were appropriate , ignores * self-loop and groups parallels together under one edge object. * * Constructor: mxSwimlaneModel * * Creates an internal ordered graph model using the vertices passed in. If * there are any, leftward edge need to be inverted in the internal model * * Arguments: * * graph - the facade describing the graph to be operated on * vertices - the vertices for this hierarchy * ordered - whether or not the vertices are already ordered * deterministic - whether or not this layout should be deterministic on each * tightenToSource - whether or not to tighten vertices towards the sources * scanRanksFromSinks - Whether rank assignment is from the sinks or sources. * usage */ function mxSwimlaneModel(layout, vertices, roots, parent, tightenToSource) { var graph = layout.getGraph(); this.tightenToSource = tightenToSource; this.roots = roots; this.parent = parent; // map of cells to internal cell needed for second run through // to setup the sink of edges correctly this.vertexMapper = new mxDictionary(); this.edgeMapper = new mxDictionary(); this.maxRank = 0; var internalVertices = []; if (vertices == null) { vertices = this.graph.getChildVertices(parent); } this.maxRank = this.SOURCESCANSTARTRANK; // map of cells to internal cell needed for second run through // to setup the sink of edges correctly. Guess size by number // of edges is roughly same as number of vertices. this.createInternalCells(layout, vertices, internalVertices); // Go through edges set their sink values. Also check the // ordering if and invert edges if necessary for (var i = 0; i < vertices.length; i++) { var edges = internalVertices[i].connectsAsSource; for (var j = 0; j < edges.length; j++) { var internalEdge = edges[j]; var realEdges = internalEdge.edges; // Only need to process the first real edge, since // all the edges connect to the same other vertex if (realEdges != null && realEdges.length > 0) { var realEdge = realEdges[0]; var targetCell = layout.getVisibleTerminal( realEdge, false); var internalTargetCell = this.vertexMapper.get(targetCell); if (internalVertices[i] == internalTargetCell) { // If there are parallel edges going between two vertices and not all are in the same direction // you can have navigated across one direction when doing the cycle reversal that isn't the same // direction as the first real edge in the array above. When that happens the if above catches // that and we correct the target cell before continuing. // This branch only detects this single case targetCell = layout.getVisibleTerminal( realEdge, true); internalTargetCell = this.vertexMapper.get(targetCell); } if (internalTargetCell != null && internalVertices[i] != internalTargetCell) { internalEdge.target = internalTargetCell; if (internalTargetCell.connectsAsTarget.length == 0) { internalTargetCell.connectsAsTarget = []; } if (mxUtils.indexOf(internalTargetCell.connectsAsTarget, internalEdge) < 0) { internalTargetCell.connectsAsTarget.push(internalEdge); } } } } // Use the temp variable in the internal nodes to mark this // internal vertex as having been visited. internalVertices[i].temp[0] = 1; } }; /** * Variable: maxRank * * Stores the largest rank number allocated */ mxSwimlaneModel.prototype.maxRank = null; /** * Variable: vertexMapper * * Map from graph vertices to internal model nodes. */ mxSwimlaneModel.prototype.vertexMapper = null; /** * Variable: edgeMapper * * Map from graph edges to internal model edges */ mxSwimlaneModel.prototype.edgeMapper = null; /** * Variable: ranks * * Mapping from rank number to actual rank */ mxSwimlaneModel.prototype.ranks = null; /** * Variable: roots * * Store of roots of this hierarchy model, these are real graph cells, not * internal cells */ mxSwimlaneModel.prototype.roots = null; /** * Variable: parent * * The parent cell whose children are being laid out */ mxSwimlaneModel.prototype.parent = null; /** * Variable: dfsCount * * Count of the number of times the ancestor dfs has been used. */ mxSwimlaneModel.prototype.dfsCount = 0; /** * Variable: SOURCESCANSTARTRANK * * High value to start source layering scan rank value from. */ mxSwimlaneModel.prototype.SOURCESCANSTARTRANK = 100000000; /** * Variable: tightenToSource * * Whether or not to tighten the assigned ranks of vertices up towards * the source cells. */ mxSwimlaneModel.prototype.tightenToSource = false; /** * Variable: ranksPerGroup * * An array of the number of ranks within each swimlane */ mxSwimlaneModel.prototype.ranksPerGroup = null; /** * Function: createInternalCells * * Creates all edges in the internal model * * Parameters: * * layout - Reference to the algorithm. * vertices - Array of that represent the vertices whom are to * have an internal representation created. * internalVertices - The array of to have their * information filled in using the real vertices. */ mxSwimlaneModel.prototype.createInternalCells = function(layout, vertices, internalVertices) { var graph = layout.getGraph(); var swimlanes = layout.swimlanes; // Create internal edges for (var i = 0; i < vertices.length; i++) { internalVertices[i] = new mxGraphHierarchyNode(vertices[i]); this.vertexMapper.put(vertices[i], internalVertices[i]); internalVertices[i].swimlaneIndex = -1; for (var ii = 0; ii < swimlanes.length; ii++) { if (graph.model.getParent(vertices[i]) == swimlanes[ii]) { internalVertices[i].swimlaneIndex = ii; break; } } // If the layout is deterministic, order the cells //List outgoingCells = graph.getNeighbours(vertices[i], deterministic); var conns = layout.getEdges(vertices[i]); internalVertices[i].connectsAsSource = []; // Create internal edges, but don't do any rank assignment yet // First use the information from the greedy cycle remover to // invert the leftward edges internally for (var j = 0; j < conns.length; j++) { var cell = layout.getVisibleTerminal(conns[j], false); // Looking for outgoing edges only if (cell != vertices[i] && layout.graph.model.isVertex(cell) && !layout.isVertexIgnored(cell)) { // We process all edge between this source and its targets // If there are edges going both ways, we need to collect // them all into one internal edges to avoid looping problems // later. We assume this direction (source -> target) is the // natural direction if at least half the edges are going in // that direction. // The check below for edges[0] being in the vertex mapper is // in case we've processed this the other way around // (target -> source) and the number of edges in each direction // are the same. All the graph edges will have been assigned to // an internal edge going the other way, so we don't want to // process them again var undirectedEdges = layout.getEdgesBetween(vertices[i], cell, false); var directedEdges = layout.getEdgesBetween(vertices[i], cell, true); if (undirectedEdges != null && undirectedEdges.length > 0 && this.edgeMapper.get(undirectedEdges[0]) == null && directedEdges.length * 2 >= undirectedEdges.length) { var internalEdge = new mxGraphHierarchyEdge(undirectedEdges); for (var k = 0; k < undirectedEdges.length; k++) { var edge = undirectedEdges[k]; this.edgeMapper.put(edge, internalEdge); // Resets all point on the edge and disables the edge style // without deleting it from the cell style graph.resetEdge(edge); if (layout.disableEdgeStyle) { layout.setEdgeStyleEnabled(edge, false); layout.setOrthogonalEdge(edge,true); } } internalEdge.source = internalVertices[i]; if (mxUtils.indexOf(internalVertices[i].connectsAsSource, internalEdge) < 0) { internalVertices[i].connectsAsSource.push(internalEdge); } } } } // Ensure temp variable is cleared from any previous use internalVertices[i].temp[0] = 0; } }; /** * Function: initialRank * * Basic determination of minimum layer ranking by working from from sources * or sinks and working through each node in the relevant edge direction. * Starting at the sinks is basically a longest path layering algorithm. */ mxSwimlaneModel.prototype.initialRank = function() { this.ranksPerGroup = []; var startNodes = []; var seen = new Object(); if (this.roots != null) { for (var i = 0; i < this.roots.length; i++) { var internalNode = this.vertexMapper.get(this.roots[i]); this.maxChainDfs(null, internalNode, null, seen, 0); if (internalNode != null) { startNodes.push(internalNode); } } } // Calculate the lower and upper rank bounds of each swimlane var lowerRank = []; var upperRank = []; for (var i = this.ranksPerGroup.length - 1; i >= 0; i--) { if (i == this.ranksPerGroup.length - 1) { lowerRank[i] = 0; } else { lowerRank[i] = upperRank[i+1] + 1; } upperRank[i] = lowerRank[i] + this.ranksPerGroup[i]; } this.maxRank = upperRank[0]; var internalNodes = this.vertexMapper.getValues(); for (var i=0; i < internalNodes.length; i++) { // Mark the node as not having had a layer assigned internalNodes[i].temp[0] = -1; } var startNodesCopy = startNodes.slice(); while (startNodes.length > 0) { var internalNode = startNodes[0]; var layerDeterminingEdges; var edgesToBeMarked; layerDeterminingEdges = internalNode.connectsAsTarget; edgesToBeMarked = internalNode.connectsAsSource; // flag to keep track of whether or not all layer determining // edges have been scanned var allEdgesScanned = true; // Work out the layer of this node from the layer determining // edges. The minimum layer number of any node connected by one of // the layer determining edges variable var minimumLayer = upperRank[0]; for (var i = 0; i < layerDeterminingEdges.length; i++) { var internalEdge = layerDeterminingEdges[i]; if (internalEdge.temp[0] == 5270620) { // This edge has been scanned, get the layer of the // node on the other end var otherNode = internalEdge.source; minimumLayer = Math.min(minimumLayer, otherNode.temp[0] - 1); } else { allEdgesScanned = false; break; } } // If all edge have been scanned, assign the layer, mark all // edges in the other direction and remove from the nodes list if (allEdgesScanned) { if (minimumLayer > upperRank[internalNode.swimlaneIndex]) { minimumLayer = upperRank[internalNode.swimlaneIndex]; } internalNode.temp[0] = minimumLayer; if (edgesToBeMarked != null) { for (var i = 0; i < edgesToBeMarked.length; i++) { var internalEdge = edgesToBeMarked[i]; // Assign unique stamp ( y/m/d/h ) internalEdge.temp[0] = 5270620; // Add node on other end of edge to LinkedList of // nodes to be analysed var otherNode = internalEdge.target; // Only add node if it hasn't been assigned a layer if (otherNode.temp[0] == -1) { startNodes.push(otherNode); // Mark this other node as neither being // unassigned nor assigned so it isn't // added to this list again, but it's // layer isn't used in any calculation. otherNode.temp[0] = -2; } } } startNodes.shift(); } else { // Not all the edges have been scanned, get to the back of // the class and put the dunces cap on var removedCell = startNodes.shift(); startNodes.push(internalNode); if (removedCell == internalNode && startNodes.length == 1) { // This is an error condition, we can't get out of // this loop. It could happen for more than one node // but that's a lot harder to detect. Log the error // TODO make log comment break; } } } // Normalize the ranks down from their large starting value to place // at least 1 sink on layer 0 // for (var key in this.vertexMapper) // { // var internalNode = this.vertexMapper[key]; // // Mark the node as not having had a layer assigned // internalNode.temp[0] -= this.maxRank; // } // Tighten the rank 0 nodes as far as possible // for ( var i = 0; i < startNodesCopy.length; i++) // { // var internalNode = startNodesCopy[i]; // var currentMaxLayer = 0; // var layerDeterminingEdges = internalNode.connectsAsSource; // // for ( var j = 0; j < layerDeterminingEdges.length; j++) // { // var internalEdge = layerDeterminingEdges[j]; // var otherNode = internalEdge.target; // internalNode.temp[0] = Math.max(currentMaxLayer, // otherNode.temp[0] + 1); // currentMaxLayer = internalNode.temp[0]; // } // } }; /** * Function: maxChainDfs * * Performs a depth first search on the internal hierarchy model. This dfs * extends the default version by keeping track of chains within groups. * Any cycles should be removed prior to running, but previously seen cells * are ignored. * * Parameters: * * parent - the parent internal node of the current internal node * root - the current internal node * connectingEdge - the internal edge connecting the internal node and the parent * internal node, if any * seen - a set of all nodes seen by this dfs * chainCount - the number of edges in the chain of vertices going through * the current swimlane */ mxSwimlaneModel.prototype.maxChainDfs = function(parent, root, connectingEdge, seen, chainCount) { if (root != null) { var rootId = mxCellPath.create(root.cell); if (seen[rootId] == null) { seen[rootId] = root; var slIndex = root.swimlaneIndex; if (this.ranksPerGroup[slIndex] == null || this.ranksPerGroup[slIndex] < chainCount) { this.ranksPerGroup[slIndex] = chainCount; } // Copy the connects as source list so that visitors // can change the original for edge direction inversions var outgoingEdges = root.connectsAsSource.slice(); for (var i = 0; i < outgoingEdges.length; i++) { var internalEdge = outgoingEdges[i]; var targetNode = internalEdge.target; // Only navigate in source->target direction within the same // swimlane, or from a lower index swimlane to a higher one if (root.swimlaneIndex < targetNode.swimlaneIndex) { this.maxChainDfs(root, targetNode, internalEdge, mxUtils.clone(seen, null , true), 0); } else if (root.swimlaneIndex == targetNode.swimlaneIndex) { this.maxChainDfs(root, targetNode, internalEdge, mxUtils.clone(seen, null , true), chainCount + 1); } } } } }; /** * Function: fixRanks * * Fixes the layer assignments to the values stored in the nodes. Also needs * to create dummy nodes for edges that cross layers. */ mxSwimlaneModel.prototype.fixRanks = function() { var rankList = []; this.ranks = []; for (var i = 0; i < this.maxRank + 1; i++) { rankList[i] = []; this.ranks[i] = rankList[i]; } // Perform a DFS to obtain an initial ordering for each rank. // Without doing this you would end up having to process // crossings for a standard tree. var rootsArray = null; if (this.roots != null) { var oldRootsArray = this.roots; rootsArray = []; for (var i = 0; i < oldRootsArray.length; i++) { var cell = oldRootsArray[i]; var internalNode = this.vertexMapper.get(cell); rootsArray[i] = internalNode; } } this.visit(function(parent, node, edge, layer, seen) { if (seen == 0 && node.maxRank < 0 && node.minRank < 0) { rankList[node.temp[0]].push(node); node.maxRank = node.temp[0]; node.minRank = node.temp[0]; // Set temp[0] to the nodes position in the rank node.temp[0] = rankList[node.maxRank].length - 1; } if (parent != null && edge != null) { var parentToCellRankDifference = parent.maxRank - node.maxRank; if (parentToCellRankDifference > 1) { // There are ranks in between the parent and current cell edge.maxRank = parent.maxRank; edge.minRank = node.maxRank; edge.temp = []; edge.x = []; edge.y = []; for (var i = edge.minRank + 1; i < edge.maxRank; i++) { // The connecting edge must be added to the // appropriate ranks rankList[i].push(edge); edge.setGeneralPurposeVariable(i, rankList[i] .length - 1); } } } }, rootsArray, false, null); }; /** * Function: visit * * A depth first search through the internal heirarchy model. * * Parameters: * * visitor - The visitor function pattern to be called for each node. * trackAncestors - Whether or not the search is to keep track all nodes * directly above this one in the search path. */ mxSwimlaneModel.prototype.visit = function(visitor, dfsRoots, trackAncestors, seenNodes) { // Run dfs through on all roots if (dfsRoots != null) { for (var i = 0; i < dfsRoots.length; i++) { var internalNode = dfsRoots[i]; if (internalNode != null) { if (seenNodes == null) { seenNodes = new Object(); } if (trackAncestors) { // Set up hash code for root internalNode.hashCode = []; internalNode.hashCode[0] = this.dfsCount; internalNode.hashCode[1] = i; this.extendedDfs(null, internalNode, null, visitor, seenNodes, internalNode.hashCode, i, 0); } else { this.dfs(null, internalNode, null, visitor, seenNodes, 0); } } } this.dfsCount++; } }; /** * Function: dfs * * Performs a depth first search on the internal hierarchy model * * Parameters: * * parent - the parent internal node of the current internal node * root - the current internal node * connectingEdge - the internal edge connecting the internal node and the parent * internal node, if any * visitor - the visitor pattern to be called for each node * seen - a set of all nodes seen by this dfs a set of all of the * ancestor node of the current node * layer - the layer on the dfs tree ( not the same as the model ranks ) */ mxSwimlaneModel.prototype.dfs = function(parent, root, connectingEdge, visitor, seen, layer) { if (root != null) { var rootId = root.id; if (seen[rootId] == null) { seen[rootId] = root; visitor(parent, root, connectingEdge, layer, 0); // Copy the connects as source list so that visitors // can change the original for edge direction inversions var outgoingEdges = root.connectsAsSource.slice(); for (var i = 0; i< outgoingEdges.length; i++) { var internalEdge = outgoingEdges[i]; var targetNode = internalEdge.target; // Root check is O(|roots|) this.dfs(root, targetNode, internalEdge, visitor, seen, layer + 1); } } else { // Use the int field to indicate this node has been seen visitor(parent, root, connectingEdge, layer, 1); } } }; /** * Function: extendedDfs * * Performs a depth first search on the internal hierarchy model. This dfs * extends the default version by keeping track of cells ancestors, but it * should be only used when necessary because of it can be computationally * intensive for deep searches. * * Parameters: * * parent - the parent internal node of the current internal node * root - the current internal node * connectingEdge - the internal edge connecting the internal node and the parent * internal node, if any * visitor - the visitor pattern to be called for each node * seen - a set of all nodes seen by this dfs * ancestors - the parent hash code * childHash - the new hash code for this node * layer - the layer on the dfs tree ( not the same as the model ranks ) */ mxSwimlaneModel.prototype.extendedDfs = function(parent, root, connectingEdge, visitor, seen, ancestors, childHash, layer) { // Explanation of custom hash set. Previously, the ancestors variable // was passed through the dfs as a HashSet. The ancestors were copied // into a new HashSet and when the new child was processed it was also // added to the set. If the current node was in its ancestor list it // meant there is a cycle in the graph and this information is passed // to the visitor.visit() in the seen parameter. The HashSet clone was // very expensive on CPU so a custom hash was developed using primitive // types. temp[] couldn't be used so hashCode[] was added to each node. // Each new child adds another int to the array, copying the prefix // from its parent. Child of the same parent add different ints (the // limit is therefore 2^32 children per parent...). If a node has a // child with the hashCode already set then the child code is compared // to the same portion of the current nodes array. If they match there // is a loop. // Note that the basic mechanism would only allow for 1 use of this // functionality, so the root nodes have two ints. The second int is // incremented through each node root and the first is incremented // through each run of the dfs algorithm (therefore the dfs is not // thread safe). The hash code of each node is set if not already set, // or if the first int does not match that of the current run. if (root != null) { if (parent != null) { // Form this nodes hash code if necessary, that is, if the // hashCode variable has not been initialized or if the // start of the parent hash code does not equal the start of // this nodes hash code, indicating the code was set on a // previous run of this dfs. if (root.hashCode == null || root.hashCode[0] != parent.hashCode[0]) { var hashCodeLength = parent.hashCode.length + 1; root.hashCode = parent.hashCode.slice(); root.hashCode[hashCodeLength - 1] = childHash; } } var rootId = root.id; if (seen[rootId] == null) { seen[rootId] = root; visitor(parent, root, connectingEdge, layer, 0); // Copy the connects as source list so that visitors // can change the original for edge direction inversions var outgoingEdges = root.connectsAsSource.slice(); var incomingEdges = root.connectsAsTarget.slice(); for (var i = 0; i < outgoingEdges.length; i++) { var internalEdge = outgoingEdges[i]; var targetNode = internalEdge.target; // Only navigate in source->target direction within the same // swimlane, or from a lower index swimlane to a higher one if (root.swimlaneIndex <= targetNode.swimlaneIndex) { this.extendedDfs(root, targetNode, internalEdge, visitor, seen, root.hashCode, i, layer + 1); } } for (var i = 0; i < incomingEdges.length; i++) { var internalEdge = incomingEdges[i]; var targetNode = internalEdge.source; // Only navigate in target->source direction from a lower index // swimlane to a higher one if (root.swimlaneIndex < targetNode.swimlaneIndex) { this.extendedDfs(root, targetNode, internalEdge, visitor, seen, root.hashCode, i, layer + 1); } } } else { // Use the int field to indicate this node has been seen visitor(parent, root, connectingEdge, layer, 1); } } };