diff options
Diffstat (limited to 'src/js/layout/hierarchical/model/mxGraphHierarchyModel.js')
-rw-r--r-- | src/js/layout/hierarchical/model/mxGraphHierarchyModel.js | 685 |
1 files changed, 685 insertions, 0 deletions
diff --git a/src/js/layout/hierarchical/model/mxGraphHierarchyModel.js b/src/js/layout/hierarchical/model/mxGraphHierarchyModel.js new file mode 100644 index 0000000..ca2ba30 --- /dev/null +++ b/src/js/layout/hierarchical/model/mxGraphHierarchyModel.js @@ -0,0 +1,685 @@ +/** + * $Id: mxGraphHierarchyModel.js,v 1.33 2012-12-18 13:16:43 david Exp $ + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Class: mxGraphHierarchyModel + * + * 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: mxGraphHierarchyModel + * + * 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 mxGraphHierarchyModel(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 Object(); + this.edgeMapper = new Object(); + 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 = graph.getView().getVisibleTerminal( + realEdge, false); + var targetCellId = mxCellPath.create(targetCell); + var internalTargetCell = this.vertexMapper[targetCellId]; + + if (internalVertices[i] == internalTargetCell) + { + // The real edge is reversed relative to the internal edge + targetCell = graph.getView().getVisibleTerminal( + realEdge, true); + targetCellId = mxCellPath.create(targetCell); + internalTargetCell = this.vertexMapper[targetCellId]; + } + + 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 + */ +mxGraphHierarchyModel.prototype.maxRank = null; + +/** + * Variable: vertexMapper + * + * Map from graph vertices to internal model nodes. + */ +mxGraphHierarchyModel.prototype.vertexMapper = null; + +/** + * Variable: edgeMapper + * + * Map from graph edges to internal model edges + */ +mxGraphHierarchyModel.prototype.edgeMapper = null; + +/** + * Variable: ranks + * + * Mapping from rank number to actual rank + */ +mxGraphHierarchyModel.prototype.ranks = null; + +/** + * Variable: roots + * + * Store of roots of this hierarchy model, these are real graph cells, not + * internal cells + */ +mxGraphHierarchyModel.prototype.roots = null; + +/** + * Variable: parent + * + * The parent cell whose children are being laid out + */ +mxGraphHierarchyModel.prototype.parent = null; + +/** + * Variable: dfsCount + * + * Count of the number of times the ancestor dfs has been used. + */ +mxGraphHierarchyModel.prototype.dfsCount = 0; + +/** + * Variable: SOURCESCANSTARTRANK + * + * High value to start source layering scan rank value from. + */ +mxGraphHierarchyModel.prototype.SOURCESCANSTARTRANK = 100000000; + +/** + * Variable: tightenToSource + * + * Whether or not to tighten the assigned ranks of vertices up towards + * the source cells. + */ +mxGraphHierarchyModel.prototype.tightenToSource = false; + +/** + * Function: createInternalCells + * + * Creates all edges in the internal model + * + * Parameters: + * + * layout - Reference to the <mxHierarchicalLayout> algorithm. + * vertices - Array of <mxCells> that represent the vertices whom are to + * have an internal representation created. + * internalVertices - The array of <mxGraphHierarchyNodes> to have their + * information filled in using the real vertices. + */ +mxGraphHierarchyModel.prototype.createInternalCells = function(layout, vertices, internalVertices) +{ + var graph = layout.getGraph(); + + // Create internal edges + for (var i = 0; i < vertices.length; i++) + { + internalVertices[i] = new mxGraphHierarchyNode(vertices[i]); + var vertexId = mxCellPath.create(vertices[i]); + this.vertexMapper[vertexId] = internalVertices[i]; + + // If the layout is deterministic, order the cells + //List outgoingCells = graph.getNeighbours(vertices[i], deterministic); + var conns = layout.getEdges(vertices[i]); + var outgoingCells = graph.getOpposites(conns, 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 < outgoingCells.length; j++) + { + var cell = outgoingCells[j]; + + 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 = graph.getEdgesBetween(vertices[i], + cell, false); + var directedEdges = graph.getEdgesBetween(vertices[i], + cell, true); + var edgeId = mxCellPath.create(undirectedEdges[0]); + + if (undirectedEdges != null && + undirectedEdges.length > 0 && + this.edgeMapper[edgeId] == null && + directedEdges.length * 2 >= undirectedEdges.length) + { + var internalEdge = new mxGraphHierarchyEdge(undirectedEdges); + + for (var k = 0; k < undirectedEdges.length; k++) + { + var edge = undirectedEdges[k]; + edgeId = mxCellPath.create(edge); + this.edgeMapper[edgeId] = 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. +*/ +mxGraphHierarchyModel.prototype.initialRank = function() +{ + var startNodes = []; + + if (this.roots != null) + { + for (var i = 0; i < this.roots.length; i++) + { + var vertexId = mxCellPath.create(this.roots[i]); + var internalNode = this.vertexMapper[vertexId]; + + if (internalNode != null) + { + startNodes.push(internalNode); + } + } + } + + for (var key in this.vertexMapper) + { + var internalNode = this.vertexMapper[key]; + + // Mark the node as not having had a layer assigned + internalNode.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 = this.SOURCESCANSTARTRANK; + + 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) + { + internalNode.temp[0] = minimumLayer; + this.maxRank = Math.min(this.maxRank, 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]; + } + } + + // Reset the maxRank to that which would be expected for a from-sink + // scan + this.maxRank = this.SOURCESCANSTARTRANK - this.maxRank; +}; + +/** + * Function: fixRanks + * + * Fixes the layer assignments to the values stored in the nodes. Also needs + * to create dummy nodes for edges that cross layers. + */ +mxGraphHierarchyModel.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 cellId = mxCellPath.create(cell); + var internalNode = this.vertexMapper[cellId]; + 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. + */ +mxGraphHierarchyModel.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 ) + */ +mxGraphHierarchyModel.prototype.dfs = function(parent, root, connectingEdge, visitor, seen, layer) +{ + if (root != null) + { + var rootId = mxCellPath.create(root.cell); + + 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 ) + */ +mxGraphHierarchyModel.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 = mxCellPath.create(root.cell); + + 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.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); + } + } +}; |