diff options
Diffstat (limited to 'src/js/layout/mxCompactTreeLayout.js')
-rw-r--r-- | src/js/layout/mxCompactTreeLayout.js | 995 |
1 files changed, 995 insertions, 0 deletions
diff --git a/src/js/layout/mxCompactTreeLayout.js b/src/js/layout/mxCompactTreeLayout.js new file mode 100644 index 0000000..db6324c --- /dev/null +++ b/src/js/layout/mxCompactTreeLayout.js @@ -0,0 +1,995 @@ +/** + * $Id: mxCompactTreeLayout.js,v 1.57 2012-05-24 13:09:34 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCompactTreeLayout + * + * Extends <mxGraphLayout> to implement a compact tree (Moen) algorithm. This + * layout is suitable for graphs that have no cycles (trees). Vertices that are + * not connected to the tree will be ignored by this layout. + * + * Example: + * + * (code) + * var layout = new mxCompactTreeLayout(graph); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxCompactTreeLayout + * + * Constructs a new compact tree layout for the specified graph + * and orientation. + */ +function mxCompactTreeLayout(graph, horizontal, invert) +{ + mxGraphLayout.call(this, graph); + this.horizontal = (horizontal != null) ? horizontal : true; + this.invert = (invert != null) ? invert : false; +}; + +/** + * Extends mxGraphLayout. + */ +mxCompactTreeLayout.prototype = new mxGraphLayout(); +mxCompactTreeLayout.prototype.constructor = mxCompactTreeLayout; + +/** + * Variable: horizontal + * + * Specifies the orientation of the layout. Default is true. + */ +mxCompactTreeLayout.prototype.horizontal = null; + +/** + * Variable: invert + * + * Specifies if edge directions should be inverted. Default is false. + */ +mxCompactTreeLayout.prototype.invert = null; + +/** + * Variable: resizeParent + * + * If the parents should be resized to match the width/height of the + * children. Default is true. + */ +mxCompactTreeLayout.prototype.resizeParent = true; + +/** + * Variable: groupPadding + * + * Padding added to resized parents + */ +mxCompactTreeLayout.prototype.groupPadding = 10; + +/** + * Variable: parentsChanged + * + * A set of the parents that need updating based on children + * process as part of the layout + */ +mxCompactTreeLayout.prototype.parentsChanged = null; + +/** + * Variable: moveTree + * + * Specifies if the tree should be moved to the top, left corner + * if it is inside a top-level layer. Default is false. + */ +mxCompactTreeLayout.prototype.moveTree = false; + +/** + * Variable: levelDistance + * + * Holds the levelDistance. Default is 10. + */ +mxCompactTreeLayout.prototype.levelDistance = 10; + +/** + * Variable: nodeDistance + * + * Holds the nodeDistance. Default is 20. + */ +mxCompactTreeLayout.prototype.nodeDistance = 20; + +/** + * Variable: resetEdges + * + * Specifies if all edge points of traversed edges should be removed. + * Default is true. + */ +mxCompactTreeLayout.prototype.resetEdges = true; + +/** + * Variable: prefHozEdgeSep + * + * The preferred horizontal distance between edges exiting a vertex + */ +mxCompactTreeLayout.prototype.prefHozEdgeSep = 5; + +/** + * Variable: prefVertEdgeOff + * + * The preferred vertical offset between edges exiting a vertex + */ +mxCompactTreeLayout.prototype.prefVertEdgeOff = 4; + +/** + * Variable: minEdgeJetty + * + * The minimum distance for an edge jetty from a vertex + */ +mxCompactTreeLayout.prototype.minEdgeJetty = 8; + +/** + * Variable: channelBuffer + * + * The size of the vertical buffer in the center of inter-rank channels + * where edge control points should not be placed + */ +mxCompactTreeLayout.prototype.channelBuffer = 4; + +/** + * Variable: edgeRouting + * + * Whether or not to apply the internal tree edge routing + */ +mxCompactTreeLayout.prototype.edgeRouting = true; + +/** + * Function: isVertexIgnored + * + * Returns a boolean indicating if the given <mxCell> should be ignored as a + * vertex. This returns true if the cell has no connections. + * + * Parameters: + * + * vertex - <mxCell> whose ignored state should be returned. + */ +mxCompactTreeLayout.prototype.isVertexIgnored = function(vertex) +{ + return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) || + this.graph.getConnections(vertex).length == 0; +}; + +/** + * Function: isHorizontal + * + * Returns <horizontal>. + */ +mxCompactTreeLayout.prototype.isHorizontal = function() +{ + return this.horizontal; +}; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute>. + * + * If the parent has any connected edges, then it is used as the root of + * the tree. Else, <mxGraph.findTreeRoots> will be used to find a suitable + * root node within the set of children of the given parent. + * + * Parameters: + * + * parent - <mxCell> whose children should be laid out. + * root - Optional <mxCell> that will be used as the root of the tree. + */ +mxCompactTreeLayout.prototype.execute = function(parent, root) +{ + this.parent = parent; + var model = this.graph.getModel(); + + if (root == null) + { + // Takes the parent as the root if it has outgoing edges + if (this.graph.getEdges(parent, model.getParent(parent), + this.invert, !this.invert, false).length > 0) + { + root = parent; + } + + // Tries to find a suitable root in the parent's + // children + else + { + var roots = this.graph.findTreeRoots(parent, true, this.invert); + + if (roots.length > 0) + { + for (var i = 0; i < roots.length; i++) + { + if (!this.isVertexIgnored(roots[i]) && + this.graph.getEdges(roots[i], null, + this.invert, !this.invert, false).length > 0) + { + root = roots[i]; + break; + } + } + } + } + } + + if (root != null) + { + if (this.resizeParent) + { + this.parentsChanged = new Object(); + } + else + { + this.parentsChanged = null; + } + + model.beginUpdate(); + + try + { + var node = this.dfs(root, parent); + + if (node != null) + { + this.layout(node); + var x0 = this.graph.gridSize; + var y0 = x0; + + if (!this.moveTree) + { + var g = this.getVertexBounds(root); + + if (g != null) + { + x0 = g.x; + y0 = g.y; + } + } + + var bounds = null; + + if (this.isHorizontal()) + { + bounds = this.horizontalLayout(node, x0, y0); + } + else + { + bounds = this.verticalLayout(node, null, x0, y0); + } + + if (bounds != null) + { + var dx = 0; + var dy = 0; + + if (bounds.x < 0) + { + dx = Math.abs(x0 - bounds.x); + } + + if (bounds.y < 0) + { + dy = Math.abs(y0 - bounds.y); + } + + if (dx != 0 || dy != 0) + { + this.moveNode(node, dx, dy); + } + + if (this.resizeParent) + { + this.adjustParents(); + } + + if (this.edgeRouting) + { + // Iterate through all edges setting their positions + this.localEdgeProcessing(node); + } + } + } + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: moveNode + * + * Moves the specified node and all of its children by the given amount. + */ +mxCompactTreeLayout.prototype.moveNode = function(node, dx, dy) +{ + node.x += dx; + node.y += dy; + this.apply(node); + + var child = node.child; + + while (child != null) + { + this.moveNode(child, dx, dy); + child = child.next; + } +}; + +/** + * Function: dfs + * + * Does a depth first search starting at the specified cell. + * Makes sure the specified parent is never left by the + * algorithm. + */ +mxCompactTreeLayout.prototype.dfs = function(cell, parent, visited) +{ + visited = (visited != null) ? visited : []; + + var id = mxCellPath.create(cell); + var node = null; + + if (cell != null && visited[id] == null && !this.isVertexIgnored(cell)) + { + visited[id] = cell; + node = this.createNode(cell); + + var model = this.graph.getModel(); + var prev = null; + var out = this.graph.getEdges(cell, parent, this.invert, !this.invert, false, true); + var view = this.graph.getView(); + + for (var i = 0; i < out.length; i++) + { + var edge = out[i]; + + if (!this.isEdgeIgnored(edge)) + { + // Resets the points on the traversed edge + if (this.resetEdges) + { + this.setEdgePoints(edge, null); + } + + if (this.edgeRouting) + { + this.setEdgeStyleEnabled(edge, false); + this.setEdgePoints(edge, null); + } + + // Checks if terminal in same swimlane + var state = view.getState(edge); + var target = (state != null) ? state.getVisibleTerminal(this.invert) : view.getVisibleTerminal(edge, this.invert); + var tmp = this.dfs(target, parent, visited); + + if (tmp != null && model.getGeometry(target) != null) + { + if (prev == null) + { + node.child = tmp; + } + else + { + prev.next = tmp; + } + + prev = tmp; + } + } + } + } + + return node; +}; + +/** + * Function: layout + * + * Starts the actual compact tree layout algorithm + * at the given node. + */ +mxCompactTreeLayout.prototype.layout = function(node) +{ + if (node != null) + { + var child = node.child; + + while (child != null) + { + this.layout(child); + child = child.next; + } + + if (node.child != null) + { + this.attachParent(node, this.join(node)); + } + else + { + this.layoutLeaf(node); + } + } +}; + +/** + * Function: horizontalLayout + */ +mxCompactTreeLayout.prototype.horizontalLayout = function(node, x0, y0, bounds) +{ + node.x += x0 + node.offsetX; + node.y += y0 + node.offsetY; + bounds = this.apply(node, bounds); + var child = node.child; + + if (child != null) + { + bounds = this.horizontalLayout(child, node.x, node.y, bounds); + var siblingOffset = node.y + child.offsetY; + var s = child.next; + + while (s != null) + { + bounds = this.horizontalLayout(s, node.x + child.offsetX, siblingOffset, bounds); + siblingOffset += s.offsetY; + s = s.next; + } + } + + return bounds; +}; + +/** + * Function: verticalLayout + */ +mxCompactTreeLayout.prototype.verticalLayout = function(node, parent, x0, y0, bounds) +{ + node.x += x0 + node.offsetY; + node.y += y0 + node.offsetX; + bounds = this.apply(node, bounds); + var child = node.child; + + if (child != null) + { + bounds = this.verticalLayout(child, node, node.x, node.y, bounds); + var siblingOffset = node.x + child.offsetY; + var s = child.next; + + while (s != null) + { + bounds = this.verticalLayout(s, node, siblingOffset, node.y + child.offsetX, bounds); + siblingOffset += s.offsetY; + s = s.next; + } + } + + return bounds; +}; + +/** + * Function: attachParent + */ +mxCompactTreeLayout.prototype.attachParent = function(node, height) +{ + var x = this.nodeDistance + this.levelDistance; + var y2 = (height - node.width) / 2 - this.nodeDistance; + var y1 = y2 + node.width + 2 * this.nodeDistance - height; + + node.child.offsetX = x + node.height; + node.child.offsetY = y1; + + node.contour.upperHead = this.createLine(node.height, 0, + this.createLine(x, y1, node.contour.upperHead)); + node.contour.lowerHead = this.createLine(node.height, 0, + this.createLine(x, y2, node.contour.lowerHead)); +}; + +/** + * Function: layoutLeaf + */ +mxCompactTreeLayout.prototype.layoutLeaf = function(node) +{ + var dist = 2 * this.nodeDistance; + + node.contour.upperTail = this.createLine( + node.height + dist, 0); + node.contour.upperHead = node.contour.upperTail; + node.contour.lowerTail = this.createLine( + 0, -node.width - dist); + node.contour.lowerHead = this.createLine( + node.height + dist, 0, node.contour.lowerTail); +}; + +/** + * Function: join + */ +mxCompactTreeLayout.prototype.join = function(node) +{ + var dist = 2 * this.nodeDistance; + + var child = node.child; + node.contour = child.contour; + var h = child.width + dist; + var sum = h; + child = child.next; + + while (child != null) + { + var d = this.merge(node.contour, child.contour); + child.offsetY = d + h; + child.offsetX = 0; + h = child.width + dist; + sum += d + h; + child = child.next; + } + + return sum; +}; + +/** + * Function: merge + */ +mxCompactTreeLayout.prototype.merge = function(p1, p2) +{ + var x = 0; + var y = 0; + var total = 0; + + var upper = p1.lowerHead; + var lower = p2.upperHead; + + while (lower != null && upper != null) + { + var d = this.offset(x, y, lower.dx, lower.dy, + upper.dx, upper.dy); + y += d; + total += d; + + if (x + lower.dx <= upper.dx) + { + x += lower.dx; + y += lower.dy; + lower = lower.next; + } + else + { + x -= upper.dx; + y -= upper.dy; + upper = upper.next; + } + } + + if (lower != null) + { + var b = this.bridge(p1.upperTail, 0, 0, lower, x, y); + p1.upperTail = (b.next != null) ? p2.upperTail : b; + p1.lowerTail = p2.lowerTail; + } + else + { + var b = this.bridge(p2.lowerTail, x, y, upper, 0, 0); + + if (b.next == null) + { + p1.lowerTail = b; + } + } + + p1.lowerHead = p2.lowerHead; + + return total; +}; + +/** + * Function: offset + */ +mxCompactTreeLayout.prototype.offset = function(p1, p2, a1, a2, b1, b2) +{ + var d = 0; + + if (b1 <= p1 || p1 + a1 <= 0) + { + return 0; + } + + var t = b1 * a2 - a1 * b2; + + if (t > 0) + { + if (p1 < 0) + { + var s = p1 * a2; + d = s / a1 - p2; + } + else if (p1 > 0) + { + var s = p1 * b2; + d = s / b1 - p2; + } + else + { + d = -p2; + } + } + else if (b1 < p1 + a1) + { + var s = (b1 - p1) * a2; + d = b2 - (p2 + s / a1); + } + else if (b1 > p1 + a1) + { + var s = (a1 + p1) * b2; + d = s / b1 - (p2 + a2); + } + else + { + d = b2 - (p2 + a2); + } + + if (d > 0) + { + return d; + } + else + { + return 0; + } +}; + +/** + * Function: bridge + */ +mxCompactTreeLayout.prototype.bridge = function(line1, x1, y1, line2, x2, y2) +{ + var dx = x2 + line2.dx - x1; + var dy = 0; + var s = 0; + + if (line2.dx == 0) + { + dy = line2.dy; + } + else + { + s = dx * line2.dy; + dy = s / line2.dx; + } + + var r = this.createLine(dx, dy, line2.next); + line1.next = this.createLine(0, y2 + line2.dy - dy - y1, r); + + return r; +}; + +/** + * Function: createNode + */ +mxCompactTreeLayout.prototype.createNode = function(cell) +{ + var node = new Object(); + node.cell = cell; + node.x = 0; + node.y = 0; + node.width = 0; + node.height = 0; + + var geo = this.getVertexBounds(cell); + + if (geo != null) + { + if (this.isHorizontal()) + { + node.width = geo.height; + node.height = geo.width; + } + else + { + node.width = geo.width; + node.height = geo.height; + } + } + + node.offsetX = 0; + node.offsetY = 0; + node.contour = new Object(); + + return node; +}; + +/** + * Function: apply + */ +mxCompactTreeLayout.prototype.apply = function(node, bounds) +{ + var model = this.graph.getModel(); + var cell = node.cell; + var g = model.getGeometry(cell); + + if (cell != null && g != null) + { + if (this.isVertexMovable(cell)) + { + g = this.setVertexLocation(cell, node.x, node.y); + + if (this.resizeParent) + { + var parent = model.getParent(cell); + var id = mxCellPath.create(parent); + + // Implements set semantic + if (this.parentsChanged[id] == null) + { + this.parentsChanged[id] = parent; + } + } + } + + if (bounds == null) + { + bounds = new mxRectangle(g.x, g.y, g.width, g.height); + } + else + { + bounds = new mxRectangle(Math.min(bounds.x, g.x), + Math.min(bounds.y, g.y), + Math.max(bounds.x + bounds.width, g.x + g.width), + Math.max(bounds.y + bounds.height, g.y + g.height)); + } + } + + return bounds; +}; + +/** + * Function: createLine + */ +mxCompactTreeLayout.prototype.createLine = function(dx, dy, next) +{ + var line = new Object(); + line.dx = dx; + line.dy = dy; + line.next = next; + + return line; +}; + +/** + * Function: adjustParents + * + * Adjust parent cells whose child geometries have changed. The default + * implementation adjusts the group to just fit around the children with + * a padding. + */ +mxCompactTreeLayout.prototype.adjustParents = function() +{ + var tmp = []; + + for (var id in this.parentsChanged) + { + tmp.push(this.parentsChanged[id]); + } + + this.arrangeGroups(mxUtils.sortCells(tmp, true), this.groupPadding); +}; + +/** + * Function: localEdgeProcessing + * + * Moves the specified node and all of its children by the given amount. + */ +mxCompactTreeLayout.prototype.localEdgeProcessing = function(node) +{ + this.processNodeOutgoing(node); + var child = node.child; + + while (child != null) + { + this.localEdgeProcessing(child); + child = child.next; + } +}; + +/** + * Function: localEdgeProcessing + * + * Separates the x position of edges as they connect to vertices + */ +mxCompactTreeLayout.prototype.processNodeOutgoing = function(node) +{ + var child = node.child; + var parentCell = node.cell; + + var childCount = 0; + var sortedCells = []; + + while (child != null) + { + childCount++; + + var sortingCriterion = child.x; + + if (this.horizontal) + { + sortingCriterion = child.y; + } + + sortedCells.push(new WeightedCellSorter(child, sortingCriterion)); + child = child.next; + } + + sortedCells.sort(WeightedCellSorter.prototype.compare); + + var availableWidth = node.width; + + var requiredWidth = (childCount + 1) * this.prefHozEdgeSep; + + // Add a buffer on the edges of the vertex if the edge count allows + if (availableWidth > requiredWidth + (2 * this.prefHozEdgeSep)) + { + availableWidth -= 2 * this.prefHozEdgeSep; + } + + var edgeSpacing = availableWidth / childCount; + + var currentXOffset = edgeSpacing / 2.0; + + if (availableWidth > requiredWidth + (2 * this.prefHozEdgeSep)) + { + currentXOffset += this.prefHozEdgeSep; + } + + var currentYOffset = this.minEdgeJetty - this.prefVertEdgeOff; + var maxYOffset = 0; + + var parentBounds = this.getVertexBounds(parentCell); + child = node.child; + + for (var j = 0; j < sortedCells.length; j++) + { + var childCell = sortedCells[j].cell.cell; + var childBounds = this.getVertexBounds(childCell); + + var edges = this.graph.getEdgesBetween(parentCell, + childCell, false); + + var newPoints = []; + var x = 0; + var y = 0; + + for (var i = 0; i < edges.length; i++) + { + if (this.horizontal) + { + // Use opposite co-ords, calculation was done for + // + x = parentBounds.x + parentBounds.width; + y = parentBounds.y + currentXOffset; + newPoints.push(new mxPoint(x, y)); + x = parentBounds.x + parentBounds.width + + currentYOffset; + newPoints.push(new mxPoint(x, y)); + y = childBounds.y + childBounds.height / 2.0; + newPoints.push(new mxPoint(x, y)); + this.setEdgePoints(edges[i], newPoints); + } + else + { + x = parentBounds.x + currentXOffset; + y = parentBounds.y + parentBounds.height; + newPoints.push(new mxPoint(x, y)); + y = parentBounds.y + parentBounds.height + + currentYOffset; + newPoints.push(new mxPoint(x, y)); + x = childBounds.x + childBounds.width / 2.0; + newPoints.push(new mxPoint(x, y)); + this.setEdgePoints(edges[i], newPoints); + } + } + + if (j < childCount / 2) + { + currentYOffset += this.prefVertEdgeOff; + } + else if (j > childCount / 2) + { + currentYOffset -= this.prefVertEdgeOff; + } + // Ignore the case if equals, this means the second of 2 + // jettys with the same y (even number of edges) + + // pos[k * 2] = currentX; + currentXOffset += edgeSpacing; + // pos[k * 2 + 1] = currentYOffset; + + maxYOffset = Math.max(maxYOffset, currentYOffset); + } +}; + +/** + * Class: WeightedCellSorter + * + * A utility class used to track cells whilst sorting occurs on the weighted + * sum of their connected edges. Does not violate (x.compareTo(y)==0) == + * (x.equals(y)) + * + * Constructor: WeightedCellSorter + * + * Constructs a new weighted cell sorted for the given cell and weight. + */ +function WeightedCellSorter(cell, weightedValue) +{ + this.cell = cell; + this.weightedValue = weightedValue; +}; + +/** + * Variable: weightedValue + * + * The weighted value of the cell stored. + */ +WeightedCellSorter.prototype.weightedValue = 0; + +/** + * Variable: nudge + * + * Whether or not to flip equal weight values. + */ +WeightedCellSorter.prototype.nudge = false; + +/** + * Variable: visited + * + * Whether or not this cell has been visited in the current assignment. + */ +WeightedCellSorter.prototype.visited = false; + +/** + * Variable: rankIndex + * + * The index this cell is in the model rank. + */ +WeightedCellSorter.prototype.rankIndex = null; + +/** + * Variable: cell + * + * The cell whose median value is being calculated. + */ +WeightedCellSorter.prototype.cell = null; + +/** + * Function: compare + * + * Compares two WeightedCellSorters. + */ +WeightedCellSorter.prototype.compare = function(a, b) +{ + if (a != null && b != null) + { + if (b.weightedValue > a.weightedValue) + { + return 1; + } + else if (b.weightedValue < a.weightedValue) + { + return -1; + } + else + { + if (b.nudge) + { + return 1; + } + else + { + return -1; + } + } + } + else + { + return 0; + } +};
\ No newline at end of file |