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