summaryrefslogtreecommitdiff
path: root/src/js/layout/mxFastOrganicLayout.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/layout/mxFastOrganicLayout.js')
-rw-r--r--src/js/layout/mxFastOrganicLayout.js591
1 files changed, 591 insertions, 0 deletions
diff --git a/src/js/layout/mxFastOrganicLayout.js b/src/js/layout/mxFastOrganicLayout.js
new file mode 100644
index 0000000..d7d6b5d
--- /dev/null
+++ b/src/js/layout/mxFastOrganicLayout.js
@@ -0,0 +1,591 @@
+/**
+ * $Id: mxFastOrganicLayout.js,v 1.37 2011-04-28 13:14:55 david Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxFastOrganicLayout
+ *
+ * Extends <mxGraphLayout> to implement a fast organic layout algorithm.
+ * The vertices need to be connected for this layout to work, vertices
+ * with no connections are ignored.
+ *
+ * Example:
+ *
+ * (code)
+ * var layout = new mxFastOrganicLayout(graph);
+ * layout.execute(graph.getDefaultParent());
+ * (end)
+ *
+ * Constructor: mxCompactTreeLayout
+ *
+ * Constructs a new fast organic layout for the specified graph.
+ */
+function mxFastOrganicLayout(graph)
+{
+ mxGraphLayout.call(this, graph);
+};
+
+/**
+ * Extends mxGraphLayout.
+ */
+mxFastOrganicLayout.prototype = new mxGraphLayout();
+mxFastOrganicLayout.prototype.constructor = mxFastOrganicLayout;
+
+/**
+ * Variable: useInputOrigin
+ *
+ * Specifies if the top left corner of the input cells should be the origin
+ * of the layout result. Default is true.
+ */
+mxFastOrganicLayout.prototype.useInputOrigin = true;
+
+/**
+ * Variable: resetEdges
+ *
+ * Specifies if all edge points of traversed edges should be removed.
+ * Default is true.
+ */
+mxFastOrganicLayout.prototype.resetEdges = true;
+
+/**
+ * Variable: disableEdgeStyle
+ *
+ * Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
+ * modified by the result. Default is true.
+ */
+mxFastOrganicLayout.prototype.disableEdgeStyle = true;
+
+/**
+ * Variable: forceConstant
+ *
+ * The force constant by which the attractive forces are divided and the
+ * replusive forces are multiple by the square of. The value equates to the
+ * average radius there is of free space around each node. Default is 50.
+ */
+mxFastOrganicLayout.prototype.forceConstant = 50;
+
+/**
+ * Variable: forceConstantSquared
+ *
+ * Cache of <forceConstant>^2 for performance.
+ */
+mxFastOrganicLayout.prototype.forceConstantSquared = 0;
+
+/**
+ * Variable: minDistanceLimit
+ *
+ * Minimal distance limit. Default is 2. Prevents of
+ * dividing by zero.
+ */
+mxFastOrganicLayout.prototype.minDistanceLimit = 2;
+
+/**
+ * Variable: minDistanceLimit
+ *
+ * Minimal distance limit. Default is 2. Prevents of
+ * dividing by zero.
+ */
+mxFastOrganicLayout.prototype.maxDistanceLimit = 500;
+
+/**
+ * Variable: minDistanceLimitSquared
+ *
+ * Cached version of <minDistanceLimit> squared.
+ */
+mxFastOrganicLayout.prototype.minDistanceLimitSquared = 4;
+
+/**
+ * Variable: initialTemp
+ *
+ * Start value of temperature. Default is 200.
+ */
+mxFastOrganicLayout.prototype.initialTemp = 200;
+
+/**
+ * Variable: temperature
+ *
+ * Temperature to limit displacement at later stages of layout.
+ */
+mxFastOrganicLayout.prototype.temperature = 0;
+
+/**
+ * Variable: maxIterations
+ *
+ * Total number of iterations to run the layout though.
+ */
+mxFastOrganicLayout.prototype.maxIterations = 0;
+
+/**
+ * Variable: iteration
+ *
+ * Current iteration count.
+ */
+mxFastOrganicLayout.prototype.iteration = 0;
+
+/**
+ * Variable: vertexArray
+ *
+ * An array of all vertices to be laid out.
+ */
+mxFastOrganicLayout.prototype.vertexArray;
+
+/**
+ * Variable: dispX
+ *
+ * An array of locally stored X co-ordinate displacements for the vertices.
+ */
+mxFastOrganicLayout.prototype.dispX;
+
+/**
+ * Variable: dispY
+ *
+ * An array of locally stored Y co-ordinate displacements for the vertices.
+ */
+mxFastOrganicLayout.prototype.dispY;
+
+/**
+ * Variable: cellLocation
+ *
+ * An array of locally stored co-ordinate positions for the vertices.
+ */
+mxFastOrganicLayout.prototype.cellLocation;
+
+/**
+ * Variable: radius
+ *
+ * The approximate radius of each cell, nodes only.
+ */
+mxFastOrganicLayout.prototype.radius;
+
+/**
+ * Variable: radiusSquared
+ *
+ * The approximate radius squared of each cell, nodes only.
+ */
+mxFastOrganicLayout.prototype.radiusSquared;
+
+/**
+ * Variable: isMoveable
+ *
+ * Array of booleans representing the movable states of the vertices.
+ */
+mxFastOrganicLayout.prototype.isMoveable;
+
+/**
+ * Variable: neighbours
+ *
+ * Local copy of cell neighbours.
+ */
+mxFastOrganicLayout.prototype.neighbours;
+
+/**
+ * Variable: indices
+ *
+ * Hashtable from cells to local indices.
+ */
+mxFastOrganicLayout.prototype.indices;
+
+/**
+ * Variable: allowedToRun
+ *
+ * Boolean flag that specifies if the layout is allowed to run. If this is
+ * set to false, then the layout exits in the following iteration.
+ */
+mxFastOrganicLayout.prototype.allowedToRun = true;
+
+/**
+ * Function: isVertexIgnored
+ *
+ * Returns a boolean indicating if the given <mxCell> should be ignored as a
+ * vertex. This returns true if the cell has no connections.
+ *
+ * Parameters:
+ *
+ * vertex - <mxCell> whose ignored state should be returned.
+ */
+mxFastOrganicLayout.prototype.isVertexIgnored = function(vertex)
+{
+ return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) ||
+ this.graph.getConnections(vertex).length == 0;
+};
+
+/**
+ * Function: execute
+ *
+ * Implements <mxGraphLayout.execute>. This operates on all children of the
+ * given parent where <isVertexIgnored> returns false.
+ */
+mxFastOrganicLayout.prototype.execute = function(parent)
+{
+ var model = this.graph.getModel();
+ this.vertexArray = [];
+ var cells = this.graph.getChildVertices(parent);
+
+ for (var i = 0; i < cells.length; i++)
+ {
+ if (!this.isVertexIgnored(cells[i]))
+ {
+ this.vertexArray.push(cells[i]);
+ }
+ }
+
+ var initialBounds = (this.useInputOrigin) ?
+ this.graph.view.getBounds(this.vertexArray) :
+ null;
+ var n = this.vertexArray.length;
+
+ this.indices = [];
+ this.dispX = [];
+ this.dispY = [];
+ this.cellLocation = [];
+ this.isMoveable = [];
+ this.neighbours = [];
+ this.radius = [];
+ this.radiusSquared = [];
+
+ if (this.forceConstant < 0.001)
+ {
+ this.forceConstant = 0.001;
+ }
+
+ this.forceConstantSquared = this.forceConstant * this.forceConstant;
+
+ // Create a map of vertices first. This is required for the array of
+ // arrays called neighbours which holds, for each vertex, a list of
+ // ints which represents the neighbours cells to that vertex as
+ // the indices into vertexArray
+ for (var i = 0; i < this.vertexArray.length; i++)
+ {
+ var vertex = this.vertexArray[i];
+ this.cellLocation[i] = [];
+
+ // Set up the mapping from array indices to cells
+ var id = mxCellPath.create(vertex);
+ this.indices[id] = i;
+ var bounds = this.getVertexBounds(vertex);
+
+ // Set the X,Y value of the internal version of the cell to
+ // the center point of the vertex for better positioning
+ var width = bounds.width;
+ var height = bounds.height;
+
+ // Randomize (0, 0) locations
+ var x = bounds.x;
+ var y = bounds.y;
+
+ this.cellLocation[i][0] = x + width / 2.0;
+ this.cellLocation[i][1] = y + height / 2.0;
+ this.radius[i] = Math.min(width, height);
+ this.radiusSquared[i] = this.radius[i] * this.radius[i];
+ }
+
+ // Moves cell location back to top-left from center locations used in
+ // algorithm, resetting the edge points is part of the transaction
+ model.beginUpdate();
+ try
+ {
+ for (var i = 0; i < n; i++)
+ {
+ this.dispX[i] = 0;
+ this.dispY[i] = 0;
+ this.isMoveable[i] = this.isVertexMovable(this.vertexArray[i]);
+
+ // Get lists of neighbours to all vertices, translate the cells
+ // obtained in indices into vertexArray and store as an array
+ // against the orginial cell index
+ var edges = this.graph.getConnections(this.vertexArray[i], parent);
+ var cells = this.graph.getOpposites(edges, this.vertexArray[i]);
+ this.neighbours[i] = [];
+
+ for (var j = 0; j < cells.length; j++)
+ {
+ // Resets the points on the traversed edge
+ if (this.resetEdges)
+ {
+ this.graph.resetEdge(edges[j]);
+ }
+
+ if (this.disableEdgeStyle)
+ {
+ this.setEdgeStyleEnabled(edges[j], false);
+ }
+
+ // Looks the cell up in the indices dictionary
+ var id = mxCellPath.create(cells[j]);
+ var index = this.indices[id];
+
+ // Check the connected cell in part of the vertex list to be
+ // acted on by this layout
+ if (index != null)
+ {
+ this.neighbours[i][j] = index;
+ }
+
+ // Else if index of the other cell doesn't correspond to
+ // any cell listed to be acted upon in this layout. Set
+ // the index to the value of this vertex (a dummy self-loop)
+ // so the attraction force of the edge is not calculated
+ else
+ {
+ this.neighbours[i][j] = i;
+ }
+ }
+ }
+ this.temperature = this.initialTemp;
+
+ // If max number of iterations has not been set, guess it
+ if (this.maxIterations == 0)
+ {
+ this.maxIterations = 20 * Math.sqrt(n);
+ }
+
+ // Main iteration loop
+ for (this.iteration = 0; this.iteration < this.maxIterations; this.iteration++)
+ {
+ if (!this.allowedToRun)
+ {
+ return;
+ }
+
+ // Calculate repulsive forces on all vertices
+ this.calcRepulsion();
+
+ // Calculate attractive forces through edges
+ this.calcAttraction();
+
+ this.calcPositions();
+ this.reduceTemperature();
+ }
+
+ var minx = null;
+ var miny = null;
+
+ for (var i = 0; i < this.vertexArray.length; i++)
+ {
+ var vertex = this.vertexArray[i];
+
+ if (this.isVertexMovable(vertex))
+ {
+ var bounds = this.getVertexBounds(vertex);
+
+ if (bounds != null)
+ {
+ this.cellLocation[i][0] -= bounds.width / 2.0;
+ this.cellLocation[i][1] -= bounds.height / 2.0;
+
+ var x = this.graph.snap(this.cellLocation[i][0]);
+ var y = this.graph.snap(this.cellLocation[i][1]);
+
+ this.setVertexLocation(vertex, x, y);
+
+ if (minx == null)
+ {
+ minx = x;
+ }
+ else
+ {
+ minx = Math.min(minx, x);
+ }
+
+ if (miny == null)
+ {
+ miny = y;
+ }
+ else
+ {
+ miny = Math.min(miny, y);
+ }
+ }
+ }
+ }
+
+ // Modifies the cloned geometries in-place. Not needed
+ // to clone the geometries again as we're in the same
+ // undoable change.
+ var dx = -(minx || 0) + 1;
+ var dy = -(miny || 0) + 1;
+
+ if (initialBounds != null)
+ {
+ dx += initialBounds.x;
+ dy += initialBounds.y;
+ }
+
+ this.graph.moveCells(this.vertexArray, dx, dy);
+ }
+ finally
+ {
+ model.endUpdate();
+ }
+};
+
+/**
+ * Function: calcPositions
+ *
+ * Takes the displacements calculated for each cell and applies them to the
+ * local cache of cell positions. Limits the displacement to the current
+ * temperature.
+ */
+mxFastOrganicLayout.prototype.calcPositions = function()
+{
+ for (var index = 0; index < this.vertexArray.length; index++)
+ {
+ if (this.isMoveable[index])
+ {
+ // Get the distance of displacement for this node for this
+ // iteration
+ var deltaLength = Math.sqrt(this.dispX[index] * this.dispX[index] +
+ this.dispY[index] * this.dispY[index]);
+
+ if (deltaLength < 0.001)
+ {
+ deltaLength = 0.001;
+ }
+
+ // Scale down by the current temperature if less than the
+ // displacement distance
+ var newXDisp = this.dispX[index] / deltaLength
+ * Math.min(deltaLength, this.temperature);
+
+ var newYDisp = this.dispY[index] / deltaLength
+ * Math.min(deltaLength, this.temperature);
+
+ // reset displacements
+ this.dispX[index] = 0;
+ this.dispY[index] = 0;
+
+ // Update the cached cell locations
+ this.cellLocation[index][0] += newXDisp;
+ this.cellLocation[index][1] += newYDisp;
+ }
+ }
+};
+
+/**
+ * Function: calcAttraction
+ *
+ * Calculates the attractive forces between all laid out nodes linked by
+ * edges
+ */
+mxFastOrganicLayout.prototype.calcAttraction = function()
+{
+ // Check the neighbours of each vertex and calculate the attractive
+ // force of the edge connecting them
+ for (var i = 0; i < this.vertexArray.length; i++)
+ {
+ for (var k = 0; k < this.neighbours[i].length; k++)
+ {
+ // Get the index of the othe cell in the vertex array
+ var j = this.neighbours[i][k];
+
+ // Do not proceed self-loops
+ if (i != j &&
+ this.isMoveable[i] &&
+ this.isMoveable[j])
+ {
+ var xDelta = this.cellLocation[i][0] - this.cellLocation[j][0];
+ var yDelta = this.cellLocation[i][1] - this.cellLocation[j][1];
+
+ // The distance between the nodes
+ var deltaLengthSquared = xDelta * xDelta + yDelta
+ * yDelta - this.radiusSquared[i] - this.radiusSquared[j];
+
+ if (deltaLengthSquared < this.minDistanceLimitSquared)
+ {
+ deltaLengthSquared = this.minDistanceLimitSquared;
+ }
+
+ var deltaLength = Math.sqrt(deltaLengthSquared);
+ var force = (deltaLengthSquared) / this.forceConstant;
+
+ var displacementX = (xDelta / deltaLength) * force;
+ var displacementY = (yDelta / deltaLength) * force;
+
+ this.dispX[i] -= displacementX;
+ this.dispY[i] -= displacementY;
+
+ this.dispX[j] += displacementX;
+ this.dispY[j] += displacementY;
+ }
+ }
+ }
+};
+
+/**
+ * Function: calcRepulsion
+ *
+ * Calculates the repulsive forces between all laid out nodes
+ */
+mxFastOrganicLayout.prototype.calcRepulsion = function()
+{
+ var vertexCount = this.vertexArray.length;
+
+ for (var i = 0; i < vertexCount; i++)
+ {
+ for (var j = i; j < vertexCount; j++)
+ {
+ // Exits if the layout is no longer allowed to run
+ if (!this.allowedToRun)
+ {
+ return;
+ }
+
+ if (j != i &&
+ this.isMoveable[i] &&
+ this.isMoveable[j])
+ {
+ var xDelta = this.cellLocation[i][0] - this.cellLocation[j][0];
+ var yDelta = this.cellLocation[i][1] - this.cellLocation[j][1];
+
+ if (xDelta == 0)
+ {
+ xDelta = 0.01 + Math.random();
+ }
+
+ if (yDelta == 0)
+ {
+ yDelta = 0.01 + Math.random();
+ }
+
+ // Distance between nodes
+ var deltaLength = Math.sqrt((xDelta * xDelta)
+ + (yDelta * yDelta));
+ var deltaLengthWithRadius = deltaLength - this.radius[i]
+ - this.radius[j];
+
+ if (deltaLengthWithRadius > this.maxDistanceLimit)
+ {
+ // Ignore vertices too far apart
+ continue;
+ }
+
+ if (deltaLengthWithRadius < this.minDistanceLimit)
+ {
+ deltaLengthWithRadius = this.minDistanceLimit;
+ }
+
+ var force = this.forceConstantSquared / deltaLengthWithRadius;
+
+ var displacementX = (xDelta / deltaLength) * force;
+ var displacementY = (yDelta / deltaLength) * force;
+
+ this.dispX[i] += displacementX;
+ this.dispY[i] += displacementY;
+
+ this.dispX[j] -= displacementX;
+ this.dispY[j] -= displacementY;
+ }
+ }
+ }
+};
+
+/**
+ * Function: reduceTemperature
+ *
+ * Reduces the temperature of the layout from an initial setting in a linear
+ * fashion to zero.
+ */
+mxFastOrganicLayout.prototype.reduceTemperature = function()
+{
+ this.temperature = this.initialTemp * (1.0 - this.iteration / this.maxIterations);
+};