1/** 2 * Copyright (c) 2006-2018, JGraph Ltd 3 * Copyright (c) 2006-2018, Gaudenz Alder 4 */ 5/** 6 * Class: mxWebColaLayout 7 * 8 * Extends <mxGraphLayout> to implement a WebCola-based layout. 9 * 10 * Example: 11 * 12 * (code) 13 * var layout = new mxWebColaLayout(graph); 14 * layout.execute(graph.getDefaultParent()); 15 * (end) 16 * 17 * Constructor: mxWebColaLayout 18 * 19 * Constructs a new WebCola-based layout for the graph. 20 * 21 * Arguments: 22 * 23 * graph - <mxGraph> that contains the cells. 24 * 25 **/ 26function mxWebColaLayout(graph, layoutType) 27/** 28 * Constructs a WebCola-based layout 29 * @param graph <mxGraph> that contains the cells. 30 * @param layoutType Type of WebCola layout 31 */ 32{ 33 mxGraphLayout.call(this, graph); 34 this.layoutType = layoutType; 35 this.originalGeometries = new mxDictionary(); 36}; 37 38mxWebColaLayout.prototype = new mxGraphLayout(); 39mxWebColaLayout.prototype.constructor = mxWebColaLayout; 40 41mxWebColaLayout.prototype.layoutType = null; 42 43mxWebColaLayout.prototype.execute = function(parent) 44 /** 45 * Runs re-layouting of the portion of a graph from a given starting cell 46 * @param parent starting cell 47 */ 48{ 49 var movableVertices = this.getReachableVertices(parent); 50 var ps = this.graph.getPageSize(); 51 this.layout = new mxWebColaAdaptor(this.graph, (this.graph.pageVisible) ? 52 [ps.width, ps.height] : [800, 800], movableVertices); 53 var initial = true; 54 var update = function (isUndoable) { 55 // console.log("mxColaLayout: update"); 56 this.updateGraph(isUndoable, initial); 57 initial = false; 58 }.bind(this); 59 this.layout.updatePositions = update; 60 //this.resetGraph(this.graph); 61 var finalLayout = this.computePositions(this.layout); 62}; 63 64mxWebColaLayout.prototype.getReachableVertices = function(parent) 65 /*** 66 * Finds all vertices reachable from a given parent 67 * @param parent starting cell of a search 68 * @returns dictionary of vertice IDs that are reachable in the form of {id: True} 69 * if undefined is returned, it means parent was not provided and it should be interpreted as all vertices 70 * are reachable 71 */ 72{ 73 if (parent == undefined) 74 return undefined; 75 // first, get all incidental edges in a sub-tree (or a connected component if with loops) 76 var edges = this.graph.getEdges(parent, null, true, true, true, true); 77 // now all vertices that are reachable are subject to change; unreachable should be fixed 78 var reachableVertices = void(0); 79 if (edges.length != 0) 80 { 81 var reachableVertices = {}; 82 for (var i = 0; i < edges.length; i++) 83 { 84 var edge = edges[i]; 85 reachableVertices[edge.source.id] = true; 86 reachableVertices[edge.target.id] = true; 87 } 88 } 89 // vertices connected by returned edges must be allowed to move, rest must be fixed in place 90 return reachableVertices; 91} 92 93mxWebColaLayout.prototype.computePositions = function() 94 /** 95 * Executes layout to compute positions 96 */ 97{ 98 return this.layout.run(); 99} 100 101mxWebColaLayout.prototype.resetGraph = function(graph) 102 /** 103 * Resets initial vertex positions 104 */ 105{ 106 var model = graph.getModel(); 107 var cells = model.cells; 108 var view = graph.getView(); 109 model.beginUpdate(); 110 try 111 { 112 for (var id in cells) 113 { 114 var cell = cells[id]; 115 var state = view.getState(cell); 116 var bounds = view.getBoundingBox(state, true); 117 var isFirst = true; 118 if (cell.isVertex()) { 119 var geometry = model.getGeometry(cell); 120 if (geometry != null && typeof geometry != "undefined") 121 { 122 geometry = geometry.clone(); 123 geometry.offset = null; 124 model.setGeometry(cell, geometry); 125 } 126 } 127 } 128 } 129 finally 130 { 131 model.endUpdate(); 132 } 133} 134 135mxWebColaLayout.prototype.getGroupBounds = function(model, groupCell) 136 /** 137 * Computes bounds of a group as boundary encompassing all children of a group 138 * @param model graph model 139 * @param groupCell starting group 140 * @returns boundaries of all children inside a group 141 */ 142{ 143 var minX = 1000000; 144 var minY = 1000000; 145 var maxX = -1000000; 146 var maxY = -1000000; 147 if (groupCell.children == null || groupCell.children.length == 0) 148 return null; 149 var cellsToVisit = []; 150 cellsToVisit = cellsToVisit.concat(groupCell.children); 151 while (cellsToVisit.length > 0) 152 { 153 var child = cellsToVisit.shift(); 154 if (child.isVertex()) 155 { 156 if (this.layout.isLeafOrCollapsed(child)) 157 { 158 var geometry = model.getGeometry(child); 159 if (geometry != null && typeof geometry != "undefined") 160 { 161 if (geometry.x < minX) 162 { 163 minX = geometry.x; 164 } 165 if (geometry.y < minY) 166 { 167 minY = geometry.y; 168 } 169 if (geometry.x + geometry.width > maxX) 170 { 171 maxX = geometry.x + geometry.width; 172 } 173 if (geometry.y + geometry.height > maxY) 174 { 175 maxY = geometry.y + geometry.height; 176 } 177 } 178 } 179 else 180 { 181 cellsToVisit = cellsToVisit.concat(child.children); 182 } 183 } 184 } 185 var width = maxX - minX; 186 var height = maxY - minY; 187 // at last look if the group has geometry already and use it as basic range 188 var groupGeometry = model.getGeometry(groupCell); 189 if (groupGeometry != null) 190 { 191 minX = Math.min(groupGeometry.x, minX); 192 minY = Math.min(groupGeometry.y, minY); 193 width = Math.max(groupGeometry.width, width); 194 height = Math.max(groupGeometry.height, height); 195 } 196 var bounds = model.getGeometry(groupCell).clone(); 197 bounds.x = minX; 198 bounds.y = minY; 199 bounds.width = width; 200 bounds.height = height; 201 return bounds; 202} 203 204mxWebColaLayout.prototype.adjustChildOffsets = function(model, groupCell, isUndoable, isInitial) 205 /** 206 * Adjusts offset of child vertices to be relative to parent groups 207 * @param model graph model 208 * @param groupCell starting group cell 209 */ 210{ 211 if (groupCell.children == null || groupCell.children.length == 0) 212 return; 213 214 var groupBounds = model.getGeometry(groupCell); 215 var offsetX = groupBounds.x; 216 var offsetY = groupBounds.y; 217 var cellsToVisit = []; 218 cellsToVisit = cellsToVisit.concat(groupCell.children); 219 220 while (cellsToVisit.length > 0) 221 { 222 var child = cellsToVisit.shift(); 223 224 if (child.isVertex()) 225 { 226 if (this.layout.isLeafOrCollapsed(child)) 227 { 228 var geometry = model.getGeometry(child); 229 230 if (geometry != null && typeof geometry != "undefined") 231 { 232// geometry = geometry.clone(); 233 geometry.x = geometry.x - offsetX; 234 geometry.y = geometry.y - offsetY; 235// model.setGeometry(child, geometry); 236 } 237 } 238 else 239 { 240 cellsToVisit = cellsToVisit.concat(child.children); 241 } 242 } 243 } 244} 245 246mxWebColaLayout.prototype.updateGraph = function(isUndoable = false, initial = false) 247 /** 248 * Updates graph based on layout's vertex/group positions 249 */ 250{ 251 // find X, Y ranges first 252 var minX = 1000000; 253 var maxX = -1000000; 254 var minY = 1000000; 255 var maxY = -1000000; 256 257 // finding limits by scanning top-left corners of shapes 258 for (var i = 0; i < this.layout.adaptor._nodes.length; i++) 259 { 260 var node = this.layout.adaptor._nodes[i]; 261 var x = node.bounds.x; 262 var y = node.bounds.y; 263 minX = Math.min(minX, x); 264 minY = Math.min(minY, y); 265 maxX = Math.max(maxX, x); 266 maxY = Math.max(maxY, y); 267 } 268 269 var spanX = maxX - minX; 270 var spanY = maxY - minY; 271 272 var model = this.graph.getModel(); 273 if (isUndoable) 274 { 275 model.beginUpdate(); 276 } 277 try 278 { 279 var cells = model.cells; 280 var view = this.graph.getView(); 281 282 // scan leaves and edges 283 for (var id in cells) 284 { 285 var cell = cells[id]; 286 var state = view.getState(cell); 287 var bounds = view.getBoundingBox(state, true); 288 289 if (cell.isVertex() && this.layout.isLeafOrCollapsed(cell)) 290 { 291 var nodeId = this.layout.cellToNode[id]; 292 293 if (typeof nodeId == "undefined") 294 continue; 295 296 var node = this.layout.adaptor._nodes[nodeId]; 297 var geometry = model.getGeometry(cell); 298 299 if (geometry != null) 300 { 301 // First run creates a temporary geometry that can 302 // be changed in-place to update the view and keeps 303 // a copy of the original geometry to use in the 304 // final undoable edit to force a change event 305 if (initial) 306 { 307 this.originalGeometries.put(cell, geometry); 308 geometry = geometry.clone(); 309 310 if (model.isVertex(cell)) 311 { 312 if (this.layout.isInZeroConnectedGroup(cell)) 313 { 314 geometry.offset = geometry.offset.clone(); 315 } 316 else 317 { 318 geometry.offset = null; 319 } 320 } 321 } 322 323 // anchor top-left corners at (0, 0) 324 geometry.x = node.bounds.x - minX; 325 geometry.y = node.bounds.y - minY; 326 327 if (isUndoable) 328 { 329 // Restores original geometry for the change to be detected 330 cell.geometry = this.originalGeometries.get(cell); 331 model.setGeometry(cell, geometry); 332 } 333 else if (initial) 334 { 335 cell.geometry = geometry; 336 } 337 else 338 { 339 this.graph.view.invalidate(cell, true, true); 340 } 341 } 342 else 343 { 344 console.log("ERROR: vertex cell id:" + id + " has no geometry!"); 345 } 346 } 347 else if (cell.isEdge()) 348 { 349 this.graph.resetEdge(cell); 350 } 351 } 352 // scan groups 353 for (var id in cells) 354 { 355 var cell = cells[id]; 356 var state = view.getState(cell); 357 var bounds = view.getBoundingBox(state, true); 358 if (cell.isVertex() && !this.layout.isLeafOrCollapsed(cell)) 359 { 360 var bounds = this.getGroupBounds(model, cell); 361 var geometry = model.getGeometry(cell); 362 if (bounds != null && typeof bounds != "undefined") 363 { 364 if (initial) 365 { 366 this.originalGeometries.put(cell, geometry); 367 geometry = geometry.clone(); 368 369 if (model.isVertex(cell)) 370 { 371 if (this.layout.isInZeroConnectedGroup(cell)) 372 { 373 geometry.offset = geometry.offset.clone(); 374 } 375 else 376 { 377 geometry.offset = null; 378 } 379 } 380 } 381 if (isUndoable) 382 { 383 cell.geometry = this.originalGeometries.get(cell); 384 model.setGeometry(cell, bounds); 385 } 386 else if (initial) 387 { 388 cell.geometry = bounds; 389 } 390 else 391 { 392 this.graph.view.invalidate(cell, true, true); 393 } 394 this.adjustChildOffsets(model, cell, isUndoable, initial); 395 } 396 } 397 } 398 } 399 finally 400 { 401 if (isUndoable) 402 { 403 model.endUpdate(); 404 } 405 else 406 { 407 this.graph.view.validate(); 408 } 409 } 410} 411