mxWebColaAdaptor.js 26 KB

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