909 lines
26 KiB
JavaScript
Vendored
909 lines
26 KiB
JavaScript
Vendored
/**
|
|
* Copyright (c) 2006-2018, JGraph Ltd
|
|
* Copyright (c) 2006-2018, Gaudenz Alder
|
|
*/
|
|
/**
|
|
* Class: mxWebColaAdaptor
|
|
*
|
|
* Extends WebCola's cola object to act as both adaptor and layout in WebCola for mxGraph.
|
|
*
|
|
* Constructor: mxWebColaAdaptor
|
|
*
|
|
* Constructs a new WebCola-based adaptor for given mxGraph.
|
|
*
|
|
* Arguments:
|
|
*
|
|
* graph - <mxGraph> that contains the cells.
|
|
* dimension - <[]> array containing [width, height] of canvas in points
|
|
* movableVertices - <[]> IDs of vertices that are movable; if undefined all vertices are movable
|
|
* options - <{}> WebCola options for layout/adapter
|
|
*
|
|
**/
|
|
var doNothing = function()
|
|
/**
|
|
* Empty method for default event handlers
|
|
*/
|
|
{
|
|
}
|
|
|
|
function mxWebColaAdaptor(graph, dimension, movableVertices, options)
|
|
/**
|
|
* Constructs a WebCola adaptor for mxGraph
|
|
* @param graph mxGraph instance
|
|
* @param dimension array containing [width, height] of drawing canvas in points
|
|
* @param movableVertices set containing IDs of vertices that are movable; if undefined all vertices are movable
|
|
* @param options WebCola options for layout/adapter
|
|
* @constructor
|
|
*/
|
|
{
|
|
this.graph = graph;
|
|
this.dimension = dimension;
|
|
if (typeof dimension === 'undefined')
|
|
{
|
|
this.dimension = [600, 600];
|
|
}
|
|
// compute vertex/group degrees from incidence
|
|
this.vertexDegrees = new mxDictionary();
|
|
this.groupDegrees = new mxDictionary();
|
|
this.computeVertexDegrees();
|
|
// convert draw.io graph to WebCola's nodes/links
|
|
var layoutResult = this.graphToLayout(graph, movableVertices);
|
|
this.nodes = layoutResult.nodes;
|
|
this.links = layoutResult.links;
|
|
this.groups = layoutResult.groups;
|
|
this.cellToNode = layoutResult.cellToNode;
|
|
this.isStopped = false;
|
|
this.options = {};
|
|
// assign default values
|
|
for (var key in this.defaultValues)
|
|
{
|
|
this.options[key] = this.defaultValues[key];
|
|
}
|
|
// if options were passed, override defaults for keys available in options
|
|
if (options != null)
|
|
{
|
|
for (var key in options)
|
|
{
|
|
this.options[key] = options[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// default layout options
|
|
mxWebColaAdaptor.prototype.defaultValues = {
|
|
doAnimations: true, // whether to show the layout as it's running
|
|
// doAnimations: false, // whether to show the layout as it's running
|
|
skipFrames: 1, // number of ticks per frame; higher is faster but more jerky
|
|
maxSimulationTime: 4000, // max length in ms to run the layout
|
|
ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
|
|
fit: true, // on every layout reposition of nodes, fit the viewport
|
|
padding: 30, // padding around the simulation
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node
|
|
|
|
// layout event callbacks
|
|
ready: function ready() {}, // on layoutready
|
|
stop: function stop() {}, // on layoutstop
|
|
|
|
// positioning options
|
|
randomize: false, // use random node positions at beginning of layout
|
|
avoidOverlap: true, // if true, prevents overlap of node bounding boxes
|
|
handleDisconnected: true, // if true, avoids disconnected components from overlapping
|
|
nodeSpacing: function nodeSpacing(node) {
|
|
return 10;
|
|
}, // extra spacing around nodes
|
|
flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 }
|
|
alignment: undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } }
|
|
gapInequalities: undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}]
|
|
|
|
// different methods of specifying edge length
|
|
// each can be a constant numerical value or a function like `function( edge ){ return 2; }`
|
|
edgeLength: undefined, // sets edge length directly in simulation
|
|
edgeSymDiffLength: undefined, // symmetric diff edge length in simulation
|
|
edgeJaccardLength: undefined, // jaccard edge length in simulation
|
|
|
|
// iterations of cola algorithm; uses default values on undefined
|
|
unconstrIter: undefined, // unconstrained initial layout iterations
|
|
userConstIter: undefined, // initial layout iterations with user-specified constraints
|
|
allConstIter: undefined, // initial layout iterations with all constraints including non-overlap
|
|
|
|
// infinite layout options
|
|
keepRunning: false // overrides all other options for a forces-all-the-time mode
|
|
};
|
|
|
|
mxWebColaAdaptor.prototype.updatePositions = function(isUndoable)
|
|
/**
|
|
* Default method for updating positions
|
|
* Should be overridden by the caller/user of the adaptor
|
|
*/
|
|
{
|
|
console.log("colaAdaptor: updatePositions");
|
|
// TODO: do all the positions here
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.kick = function (colaAdaptor)
|
|
/**
|
|
* Starts WebCola computation on the given adaptor
|
|
*/
|
|
{
|
|
console.log("colaAdaptor: step");
|
|
|
|
if ('doAnimations' in this.options && this.options.doAnimations)
|
|
{
|
|
doRendering(this.callback);
|
|
}
|
|
else
|
|
{
|
|
// run until the end
|
|
while (!this.process(colaAdaptor))
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.step = function (colaAdaptor)
|
|
/**
|
|
* Notifies about a single layout computation step on WebCola adaptor
|
|
*/
|
|
{
|
|
if ('doAnimations' in this.options && this.options.doAnimations)
|
|
{
|
|
this.updatePositions(false);
|
|
}
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.frameSteps = function(colaAdaptor)
|
|
/**
|
|
* Runs multiple ticks on WebCola adaptor until finished
|
|
*/
|
|
{
|
|
var result = void 0;
|
|
|
|
for (var i = 0; i < this.options.skipFrames && !result; i++) {
|
|
result = result || this.process(colaAdaptor);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.process = function(colaAdaptor)
|
|
/**
|
|
* Executes the whole layout computation on WebCola adaptor
|
|
*/
|
|
{
|
|
if (this.isStopped)
|
|
{
|
|
this.finish();
|
|
return true;
|
|
}
|
|
var result = colaAdaptor.tick();
|
|
if (result && this.options.keepRunning) {
|
|
colaAdaptor.resume();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.renderingChain = function(colaAdaptor)
|
|
/**
|
|
* This keeps rendering new simulation frames until end is reached
|
|
*/
|
|
{
|
|
if (this.process(colaAdaptor))
|
|
{
|
|
return;
|
|
}
|
|
doRendering(this.callback);
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.finish = function()
|
|
{
|
|
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.run = function()
|
|
/**
|
|
* Runs the layout computation on given nodes/links/groups
|
|
* @returns Nothing
|
|
*/
|
|
{
|
|
var layout = this;
|
|
var options = this.options;
|
|
|
|
var colaAdaptor = layout.adaptor = cola.adaptor
|
|
({
|
|
trigger: function (evt)
|
|
{
|
|
var START = cola.EventType ? cola.EventType.start : 'start';
|
|
var TICK = cola.EventType ? cola.EventType.tick : 'tick';
|
|
var END = cola.EventType ? cola.EventType.end : 'end';
|
|
|
|
switch (evt.type)
|
|
{
|
|
case START:
|
|
{
|
|
// colaAdaptor.start();
|
|
}
|
|
break;
|
|
case TICK:
|
|
{
|
|
layout.step();
|
|
}
|
|
break;
|
|
case END:
|
|
{
|
|
console.log("colaAdaptor: end");
|
|
layout.updatePositions(true);
|
|
if (!options.keepRunning)
|
|
{
|
|
layout.finish();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
kick: function ()
|
|
{
|
|
layout.kick(colaAdaptor);
|
|
},
|
|
|
|
finish: function()
|
|
{
|
|
layout.finish();
|
|
},
|
|
|
|
on: doNothing,
|
|
|
|
drag: doNothing
|
|
});
|
|
|
|
colaAdaptor.nodes(this.nodes)
|
|
.links(this.links)
|
|
.groups(this.groups)
|
|
.size(this.dimension)
|
|
.linkDistance(function (link)
|
|
{
|
|
return link.length;
|
|
});
|
|
|
|
layout.callback = function()
|
|
{
|
|
layout.renderingChain(colaAdaptor);
|
|
}
|
|
|
|
colaAdaptor.avoidOverlaps(options.avoidOverlap)
|
|
.handleDisconnected(options.handleDisconnected)
|
|
// .constraints(constraints)
|
|
// .start(100, 100, 100);
|
|
.start();
|
|
return this.adaptor;
|
|
}
|
|
|
|
function getScreenConstraints(layout, width, height)
|
|
/**
|
|
* Returns a set of constraints covering limits of screen
|
|
* @param layout
|
|
* @param width
|
|
* @param height
|
|
* @returns {Array}
|
|
*/
|
|
{
|
|
var gap = 20;
|
|
var size = layout._nodes.length;
|
|
var topLeft = {x: 0, y: 0, fixed: true, index: size};
|
|
var bottomRight = {x: width, y: height, fixed: true, index: size + 1};
|
|
layout._nodes.push(topLeft);
|
|
layout._nodes.push(bottomRight);
|
|
var constraints = [];
|
|
for (var i = 0; i < size; i++) {
|
|
var index = layout._nodes[i].index;
|
|
constraints.push({ axis: 'x', type: 'separation', left: topLeft.index, right: index, gap: gap });
|
|
constraints.push({ axis: 'y', type: 'separation', left: topLeft.index, right: index, gap: gap });
|
|
constraints.push({ axis: 'x', type: 'separation', left: index, right: bottomRight.index, gap: gap });
|
|
constraints.push({ axis: 'y', type: 'separation', left: index, right: bottomRight.index, gap: gap });
|
|
}
|
|
return constraints;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.graphToLayout = function(graph, movableVertices)
|
|
/**
|
|
* Returns a WebCola layout set up for the given Draw.io graph
|
|
* In WebCola's TypeScript source: vertex cell -> InputNode
|
|
* edge cell -> Link
|
|
* parent/child -> Group
|
|
* @param graph Draw.io graph object
|
|
* @param fixedVertices Vertices that shouldn't be moved (dictionary with {id: True} pairs, id is vertex id)
|
|
* optional, if undefined all vertices are considered movable
|
|
* @returns list of WebCola nodes, list of WebCola links, and a dictionary from Draw.io cell ID to WebCola node ID
|
|
* returned as a dictionary: {nodes: ..., links: ..., cellToNode: ...}
|
|
*/
|
|
{
|
|
var activeMaps = this.findActiveVertices(graph); // list of all active vertices, i.e. with no collapsed parents
|
|
var activeVertices = activeMaps.activeVertices; // inactive vertex to its nearest active parent map
|
|
var inactiveToActiveMap = activeMaps.inactiveToActiveMap;
|
|
var model = graph.getModel();
|
|
var cells = model.cells;
|
|
var view = graph.getView();
|
|
var cellSpacing = 20;
|
|
|
|
// Ignores cells that have no states
|
|
var tmp = {};
|
|
|
|
for (var id in cells)
|
|
{
|
|
if (view.getState(cells[id]) != null)
|
|
{
|
|
tmp[id] = cells[id];
|
|
}
|
|
}
|
|
|
|
cells = tmp;
|
|
|
|
var nodeCells = {};
|
|
var linkCells = {};
|
|
var cellIds = {};
|
|
var edgeIds = {};
|
|
var colaId = 0;
|
|
var nodes = [];
|
|
var links = [];
|
|
// process nodes first
|
|
for (var id in cells)
|
|
{
|
|
var cell = cells[id];
|
|
var state = view.getState(cell);
|
|
var bounds = view.getBoundingBox(state, true);
|
|
var bounds = model.getGeometry(cell);
|
|
var isFirst = true;
|
|
// if (cell.isVertex() && this.isLeafOrCollapsed(cell)) {
|
|
// only active vertices should be considered (i.e. not hidden by a collapsed or layouted vertex)
|
|
// if (cell.isVertex() && activeVertices[cell.id])
|
|
if (cell.isVertex() && this.isLeafOrCollapsed(cell) && activeVertices[cell.id])
|
|
{
|
|
var node = {};
|
|
// node.x = bounds.getCenterX();
|
|
// node.y = bounds.getCenterY();
|
|
node.width = bounds.width + cellSpacing;
|
|
node.height = bounds.height + cellSpacing;
|
|
node.index = colaId;
|
|
node.name = cell.value;
|
|
node.fixed = false;
|
|
if (typeof movableVertices !== 'undefined' && !(id in movableVertices))
|
|
{
|
|
node.fixed = true;
|
|
}
|
|
nodes.push(node);
|
|
cellIds[id] = colaId;
|
|
nodeCells[colaId] = cell;
|
|
colaId++;
|
|
}
|
|
}
|
|
// now edges can be processed as well
|
|
for (var id in cells)
|
|
{
|
|
var cell = cells[id];
|
|
var state = view.getState(cell);
|
|
if (cell.isEdge() && cell.getTerminal(true) != null && cell.getTerminal(false) != null)
|
|
{
|
|
// attach edges to lowest active vertex corresponding to each of their terminals
|
|
var terminal_id1 = inactiveToActiveMap[cell.source.id];
|
|
var terminal_id2 = inactiveToActiveMap[cell.target.id];
|
|
if (terminal_id1 == terminal_id2)
|
|
{
|
|
// both terminals are under the same active parent, no need to make an invisible edge
|
|
continue;
|
|
}
|
|
// if either of terminals are groups, we need to insert complete graph between nodes within these groups
|
|
var terminal1 = cells[terminal_id1];
|
|
var terminal2 = cells[terminal_id2];
|
|
var addedLinks = [];
|
|
if (this.isGroup(terminal1) || this.isGroup(terminal2))
|
|
{
|
|
addedLinks = this.addGroupConstraintLinks(terminal1, terminal2, activeVertices, inactiveToActiveMap, cellIds);
|
|
}
|
|
else
|
|
{
|
|
// link = {}
|
|
// link.source = cellIds[cell.source.id];
|
|
// link.target = cellIds[cell.target.id];
|
|
var link = this.createLink(terminal_id1, terminal_id2, cellIds);
|
|
addedLinks.push(link);
|
|
}
|
|
for (var i = 0; i < addedLinks.length; i++)
|
|
{
|
|
var link = addedLinks[i];
|
|
links.push(link);
|
|
edgeIds[cell] = id;
|
|
linkCells[link] = cell;
|
|
}
|
|
}
|
|
}
|
|
links = this.getUniqueLinks(links);
|
|
// finally, groups need to be extracted
|
|
// mxGraph.getCellsForGroup
|
|
// mxGraphModel.getChildCount
|
|
// mxGraph.getBoundsForGroup
|
|
// first, get all possible parents and their children
|
|
var groupParents = {};
|
|
var directParentChildren = {};
|
|
for (var id in cells)
|
|
{
|
|
var cell = cells[id];
|
|
if (!cell.isVertex() || !this.isLeafOrCollapsed(cell))
|
|
continue;
|
|
var parent = cell.getParent();
|
|
if (parent.isVertex())
|
|
{
|
|
groupParents[parent.id] = parent;
|
|
if (!(parent.id in directParentChildren))
|
|
{
|
|
directParentChildren[parent.id] = {}
|
|
}
|
|
directParentChildren[parent.id][id] = cell;
|
|
}
|
|
}
|
|
// now go through all parents/children and build a group hierarchy for WebCola
|
|
var preliminaryGroups = [];
|
|
var groupId = 0;
|
|
var groupToParent = {}
|
|
for (var parentId in groupParents)
|
|
{
|
|
var parentChildren = directParentChildren[parentId];
|
|
var groupNodes = []
|
|
for (var childId in parentChildren)
|
|
{
|
|
if (activeVertices[childId])
|
|
{
|
|
groupNodes.push(cellIds[childId]);
|
|
}
|
|
}
|
|
preliminaryGroups.push({id: groupId, parentId: parentId, nodes: parentChildren, leaves: groupNodes, groups: []});
|
|
groupToParent[groupId] = parentId;
|
|
groupId++;
|
|
}
|
|
// here scan newly formed groups if their parent is a child of any of the nodes in any of the groups
|
|
for (var i = 0; i < preliminaryGroups.length; i++)
|
|
{
|
|
var parentGroup = preliminaryGroups[i];
|
|
var parentId = parentGroup.parentId;
|
|
for (var j = 0; j < preliminaryGroups.length; j++)
|
|
{
|
|
if (i == j)
|
|
continue;
|
|
var groupParentId = cells[preliminaryGroups[j].parentId].getParent().id;
|
|
if (parentId == groupParentId)
|
|
parentGroup.groups.push(j);
|
|
}
|
|
}
|
|
// finalize groups
|
|
var groups = [];
|
|
for (var i = 0; i < preliminaryGroups.length; i++)
|
|
{
|
|
var group = preliminaryGroups[i];
|
|
var graphGroup = {};
|
|
if (group.leaves.length > 0)
|
|
{
|
|
graphGroup["leaves"] = group.leaves;
|
|
}
|
|
if (group.groups.length > 0)
|
|
{
|
|
graphGroup["groups"] = group.groups;
|
|
}
|
|
if (graphGroup.hasOwnProperty("leaves") || graphGroup.hasOwnProperty("groups"))
|
|
{
|
|
groups.push(graphGroup);
|
|
}
|
|
}
|
|
|
|
return {nodes: nodes, links: links, groups: groups, cellToNode: cellIds};
|
|
};
|
|
|
|
mxWebColaAdaptor.prototype.createLink = function(sourceId, targetId, cellIds)
|
|
/**
|
|
* Creates a default version of a WebCola link/edge
|
|
* @param sourceId ID of the edge's source vertex cell
|
|
* @param targetId ID of the edge's target vertex cell
|
|
* @param cellIds cell ID to WebCola's node ID mapping
|
|
* @returns a WebCola link corresponding to the edge [sourceId, targetId]
|
|
* in WebCola node IDs
|
|
*/
|
|
{
|
|
var link = {};
|
|
link.source = cellIds[sourceId];
|
|
link.target = cellIds[targetId];
|
|
link.weight = 0.5;
|
|
link.length = 200; //Graph.prototype.defaultEdgeLength;
|
|
return link;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.computeVertexDegrees = function()
|
|
/**
|
|
* Computes group vertex and vertex degrees. Useful to stop layouting for groups
|
|
* with no internal, incoming or outgoing edges.
|
|
*/
|
|
{
|
|
var model = this.graph.getModel();
|
|
var cells = model.cells;
|
|
|
|
// compute individual vertex degrees
|
|
for (var id in cells)
|
|
{
|
|
var cell = cells[id];
|
|
if (cell.isEdge() && cell.source != null && cell.target != null)
|
|
{
|
|
// scan all edges, ignore other types
|
|
var sourceId = cell.source.id;
|
|
var targetId = cell.target.id;
|
|
var source = cells[sourceId];
|
|
var target = cells[targetId];
|
|
|
|
if (sourceId == targetId)
|
|
{
|
|
// self-loops are irrelevant
|
|
continue;
|
|
}
|
|
|
|
var sourceDegree = this.vertexDegrees.get(source);
|
|
if (typeof sourceDegree == "undefined")
|
|
{
|
|
sourceDegree = 0;
|
|
}
|
|
sourceDegree++;
|
|
this.vertexDegrees.put(source, sourceDegree);
|
|
|
|
var targetDegree = this.vertexDegrees.get(target);
|
|
if (typeof targetDegree == "undefined")
|
|
{
|
|
targetDegree = 0;
|
|
}
|
|
targetDegree++;
|
|
this.vertexDegrees.put(target, targetDegree);
|
|
}
|
|
}
|
|
// compute sub-group degree, i.e. sum of all degrees of children
|
|
// algorithm goes through all vertices, then for each vertex it goes on its material
|
|
// path to root and adds its contribution to all vertices on this path
|
|
// this should for each vertex place exactly the sum of degrees of all its vertices
|
|
// and itself, nothing less, nothing more
|
|
for (var id in cells)
|
|
{
|
|
var cell = cells[id];
|
|
if (cell.isVertex())
|
|
{
|
|
// scan all vertices, ignore other types
|
|
var vertexDegree = this.vertexDegrees.get(cell);
|
|
if (typeof vertexDegree == "undefined")
|
|
{
|
|
vertexDegree = 0;
|
|
}
|
|
var parent = cell;
|
|
while (parent != null && typeof parent != "undefined")
|
|
{
|
|
var groupDegree = this.groupDegrees.get(parent);
|
|
if (typeof groupDegree == "undefined")
|
|
{
|
|
groupDegree = 0;
|
|
}
|
|
groupDegree += vertexDegree;
|
|
this.groupDegrees.put(parent, groupDegree);
|
|
parent = parent.parent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.isZeroConnected = function(groupCell)
|
|
/**
|
|
* Indicates if all group cell's vertices have no incidental edges
|
|
* @params groupCell group cell
|
|
* @returns true if the group cell doesn't contain any vertices with edges
|
|
*/
|
|
{
|
|
var groupDegree = this.groupDegrees.get(groupCell);
|
|
console.log("Group " + groupCell.id + " degree: " + groupDegree);
|
|
if (typeof groupDegree != "undefined" && groupDegree > 0)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.isInZeroConnectedGroup = function(cell)
|
|
{
|
|
var parent = cell.parent;
|
|
if (parent == null || typeof parent == "undefined")
|
|
{
|
|
return this.isZeroConnected(cell);
|
|
}
|
|
else
|
|
{
|
|
return this.isZeroConnected(parent);
|
|
}
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.isLeafOrCollapsed = function(cell)
|
|
/**
|
|
* Returns true if a cell is either a leaf or a collapsed group
|
|
* @param cell cell to investigate
|
|
* @returns true if a cell is either a leaf or a collapsed group, false otherwise
|
|
*/
|
|
{
|
|
if (cell.isCollapsed() ||
|
|
cell.children == null || cell.children.length == 0 ||
|
|
typeof this.graph.getCellStyle(cell)['childLayout'] != 'undefined')
|
|
{
|
|
return true;
|
|
}
|
|
if (this.isZeroConnected(cell))
|
|
{
|
|
return true;
|
|
}
|
|
/*
|
|
if (!cell.isCollapsed() && cell.children != null && cell.children.length > 0 && this.graph.getEdges(cell, null, true, true, true, true).length == 0)
|
|
{
|
|
console.log("cell " + cell.id + " is 0-connected.");
|
|
return true;
|
|
}
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.findActiveVertices = function(graph)
|
|
/**
|
|
* Scans all groups and finds active vertices, as well as an inactive-vertex-to-active-parent map
|
|
* @param graph input graph
|
|
*/
|
|
{
|
|
var inactiveToActiveMap = {};
|
|
var activeVertices = {};
|
|
var root = graph.getModel().root;
|
|
var cellsToExplore = [{vertex: root, isActive: true, activeParent: root}]
|
|
while (cellsToExplore.length > 0)
|
|
{
|
|
var currentCellInfo = cellsToExplore.shift();
|
|
var cell = currentCellInfo.vertex;
|
|
if (cell.isEdge())
|
|
{
|
|
// cut at edge group, those are ignored
|
|
continue;
|
|
}
|
|
var isActive = currentCellInfo.isActive;
|
|
var activeParent = currentCellInfo.activeParent;
|
|
if (cell.isVertex())
|
|
{
|
|
if (isActive)
|
|
{
|
|
activeVertices[cell.id] = true;
|
|
}
|
|
else
|
|
{
|
|
activeVertices[cell.id] = false;
|
|
}
|
|
}
|
|
// prepare children
|
|
// child can be active only if any of its parents is not collapsed
|
|
var isActive = isActive && !this.isLeafOrCollapsed(cell);
|
|
var children = cell.children;
|
|
if (children != null && children.length > 0)
|
|
{
|
|
for (var i = 0; i < children.length; i++)
|
|
{
|
|
var child = children[i];
|
|
var childActiveParent = isActive? child: activeParent;
|
|
cellsToExplore.push({vertex: child, isActive: isActive, activeParent: childActiveParent});
|
|
if (child.isVertex())
|
|
{
|
|
inactiveToActiveMap[child.id] = childActiveParent.id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return {activeVertices: activeVertices, inactiveToActiveMap: inactiveToActiveMap};
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.getActiveVerticesInGroup = function(groupCell, activeVertices, includeCollapsedGroups)
|
|
/**
|
|
* Scans all children in group and returns all active vertices inside group
|
|
* This method is for creating redundant edges between members of groups to simulate group edges in WebCola
|
|
* See https://github.com/tgdwyer/WebCola/issues/38
|
|
* @param groupCell group cell
|
|
*/
|
|
{
|
|
var activeChildren = [];
|
|
if (includeCollapsedGroups && this.isLeafOrCollapsed(groupCell))
|
|
{
|
|
activeChildren.push(groupCell);
|
|
}
|
|
var cellsToExplore = [groupCell];
|
|
while (cellsToExplore.length > 0)
|
|
{
|
|
var cell = cellsToExplore.shift();
|
|
if (!cell.isVertex() || !activeVertices[cell])
|
|
{
|
|
// cut at edge group, those are ignored
|
|
continue;
|
|
}
|
|
if (this.isLeafOrCollapsed(cell))
|
|
{
|
|
activeChildren.push(cell);
|
|
}
|
|
else
|
|
{
|
|
var children = cell.children;
|
|
if (children == null || children.length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
cellsToExplore = cellsToExplore.concat(children);
|
|
}
|
|
}
|
|
return activeChildren;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.getAllVerticesInGroup = function(groupCell, includeCollapsedGroups)
|
|
/**
|
|
* Scans all children in group and returns all active vertices inside group
|
|
* This method is for creating redundant edges between members of groups to simulate group edges in WebCola
|
|
* See https://github.com/tgdwyer/WebCola/issues/38
|
|
* @param groupCell group cell
|
|
*/
|
|
{
|
|
var result = [];
|
|
if (includeCollapsedGroups && this.isLeafOrCollapsed(groupCell))
|
|
{
|
|
result.push(groupCell);
|
|
}
|
|
var cellsToExplore = [groupCell];
|
|
while (cellsToExplore.length > 0)
|
|
{
|
|
var cell = cellsToExplore.shift();
|
|
if (!cell.isVertex())
|
|
{
|
|
// cut at edge group, those are ignored
|
|
continue;
|
|
}
|
|
if (this.isLeafOrCollapsed(cell))
|
|
{
|
|
result.push(cell);
|
|
}
|
|
else
|
|
{
|
|
var children = cell.children;
|
|
if (children == null || children.length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
cellsToExplore = cellsToExplore.concat(children);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.hasVertexChildren = function(cell)
|
|
/**
|
|
* Returns true if a (group) cell has vertex children in its subtree
|
|
* @param cell (group) cell
|
|
* @returns true if if a (group) cell has vertex children in its subtree, false otherwise
|
|
*/
|
|
{
|
|
if (cell.children == null || cell.children.length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
var toBeExamined = []
|
|
toBeExamined = toBeExamined.concat(cell.children);
|
|
while (toBeExamined.length > 0)
|
|
{
|
|
var cell = toBeExamined.shift();
|
|
if (cell.isVertex())
|
|
return true;
|
|
if (cell.children != null && cell.children.length > 0)
|
|
{
|
|
toBeExamined = toBeExamined.concat(cell.children);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.isInCollapsedTree = function(cell)
|
|
{
|
|
// scan the material path for collapsed group node
|
|
while (cell != null)
|
|
{
|
|
cell = cell.getParent();
|
|
if (cell != null && cell.isCollapsed())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.isGroup = function(cell)
|
|
/**
|
|
* Returns true if cell is a group (has children)
|
|
* @param cell cell
|
|
* @returns true if cell is a group (has children); false otherwise
|
|
*/
|
|
{
|
|
return cell.children != null && cell.children.length > 0;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.addGroupConstraintLinks = function(groupA, groupB, activeVertices, inactiveToActiveMap, cellIds)
|
|
/**
|
|
* Adds edges between vertex and group or two groups. Each vertex child of a group must be connected to the vertex/
|
|
* group, as this way WebCola simulates edges on the group level (as groups don't exist as vertices in WebCola)
|
|
* @param rootCell root cell
|
|
*/
|
|
{
|
|
var result = []
|
|
// var childrenA = this.getActiveVerticesInGroup(groupA, activeVertices, false);
|
|
// var childrenB = this.getActiveVerticesInGroup(groupB, activeVertices, false);
|
|
var childrenA = [groupA];
|
|
var childrenB = [groupB];
|
|
if (!groupA.isCollapsed())
|
|
{
|
|
childrenA = this.getAllVerticesInGroup(groupA, activeVertices, false);
|
|
}
|
|
if (!groupB.isCollapsed())
|
|
{
|
|
childrenB = this.getAllVerticesInGroup(groupB, activeVertices, false);
|
|
}
|
|
if (childrenA == null || childrenA.length == 0 || childrenB == null || childrenB.length == 0)
|
|
return result;
|
|
for (var i = 0; i < childrenA.length; i++)
|
|
{
|
|
var childA_Id = inactiveToActiveMap[childrenA[i].id];
|
|
for (var j = 0; j < childrenB.length; j++)
|
|
{
|
|
var childB_Id = inactiveToActiveMap[childrenB[j].id];
|
|
var link = this.createLink(childA_Id, childB_Id, cellIds);
|
|
result.push(link);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
mxWebColaAdaptor.prototype.getUniqueLinks = function(links)
|
|
/**
|
|
* Returns an array of unique links from an array of links
|
|
* @param links array of links containing duplicate links
|
|
* @returns array of unique links
|
|
*/
|
|
{
|
|
var result = [];
|
|
// TODO: this part is inefficient - O(n^2); Theta(n) should be possible with hashmap
|
|
for (var i = 0; i < links.length; i++)
|
|
{
|
|
var link = links[i];
|
|
var shouldBeAdded = true;
|
|
for (var j = 0; j < result.length; j++)
|
|
{
|
|
var existingLink = result[j];
|
|
if (link.source == existingLink.source && link.target == existingLink.target)
|
|
{
|
|
shouldBeAdded = false;
|
|
break;
|
|
}
|
|
}
|
|
if (shouldBeAdded)
|
|
{
|
|
result.push(link);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
var doRendering = void 0;
|
|
|
|
if ((typeof window === "undefined" ? "undefined" : typeof(window)) !== ( true ? "undefined" : typeof(undefined)))
|
|
{
|
|
doRendering = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
|
|
}
|
|
else
|
|
{
|
|
// if not available, all you get is immediate calls
|
|
function doRendering(callback)
|
|
{
|
|
callback();
|
|
};
|
|
}
|