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