Skip to content

Instantly share code, notes, and snippets.

@stream7
Forked from davidjgraph/mxSwimlaneLayout.js
Created November 7, 2017 16:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stream7/3dbbbb39c2218ab26baa453b57718071 to your computer and use it in GitHub Desktop.
Save stream7/3dbbbb39c2218ab26baa453b57718071 to your computer and use it in GitHub Desktop.
23.09.2013 Swimlane layout fix
/**
* $Id: mxSwimlaneLayout.js,v 1.3 2013/09/23 14:11:22 david Exp $
* Copyright (c) 2005-2012, JGraph Ltd
*/
/**
* Class: mxSwimlaneLayout
*
* A hierarchical layout algorithm.
*
* Constructor: mxSwimlaneLayout
*
* Constructs a new hierarchical layout algorithm.
*
* Arguments:
*
* graph - Reference to the enclosing <mxGraph>.
* 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 mxSwimlaneLayout(graph, orientation, deterministic)
{
mxGraphLayout.call(this, graph);
this.orientation = (orientation != null) ? orientation : mxConstants.DIRECTION_NORTH;
this.deterministic = (deterministic != null) ? deterministic : true;
};
/**
* Extends mxGraphLayout.
*/
mxSwimlaneLayout.prototype = new mxGraphLayout();
mxSwimlaneLayout.prototype.constructor = mxSwimlaneLayout;
/**
* Variable: roots
*
* Holds the array of <mxCell> that this layout contains.
*/
mxSwimlaneLayout.prototype.roots = null;
/**
* Variable: swimlanes
*
* Holds the array of <mxCell> of the ordered swimlanes to lay out
*/
mxSwimlaneLayout.prototype.swimlanes = null;
/**
* Variable: dummyVertices
*
* Holds an array of <mxCell> of dummy vertices inserted during the layout
* to pad out empty swimlanes
*/
mxSwimlaneLayout.prototype.dummyVertices = null;
/**
* Variable: dummyVertexWidth
*
* The cell width of any dummy vertices inserted
*/
mxSwimlaneLayout.prototype.dummyVertexWidth = 50;
/**
* 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 <parentBorder>.
*/
mxSwimlaneLayout.prototype.resizeParent = false;
/**
* Variable: moveParent
*
* Specifies if the parent should be moved if <resizeParent> is enabled.
* Default is false.
*/
mxSwimlaneLayout.prototype.moveParent = false;
/**
* Variable: parentBorder
*
* The border to be added around the children if the parent is to be
* resized using <resizeParent>. Default is 0.
*/
mxSwimlaneLayout.prototype.parentBorder = 30;
/**
* Variable: intraCellSpacing
*
* The spacing buffer added between cells on the same layer. Default is 30.
*/
mxSwimlaneLayout.prototype.intraCellSpacing = 30;
/**
* Variable: interRankCellSpacing
*
* The spacing buffer added between cell on adjacent layers. Default is 50.
*/
mxSwimlaneLayout.prototype.interRankCellSpacing = 100;
/**
* Variable: interHierarchySpacing
*
* The spacing buffer between unconnected hierarchies. Default is 60.
*/
mxSwimlaneLayout.prototype.interHierarchySpacing = 60;
/**
* Variable: parallelEdgeSpacing
*
* The distance between each parallel edge on each ranks for long edges
*/
mxSwimlaneLayout.prototype.parallelEdgeSpacing = 10;
/**
* Variable: orientation
*
* The position of the root node(s) relative to the laid out graph in.
* Default is <mxConstants.DIRECTION_NORTH>.
*/
mxSwimlaneLayout.prototype.orientation = mxConstants.DIRECTION_NORTH;
/**
* Variable: fineTuning
*
* Whether or not to perform local optimisations and iterate multiple times
* through the algorithm. Default is true.
*/
mxSwimlaneLayout.prototype.fineTuning = true;
/**
*
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells.
*/
mxSwimlaneLayout.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.
*/
mxSwimlaneLayout.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
*/
mxSwimlaneLayout.prototype.traverseAncestors = true;
/**
* Variable: model
*
* The internal <mxSwimlaneModel> formed of the layout.
*/
mxSwimlaneLayout.prototype.model = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxSwimlaneLayout.prototype.edgesCache = null;
/**
* Function: getModel
*
* Returns the internal <mxSwimlaneModel> for this layout algorithm.
*/
mxSwimlaneLayout.prototype.getModel = function()
{
return this.model;
};
/**
* Function: execute
*
* Executes the layout for the children of the specified parent.
*
* Parameters:
*
* parent - Parent <mxCell> that contains the children to be laid out.
* swimlanes - Ordered array of swimlanes to be laid out
*/
mxSwimlaneLayout.prototype.execute = function(parent, swimlanes)
{
this.parent = parent;
var model = this.graph.model;
this.edgesCache = new Object();
// 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 (swimlanes == null || swimlanes.length < 1)
{
// TODO indicate the problem
return;
}
if (parent == null)
{
parent = model.getParent(swimlanes[0]);
}
this.swimlanes = swimlanes;
this.dummyVertices = [];
// Check the swimlanes all have vertices
// in them
for (var i = 0; i < swimlanes.length; i++)
{
var children = this.graph.getChildCells(swimlanes[i]);
if (children == null || children.length == 0)
{
var vertex = this.graph.insertVertex(swimlanes[i], null, null, 0, 0, this.dummyVertexWidth, 0);
this.dummyVertices.push(vertex);
}
}
model.beginUpdate();
try
{
this.run(parent);
if (this.resizeParent &&
!this.graph.isCellCollapsed(parent))
{
this.updateGroupBounds();
}
this.graph.removeCells(this.dummyVertices);
}
finally
{
model.endUpdate();
}
};
/**
* Function: updateGroupBounds
*
* Updates the bounds of the given array of groups so that it includes
* all child vertices.
*
*/
mxSwimlaneLayout.prototype.updateGroupBounds = function()
{
// Get all vertices and edge in the layout
var cells = [];
var model = this.model;
for (var key in model.edgeMapper)
{
var edge = model.edgeMapper[key];
for (var i = 0; i < edge.edges.length; i++)
{
cells.push(edge.edges[i]);
}
}
var layoutBounds = this.graph.getBoundingBoxFromGeometry(cells, true);
var childBounds = [];
for (var i = 0; i < this.swimlanes.length; i++)
{
var lane = this.swimlanes[i];
var geo = this.graph.getCellGeometry(lane);
if (geo != null)
{
var children = this.graph.getChildCells(lane);
var size = (this.graph.isSwimlane(lane)) ?
this.graph.getStartSize(lane) : new mxRectangle();
var bounds = this.graph.getBoundingBoxFromGeometry(children);
childBounds[i] = bounds;
var childrenY = bounds.y + geo.y - size.height - this.parentBorder;
var maxChildrenY = bounds.y + geo.y + bounds.height;
if (layoutBounds == null)
{
layoutBounds = new mxRectangle(0, childrenY, 0, maxChildrenY - childrenY);
}
else
{
layoutBounds.y = Math.min(layoutBounds.y, childrenY);
var maxY = Math.max(layoutBounds.y + layoutBounds.height, maxChildrenY);
layoutBounds.height = maxY - layoutBounds.y;
}
}
}
for (var i = 0; i < this.swimlanes.length; i++)
{
var lane = this.swimlanes[i];
var geo = this.graph.getCellGeometry(lane);
if (geo != null)
{
var children = this.graph.getChildCells(lane);
var size = (this.graph.isSwimlane(lane)) ?
this.graph.getStartSize(lane) : new mxRectangle();
var newGeo = geo.clone();
var leftGroupBorder = (i == 0) ? this.parentBorder : this.interRankCellSpacing/2;
newGeo.x += childBounds[i].x - size.width - leftGroupBorder;
newGeo.y = newGeo.y + layoutBounds.y - geo.y - this.parentBorder;
newGeo.width = childBounds[i].width + size.width + this.interRankCellSpacing/2 + leftGroupBorder;
newGeo.height = layoutBounds.height + size.height + 2 * this.parentBorder;
this.graph.model.setGeometry(lane, newGeo);
this.graph.moveCells(children, -childBounds[i].x + size.width + leftGroupBorder,
geo.y - layoutBounds.y + this.parentBorder);
}
}
};
/**
* 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 - <mxCell> whose children should be checked.
* vertices - array of vertices to limit search to
*/
mxSwimlaneLayout.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 (cell != null && model.isVertex(cell) && this.graph.isCellVisible(cell) && model.isAncestor(parent, 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)
{
// Only count connection within this swimlane
var other = this.getVisibleTerminal(conns[k], false);
if (model.isAncestor(parent, other))
{
fanOut++;
}
}
else if (model.isAncestor(parent, src))
{
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 - <mxCell> whose edges should be returned.
*/
mxSwimlaneLayout.prototype.getEdges = function(cell)
{
var cellID = mxCellPath.create(cell);
if (this.edgesCache[cellID] != null)
{
return this.edgesCache[cellID];
}
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.graph.isValidAncestor(source, this.parent, this.traverseAncestors))) ||
(source == cell && (this.parent == null ||
this.graph.isValidAncestor(target, this.parent, this.traverseAncestors))))))
{
result.push(edges[i]);
}
}
this.edgesCache[cellID] = result;
return result;
};
/**
* Function: getVisibleTerminal
*
* Helper function to return visible terminal for edge allowing for ports
*
* Parameters:
*
* edge - <mxCell> whose edges should be returned.
* source - Boolean that specifies whether the source or target terminal is to be returned
*/
mxSwimlaneLayout.prototype.getVisibleTerminal = function(edge, source)
{
var state = this.graph.view.getState(edge);
var terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
if (this.isPort(terminal))
{
terminal = this.graph.model.getParent(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.
*/
mxSwimlaneLayout.prototype.run = function(parent)
{
// Separate out unconnected hierarchies
var hierarchyVertices = [];
var allVertexSet = [];
if (this.swimlanes != null && this.swimlanes.length > 0 && parent != null)
{
var filledVertexSet = Object();
for (var i = 0; i < this.swimlanes.length; i++)
{
this.filterDescendants(this.swimlanes[i], filledVertexSet);
}
this.roots = [];
var filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
// Only test for candidates in each swimlane in order
var laneCounter = 0;
while (!filledVertexSetEmpty && laneCounter < this.swimlanes.length)
{
var candidateRoots = this.findRoots(this.swimlanes[laneCounter], filledVertexSet);
if (candidateRoots.length == 0)
{
laneCounter++;
continue;
}
// 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, laneCounter);
}
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);
}
}
var tmp = [];
for (var key in allVertexSet)
{
tmp.push(allVertexSet[key]);
}
this.model = new mxSwimlaneModel(this, tmp, this.roots,
parent, this.tightenToSource);
this.cycleStage(parent);
this.layeringStage();
this.crossingStage(parent);
initialX = this.placementStage(0, parent);
};
/**
* Function: filterDescendants
*
* Creates an array of descendant cells
*/
mxSwimlaneLayout.prototype.filterDescendants = function(cell, result)
{
var model = this.graph.model;
if (model.isVertex(cell) && cell != this.parent && model.getParent(cell) != this.parent && this.graph.isCellVisible(cell))
{
result[mxCellPath.create(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 - <mxCell> that represents the port.
*/
mxSwimlaneLayout.prototype.isPort = function(cell)
{
if (cell.geometry.relative)
{
return true;
}
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 -
*/
mxSwimlaneLayout.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 - <mxCell> 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 <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* allVertices - Array of cell paths for the visited cells.
* swimlaneIndex - the laid out order index of the swimlane vertex is contained in
*/
mxSwimlaneLayout.prototype.traverse = function(vertex, directed, edge, allVertices, currentComp,
hierarchyVertices, filledVertexSet, swimlaneIndex)
{
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 = mxCellPath.create(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 model = this.graph.model;
for (var i = 0; i < edges.length; i++)
{
var otherVertex = this.getVisibleTerminal(edges[i], true);
var isSource = otherVertex == vertex;
if (isSource)
{
otherVertex = this.getVisibleTerminal(edges[i], false);
}
var otherIndex = 0;
// Get the swimlane index of the other terminal
for (var otherIndex = 0; otherIndex < this.swimlanes.length; otherIndex++)
{
if (model.isAncestor(this.swimlanes[otherIndex], otherVertex))
{
break;
}
}
if (otherIndex >= this.swimlanes.length)
{
continue;
}
// Traverse if the other vertex is within the same swimlane as
// as the current vertex, or if the swimlane index of the other
// vertex is greater than that of this vertex
if ((otherIndex > swimlaneIndex) ||
((!directed || isSource) && otherIndex == swimlaneIndex))
{
currentComp = this.traverse(otherVertex, directed, edges[i], allVertices,
currentComp, hierarchyVertices,
filledVertexSet, otherIndex);
}
}
}
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 currentComp)
{
comp[key] = currentComp[key];
}
// Remove the current component from the hierarchy set
hierarchyVertices.pop();
return comp;
}
}
}
}
}
return currentComp;
};
/**
* Function: cycleStage
*
* Executes the cycle stage using mxMinimumCycleRemover.
*/
mxSwimlaneLayout.prototype.cycleStage = function(parent)
{
var cycleStage = new mxSwimlaneOrdering(this);
cycleStage.execute(parent);
};
/**
* Function: layeringStage
*
* Implements first stage of a Sugiyama layout.
*/
mxSwimlaneLayout.prototype.layeringStage = function()
{
this.model.initialRank();
this.model.fixRanks();
};
/**
* Function: crossingStage
*
* Executes the crossing stage using mxMedianHybridCrossingReduction.
*/
mxSwimlaneLayout.prototype.crossingStage = function(parent)
{
var crossingStage = new mxMedianHybridCrossingReduction(this);
crossingStage.execute(parent);
};
/**
* Function: placementStage
*
* Executes the placement stage using mxCoordinateAssignment.
*/
mxSwimlaneLayout.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;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment