1/**
2 * Copyright (c) 2006-2018, JGraph Ltd
3 * Copyright (c) 2006-2018, Gaudenz Alder
4 */
5/**
6 * Class: mxWebColaAdaptor
7 *
8 * Extends WebCola's cola object to act as both adaptor and layout in WebCola for mxGraph.
9 *
10 * Constructor: mxWebColaAdaptor
11 *
12 * Constructs a new WebCola-based adaptor for given mxGraph.
13 *
14 * Arguments:
15 *
16 * graph - <mxGraph> that contains the cells.
17 * dimension - <[]> array containing [width, height] of canvas in points
18 * movableVertices - <[]> IDs of vertices that are movable; if undefined all vertices are movable
19 * options - <{}> WebCola options for layout/adapter
20 *
21 **/
22var doNothing = function()
23  /**
24   * Empty method for default event handlers
25   */
26{
27}
28
29function mxWebColaAdaptor(graph, dimension, movableVertices, options)
30/**
31 * Constructs a WebCola adaptor for mxGraph
32 * @param graph mxGraph instance
33 * @param dimension array containing [width, height] of drawing canvas in points
34 * @param movableVertices set containing IDs of vertices that are movable; if undefined all vertices are movable
35 * @param options WebCola options for layout/adapter
36 * @constructor
37 */
38{
39  this.graph = graph;
40  this.dimension = dimension;
41  if (typeof dimension === 'undefined')
42  {
43    this.dimension = [600, 600];
44  }
45  // compute vertex/group degrees from incidence
46  this.vertexDegrees = new mxDictionary();
47  this.groupDegrees = new mxDictionary();
48  this.computeVertexDegrees();
49  // convert draw.io graph to WebCola's nodes/links
50  var layoutResult = this.graphToLayout(graph, movableVertices);
51  this.nodes = layoutResult.nodes;
52  this.links = layoutResult.links;
53  this.groups = layoutResult.groups;
54  this.cellToNode = layoutResult.cellToNode;
55  this.isStopped = false;
56  this.options = {};
57  // assign default values
58  for (var key in this.defaultValues)
59  {
60    this.options[key] = this.defaultValues[key];
61  }
62  // if options were passed, override defaults for keys available in options
63  if (options != null)
64  {
65    for (var key in options)
66    {
67      this.options[key] = options[key];
68    }
69  }
70}
71
72// default layout options
73mxWebColaAdaptor.prototype.defaultValues = {
74  doAnimations: true, // whether to show the layout as it's running
75  // doAnimations: false, // whether to show the layout as it's running
76  skipFrames: 1, // number of ticks per frame; higher is faster but more jerky
77  maxSimulationTime: 4000, // max length in ms to run the layout
78  ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
79  fit: true, // on every layout reposition of nodes, fit the viewport
80  padding: 30, // padding around the simulation
81  boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
82  nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node
83
84  // layout event callbacks
85  ready: function ready() {}, // on layoutready
86  stop: function stop() {}, // on layoutstop
87
88  // positioning options
89  randomize: false, // use random node positions at beginning of layout
90  avoidOverlap: true, // if true, prevents overlap of node bounding boxes
91  handleDisconnected: true, // if true, avoids disconnected components from overlapping
92  nodeSpacing: function nodeSpacing(node) {
93    return 10;
94  }, // extra spacing around nodes
95  flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 }
96  alignment: undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } }
97  gapInequalities: undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}]
98
99  // different methods of specifying edge length
100  // each can be a constant numerical value or a function like `function( edge ){ return 2; }`
101  edgeLength: undefined, // sets edge length directly in simulation
102  edgeSymDiffLength: undefined, // symmetric diff edge length in simulation
103  edgeJaccardLength: undefined, // jaccard edge length in simulation
104
105  // iterations of cola algorithm; uses default values on undefined
106  unconstrIter: undefined, // unconstrained initial layout iterations
107  userConstIter: undefined, // initial layout iterations with user-specified constraints
108  allConstIter: undefined, // initial layout iterations with all constraints including non-overlap
109
110  // infinite layout options
111  keepRunning: false // overrides all other options for a forces-all-the-time mode
112};
113
114mxWebColaAdaptor.prototype.updatePositions = function(isUndoable)
115  /**
116   * Default method for updating positions
117   * Should be overridden by the caller/user of the adaptor
118   */
119{
120  console.log("colaAdaptor: updatePositions");
121  // TODO: do all the positions here
122}
123
124mxWebColaAdaptor.prototype.kick = function (colaAdaptor)
125/**
126 * Starts WebCola computation on the given adaptor
127 */
128{
129  console.log("colaAdaptor: step");
130
131  if ('doAnimations' in this.options && this.options.doAnimations)
132  {
133    doRendering(this.callback);
134  }
135  else
136  {
137    // run until the end
138    while (!this.process(colaAdaptor))
139    {
140    }
141  }
142}
143
144mxWebColaAdaptor.prototype.step = function (colaAdaptor)
145/**
146 * Notifies about a single layout computation step on WebCola adaptor
147 */
148{
149  if ('doAnimations' in this.options && this.options.doAnimations)
150  {
151    this.updatePositions(false);
152  }
153}
154
155mxWebColaAdaptor.prototype.frameSteps = function(colaAdaptor)
156/**
157 * Runs multiple ticks on WebCola adaptor until finished
158 */
159{
160  var result = void 0;
161
162  for (var i = 0; i < this.options.skipFrames && !result; i++) {
163    result = result || this.process(colaAdaptor);
164  }
165  return result;
166}
167
168mxWebColaAdaptor.prototype.process = function(colaAdaptor)
169/**
170 * Executes the whole layout computation on WebCola adaptor
171 */
172{
173  if (this.isStopped)
174  {
175    this.finish();
176    return true;
177  }
178  var result = colaAdaptor.tick();
179  if (result && this.options.keepRunning) {
180    colaAdaptor.resume();
181  }
182  return result;
183}
184
185mxWebColaAdaptor.prototype.renderingChain = function(colaAdaptor)
186/**
187 * This keeps rendering new simulation frames until end is reached
188 */
189{
190  if (this.process(colaAdaptor))
191  {
192    return;
193  }
194  doRendering(this.callback);
195}
196
197mxWebColaAdaptor.prototype.finish = function()
198{
199
200}
201
202mxWebColaAdaptor.prototype.run = function()
203  /**
204   * Runs the layout computation on given nodes/links/groups
205   * @returns Nothing
206   */
207{
208  var layout = this;
209  var options = this.options;
210
211  var colaAdaptor = layout.adaptor = cola.adaptor
212  ({
213    trigger: function (evt)
214    {
215      var START = cola.EventType ? cola.EventType.start : 'start';
216      var TICK = cola.EventType ? cola.EventType.tick : 'tick';
217      var END = cola.EventType ? cola.EventType.end : 'end';
218
219      switch (evt.type)
220      {
221        case START:
222        {
223          // colaAdaptor.start();
224        }
225        break;
226        case TICK:
227        {
228          layout.step();
229        }
230        break;
231        case END:
232        {
233          console.log("colaAdaptor: end");
234          layout.updatePositions(true);
235          if (!options.keepRunning)
236          {
237            layout.finish();
238          }
239        }
240        break;
241      }
242    },
243
244    kick: function ()
245    {
246      layout.kick(colaAdaptor);
247    },
248
249    finish: function()
250    {
251      layout.finish();
252    },
253
254    on: doNothing,
255
256    drag: doNothing
257  });
258
259  colaAdaptor.nodes(this.nodes)
260             .links(this.links)
261             .groups(this.groups)
262             .size(this.dimension)
263             .linkDistance(function (link)
264              {
265                return link.length;
266              });
267
268  layout.callback = function()
269  {
270    layout.renderingChain(colaAdaptor);
271  }
272
273  colaAdaptor.avoidOverlaps(options.avoidOverlap)
274             .handleDisconnected(options.handleDisconnected)
275             // .constraints(constraints)
276             // .start(100, 100, 100);
277             .start();
278  return this.adaptor;
279}
280
281function getScreenConstraints(layout, width, height)
282/**
283 * Returns a set of constraints covering limits of screen
284 * @param layout
285 * @param width
286 * @param height
287 * @returns {Array}
288 */
289{
290  var gap = 20;
291  var size = layout._nodes.length;
292  var topLeft = {x: 0, y: 0, fixed: true, index: size};
293  var bottomRight = {x: width, y: height, fixed: true, index: size + 1};
294  layout._nodes.push(topLeft);
295  layout._nodes.push(bottomRight);
296  var constraints = [];
297  for (var i = 0; i < size; i++) {
298    var index = layout._nodes[i].index;
299    constraints.push({ axis: 'x', type: 'separation', left: topLeft.index, right: index, gap: gap });
300    constraints.push({ axis: 'y', type: 'separation', left: topLeft.index, right: index, gap: gap });
301    constraints.push({ axis: 'x', type: 'separation', left: index, right: bottomRight.index, gap: gap });
302    constraints.push({ axis: 'y', type: 'separation', left: index, right: bottomRight.index, gap: gap });
303  }
304  return constraints;
305}
306
307mxWebColaAdaptor.prototype.graphToLayout = function(graph, movableVertices)
308/**
309 * Returns a WebCola layout set up for the given Draw.io graph
310 * In WebCola's TypeScript source: vertex cell -> InputNode
311 *                                 edge cell -> Link
312 *                                 parent/child -> Group
313 * @param graph Draw.io graph object
314 * @param fixedVertices Vertices that shouldn't be moved (dictionary with {id: True} pairs, id is vertex id)
315 *                      optional, if undefined all vertices are considered movable
316 * @returns list of WebCola nodes, list of WebCola links, and a dictionary from Draw.io cell ID to WebCola node ID
317 *          returned as a dictionary: {nodes: ..., links: ..., cellToNode: ...}
318 */
319{
320  var activeMaps = this.findActiveVertices(graph);  // list of all active vertices, i.e. with no collapsed parents
321  var activeVertices = activeMaps.activeVertices;  // inactive vertex to its nearest active parent map
322  var inactiveToActiveMap = activeMaps.inactiveToActiveMap;
323  var model = graph.getModel();
324  var cells = model.cells;
325  var view = graph.getView();
326  var cellSpacing = 20;
327
328  // Ignores cells that have no states
329  var tmp = {};
330
331  for (var id in cells)
332  {
333	  if (view.getState(cells[id]) != null)
334	  {
335		  tmp[id] = cells[id];
336	  }
337  }
338
339  cells = tmp;
340
341  var nodeCells = {};
342  var linkCells = {};
343  var cellIds = {};
344  var edgeIds = {};
345  var colaId = 0;
346  var nodes = [];
347  var links = [];
348  // process nodes first
349  for (var id in cells)
350  {
351    var cell = cells[id];
352    var state = view.getState(cell);
353    var bounds = view.getBoundingBox(state, true);
354    var bounds = model.getGeometry(cell);
355    var isFirst = true;
356    // if (cell.isVertex() && this.isLeafOrCollapsed(cell)) {
357    // only active vertices should be considered (i.e. not hidden by a collapsed or layouted vertex)
358    // if (cell.isVertex() && activeVertices[cell.id])
359    if (cell.isVertex() && this.isLeafOrCollapsed(cell) && activeVertices[cell.id])
360    {
361      var node = {};
362      // node.x = bounds.getCenterX();
363      // node.y = bounds.getCenterY();
364      node.width = bounds.width + cellSpacing;
365      node.height = bounds.height + cellSpacing;
366      node.index = colaId;
367      node.name = cell.value;
368      node.fixed = false;
369      if (typeof movableVertices !== 'undefined' && !(id in movableVertices))
370      {
371        node.fixed = true;
372      }
373      nodes.push(node);
374      cellIds[id] = colaId;
375      nodeCells[colaId] = cell;
376      colaId++;
377    }
378  }
379  // now edges can be processed as well
380  for (var id in cells)
381  {
382    var cell = cells[id];
383    var state = view.getState(cell);
384    if (cell.isEdge() && cell.getTerminal(true) != null && cell.getTerminal(false) != null)
385    {
386      // attach edges to lowest active vertex corresponding to each of their terminals
387      var terminal_id1 = inactiveToActiveMap[cell.source.id];
388      var terminal_id2 = inactiveToActiveMap[cell.target.id];
389      if (terminal_id1 == terminal_id2)
390      {
391        // both terminals are under the same active parent, no need to make an invisible edge
392        continue;
393      }
394      // if either of terminals are groups, we need to insert complete graph between nodes within these groups
395      var terminal1 = cells[terminal_id1];
396      var terminal2 = cells[terminal_id2];
397      var addedLinks = [];
398      if (this.isGroup(terminal1) || this.isGroup(terminal2))
399      {
400        addedLinks = this.addGroupConstraintLinks(terminal1, terminal2, activeVertices, inactiveToActiveMap, cellIds);
401      }
402      else
403      {
404        // link = {}
405        // link.source = cellIds[cell.source.id];
406        // link.target = cellIds[cell.target.id];
407        var link = this.createLink(terminal_id1, terminal_id2, cellIds);
408        addedLinks.push(link);
409      }
410      for (var i = 0; i < addedLinks.length; i++)
411      {
412        var link = addedLinks[i];
413        links.push(link);
414        edgeIds[cell] = id;
415        linkCells[link] = cell;
416      }
417    }
418  }
419  links = this.getUniqueLinks(links);
420  // finally, groups need to be extracted
421  // mxGraph.getCellsForGroup
422  // mxGraphModel.getChildCount
423  // mxGraph.getBoundsForGroup
424  // first, get all possible parents and their children
425  var groupParents = {};
426  var directParentChildren = {};
427  for (var id in cells)
428  {
429    var cell = cells[id];
430    if (!cell.isVertex() || !this.isLeafOrCollapsed(cell))
431      continue;
432    var parent = cell.getParent();
433    if (parent.isVertex())
434    {
435      groupParents[parent.id] = parent;
436      if (!(parent.id in directParentChildren))
437      {
438        directParentChildren[parent.id] = {}
439      }
440      directParentChildren[parent.id][id] = cell;
441    }
442  }
443  // now go through all parents/children and build a group hierarchy for WebCola
444  var preliminaryGroups = [];
445  var groupId = 0;
446  var groupToParent = {}
447  for (var parentId in groupParents)
448  {
449    var parentChildren = directParentChildren[parentId];
450    var groupNodes = []
451    for (var childId in parentChildren)
452    {
453      if (activeVertices[childId])
454      {
455        groupNodes.push(cellIds[childId]);
456      }
457    }
458    preliminaryGroups.push({id: groupId, parentId: parentId, nodes: parentChildren, leaves: groupNodes, groups: []});
459    groupToParent[groupId] = parentId;
460    groupId++;
461  }
462  // here scan newly formed groups if their parent is a child of any of the nodes in any of the groups
463  for (var i = 0; i < preliminaryGroups.length; i++)
464  {
465    var parentGroup = preliminaryGroups[i];
466    var parentId = parentGroup.parentId;
467    for (var j = 0; j < preliminaryGroups.length; j++)
468    {
469      if (i == j)
470        continue;
471      var groupParentId = cells[preliminaryGroups[j].parentId].getParent().id;
472      if (parentId == groupParentId)
473        parentGroup.groups.push(j);
474    }
475  }
476  // finalize groups
477  var groups = [];
478  for (var i = 0; i < preliminaryGroups.length; i++)
479  {
480    var group = preliminaryGroups[i];
481    var graphGroup = {};
482    if (group.leaves.length > 0)
483    {
484      graphGroup["leaves"] = group.leaves;
485    }
486    if (group.groups.length > 0)
487    {
488      graphGroup["groups"] = group.groups;
489    }
490    if (graphGroup.hasOwnProperty("leaves") || graphGroup.hasOwnProperty("groups"))
491    {
492      groups.push(graphGroup);
493    }
494  }
495
496  return {nodes: nodes, links: links, groups: groups, cellToNode: cellIds};
497};
498
499mxWebColaAdaptor.prototype.createLink = function(sourceId, targetId, cellIds)
500/**
501 * Creates a default version of a WebCola link/edge
502 * @param sourceId ID of the edge's source vertex cell
503 * @param targetId ID of the edge's target vertex cell
504 * @param cellIds cell ID to WebCola's node ID mapping
505 * @returns a WebCola link corresponding to the edge [sourceId, targetId]
506 * in WebCola node IDs
507 */
508{
509  var link = {};
510  link.source = cellIds[sourceId];
511  link.target = cellIds[targetId];
512  link.weight = 0.5;
513  link.length = 200; //Graph.prototype.defaultEdgeLength;
514  return link;
515}
516
517mxWebColaAdaptor.prototype.computeVertexDegrees = function()
518/**
519 * Computes group vertex and vertex degrees. Useful to stop layouting for groups
520 * with no internal, incoming or outgoing edges.
521 */
522{
523  var model = this.graph.getModel();
524  var cells = model.cells;
525
526  // compute individual vertex degrees
527  for (var id in cells)
528  {
529    var cell = cells[id];
530    if (cell.isEdge() && cell.source != null && cell.target != null)
531    {
532    	  // scan all edges, ignore other types
533    	  var sourceId = cell.source.id;
534    	  var targetId = cell.target.id;
535    	  var source = cells[sourceId];
536    	  var target = cells[targetId];
537
538    	  if (sourceId == targetId)
539      {
540    		 // self-loops are irrelevant
541         continue;
542    	  }
543
544    	  var sourceDegree = this.vertexDegrees.get(source);
545    	  if (typeof sourceDegree == "undefined")
546      {
547    		sourceDegree = 0;
548      }
549    	  sourceDegree++;
550    	  this.vertexDegrees.put(source, sourceDegree);
551
552    	  var targetDegree = this.vertexDegrees.get(target);
553    	  if (typeof targetDegree == "undefined")
554      {
555    		  targetDegree = 0;
556      }
557    	  targetDegree++;
558    	  this.vertexDegrees.put(target, targetDegree);
559    }
560  }
561  // compute sub-group degree, i.e. sum of all degrees of children
562  // algorithm goes through all vertices, then for each vertex it goes on its material
563  // path to root and adds its contribution to all vertices on this path
564  // this should for each vertex place exactly the sum of degrees of all its vertices
565  // and itself, nothing less, nothing more
566  for (var id in cells)
567  {
568    var cell = cells[id];
569    if (cell.isVertex())
570    {
571    	  // scan all vertices, ignore other types
572    	  var vertexDegree = this.vertexDegrees.get(cell);
573    	  if (typeof vertexDegree == "undefined")
574    	  {
575    		vertexDegree = 0;
576    	  }
577      var parent = cell;
578    	  while (parent != null && typeof parent != "undefined")
579	  {
580	    	var groupDegree = this.groupDegrees.get(parent);
581	    	if (typeof groupDegree == "undefined")
582	    	{
583	      groupDegree = 0;
584	    }
585	    groupDegree += vertexDegree;
586        this.groupDegrees.put(parent, groupDegree);
587        parent = parent.parent;
588      }
589    }
590  }
591}
592
593mxWebColaAdaptor.prototype.isZeroConnected = function(groupCell)
594/**
595 * Indicates if all group cell's vertices have no incidental edges
596 * @params groupCell group cell
597 * @returns true if the group cell doesn't contain any vertices with edges
598 */
599{
600  var groupDegree = this.groupDegrees.get(groupCell);
601  console.log("Group " + groupCell.id + " degree: " + groupDegree);
602  if (typeof groupDegree != "undefined" && groupDegree > 0)
603  {
604	return false;
605  }
606  return true;
607}
608
609mxWebColaAdaptor.prototype.isInZeroConnectedGroup = function(cell)
610{
611  var parent = cell.parent;
612  if (parent == null || typeof parent == "undefined")
613  {
614	return this.isZeroConnected(cell);
615  }
616  else
617  {
618	return this.isZeroConnected(parent);
619  }
620}
621
622mxWebColaAdaptor.prototype.isLeafOrCollapsed = function(cell)
623  /**
624   * Returns true if a cell is either a leaf or a collapsed group
625   * @param cell cell to investigate
626   * @returns true if a cell is either a leaf or a collapsed group, false otherwise
627   */
628{
629  if (cell.isCollapsed() ||
630      cell.children == null || cell.children.length == 0 ||
631      typeof this.graph.getCellStyle(cell)['childLayout'] != 'undefined')
632  {
633    return true;
634  }
635  if (this.isZeroConnected(cell))
636  {
637	return true;
638  }
639  /*
640  if (!cell.isCollapsed() && cell.children != null && cell.children.length > 0 && this.graph.getEdges(cell, null, true, true, true, true).length == 0)
641  {
642	console.log("cell " + cell.id + " is 0-connected.");
643	return true;
644  }
645  */
646  return false;
647}
648
649mxWebColaAdaptor.prototype.findActiveVertices = function(graph)
650  /**
651   * Scans all groups and finds active vertices, as well as an inactive-vertex-to-active-parent map
652   * @param graph input graph
653   */
654{
655  var inactiveToActiveMap = {};
656  var activeVertices = {};
657  var root = graph.getModel().root;
658  var cellsToExplore = [{vertex: root, isActive: true, activeParent: root}]
659  while (cellsToExplore.length > 0)
660  {
661    var currentCellInfo = cellsToExplore.shift();
662    var cell = currentCellInfo.vertex;
663    if (cell.isEdge())
664    {
665      // cut at edge group, those are ignored
666      continue;
667    }
668    var isActive = currentCellInfo.isActive;
669    var activeParent = currentCellInfo.activeParent;
670    if (cell.isVertex())
671    {
672      if (isActive)
673      {
674        activeVertices[cell.id] = true;
675      }
676      else
677      {
678        activeVertices[cell.id] = false;
679      }
680    }
681    // prepare children
682    // child can be active only if any of its parents is not collapsed
683    var isActive = isActive && !this.isLeafOrCollapsed(cell);
684    var children = cell.children;
685    if (children != null && children.length > 0)
686    {
687      for (var i = 0; i < children.length; i++)
688      {
689        var child = children[i];
690        var childActiveParent = isActive? child: activeParent;
691        cellsToExplore.push({vertex: child, isActive: isActive, activeParent: childActiveParent});
692        if (child.isVertex())
693        {
694          inactiveToActiveMap[child.id] = childActiveParent.id;
695        }
696      }
697    }
698  }
699  return {activeVertices: activeVertices, inactiveToActiveMap: inactiveToActiveMap};
700}
701
702mxWebColaAdaptor.prototype.getActiveVerticesInGroup = function(groupCell, activeVertices, includeCollapsedGroups)
703  /**
704   * Scans all children in group and returns all active vertices inside group
705   * This method is for creating redundant edges between members of groups to simulate group edges in WebCola
706   * See https://github.com/tgdwyer/WebCola/issues/38
707   * @param groupCell group cell
708   */
709{
710  var activeChildren = [];
711  if (includeCollapsedGroups && this.isLeafOrCollapsed(groupCell))
712  {
713    activeChildren.push(groupCell);
714  }
715  var cellsToExplore = [groupCell];
716  while (cellsToExplore.length > 0)
717  {
718    var cell = cellsToExplore.shift();
719    if (!cell.isVertex() || !activeVertices[cell])
720    {
721      // cut at edge group, those are ignored
722      continue;
723    }
724    if (this.isLeafOrCollapsed(cell))
725    {
726      activeChildren.push(cell);
727    }
728    else
729    {
730      var children = cell.children;
731      if (children == null || children.length == 0)
732      {
733        continue;
734      }
735      cellsToExplore = cellsToExplore.concat(children);
736    }
737  }
738  return activeChildren;
739}
740
741mxWebColaAdaptor.prototype.getAllVerticesInGroup = function(groupCell, includeCollapsedGroups)
742  /**
743   * Scans all children in group and returns all active vertices inside group
744   * This method is for creating redundant edges between members of groups to simulate group edges in WebCola
745   * See https://github.com/tgdwyer/WebCola/issues/38
746   * @param groupCell group cell
747   */
748{
749  var result = [];
750  if (includeCollapsedGroups && this.isLeafOrCollapsed(groupCell))
751  {
752    result.push(groupCell);
753  }
754  var cellsToExplore = [groupCell];
755  while (cellsToExplore.length > 0)
756  {
757    var cell = cellsToExplore.shift();
758    if (!cell.isVertex())
759    {
760      // cut at edge group, those are ignored
761      continue;
762    }
763    if (this.isLeafOrCollapsed(cell))
764    {
765      result.push(cell);
766    }
767    else
768    {
769      var children = cell.children;
770      if (children == null || children.length == 0)
771      {
772        continue;
773      }
774      cellsToExplore = cellsToExplore.concat(children);
775    }
776  }
777  return result;
778}
779
780mxWebColaAdaptor.prototype.hasVertexChildren = function(cell)
781  /**
782   * Returns true if a (group) cell has vertex children in its subtree
783   * @param cell (group) cell
784   * @returns true if if a (group) cell has vertex children in its subtree, false otherwise
785   */
786{
787  if (cell.children == null || cell.children.length == 0)
788  {
789    return false;
790  }
791  var toBeExamined = []
792  toBeExamined = toBeExamined.concat(cell.children);
793  while (toBeExamined.length > 0)
794  {
795    var cell = toBeExamined.shift();
796    if (cell.isVertex())
797      return true;
798    if (cell.children != null && cell.children.length > 0)
799    {
800      toBeExamined = toBeExamined.concat(cell.children);
801    }
802  }
803  return false;
804}
805
806mxWebColaAdaptor.prototype.isInCollapsedTree = function(cell)
807{
808  // scan the material path for collapsed group node
809  while (cell != null)
810  {
811    cell = cell.getParent();
812    if (cell != null && cell.isCollapsed())
813    {
814      return true;
815    }
816  }
817  return false;
818}
819
820mxWebColaAdaptor.prototype.isGroup = function(cell)
821  /**
822   * Returns true if cell is a group (has children)
823   * @param cell cell
824   * @returns true if cell is a group (has children); false otherwise
825   */
826{
827  return cell.children != null && cell.children.length > 0;
828}
829
830mxWebColaAdaptor.prototype.addGroupConstraintLinks = function(groupA, groupB, activeVertices, inactiveToActiveMap, cellIds)
831  /**
832   * Adds edges between vertex and group or two groups. Each vertex child of a group must be connected to the vertex/
833   * group, as this way WebCola simulates edges on the group level (as groups don't exist as vertices in WebCola)
834   * @param rootCell root cell
835   */
836{
837  var result = []
838  // var childrenA = this.getActiveVerticesInGroup(groupA, activeVertices, false);
839  // var childrenB = this.getActiveVerticesInGroup(groupB, activeVertices, false);
840  var childrenA = [groupA];
841  var childrenB = [groupB];
842  if (!groupA.isCollapsed())
843  {
844    childrenA = this.getAllVerticesInGroup(groupA, activeVertices, false);
845  }
846  if (!groupB.isCollapsed())
847  {
848    childrenB = this.getAllVerticesInGroup(groupB, activeVertices, false);
849  }
850  if (childrenA == null || childrenA.length == 0 || childrenB == null || childrenB.length == 0)
851    return result;
852  for (var i = 0; i < childrenA.length; i++)
853  {
854    var childA_Id = inactiveToActiveMap[childrenA[i].id];
855    for (var j = 0; j < childrenB.length; j++)
856    {
857      var childB_Id = inactiveToActiveMap[childrenB[j].id];
858      var link = this.createLink(childA_Id, childB_Id, cellIds);
859      result.push(link);
860    }
861  }
862  return result;
863}
864
865mxWebColaAdaptor.prototype.getUniqueLinks = function(links)
866  /**
867   * Returns an array of unique links from an array of links
868   * @param links array of links containing duplicate links
869   * @returns array of unique links
870   */
871{
872  var result = [];
873  // TODO: this part is inefficient - O(n^2); Theta(n) should be possible with hashmap
874  for (var i = 0; i < links.length; i++)
875  {
876    var link = links[i];
877    var shouldBeAdded = true;
878    for (var j = 0; j < result.length; j++)
879    {
880      var existingLink = result[j];
881      if (link.source == existingLink.source && link.target == existingLink.target)
882      {
883        shouldBeAdded = false;
884        break;
885      }
886    }
887    if (shouldBeAdded)
888    {
889      result.push(link);
890    }
891  }
892  return result;
893}
894
895var doRendering = void 0;
896
897if ((typeof window === "undefined" ? "undefined" : typeof(window)) !== ( true ? "undefined" : typeof(undefined)))
898{
899  doRendering = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
900}
901else
902{
903  // if not available, all you get is immediate calls
904  function doRendering(callback)
905  {
906    callback();
907  };
908}
909