package automenta.vivisect.dimensionalize; import automenta.vivisect.graph.AbstractGraphVis; import automenta.vivisect.graph.EdgeVis; import automenta.vivisect.graph.GraphDisplay; import automenta.vivisect.graph.VertexVis; import com.mxgraph.util.mxRectangle; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.jgrapht.Graph; /** * Fast organic layout algorithm, adapted from JGraph */ public class FastOrganicLayout<V, E> implements GraphDisplay<V,E> { /** * Specifies if the top left corner of the input cells should be the origin * of the layout result. Default is true. */ protected boolean useInputOrigin = true; /** * Specifies if all edge points of traversed edge should be removed. * Default is true. */ protected boolean resetEdges = true; /** * Specifies if the STYLE_NOEDGESTYLE flag should be set on edge that are modified by the result. Default is true. */ protected boolean disableEdgeStyle = true; /** * 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. */ protected double forceConstant = 50; /** * Cache of <forceConstant>^2 for performance. */ protected double forceConstantSquared = 0; /** * Minimal distance limit. Default is 2. Prevents of dividing by zero. */ protected double minDistanceLimit = 2; /** * Cached version of <minDistanceLimit> squared. */ protected double minDistanceLimitSquared = 0; /** * The maximum distance between vertex, beyond which their repulsion no longer has an effect */ protected double maxDistanceLimit = 500; /** * Start value of temperature. Default is 200. */ protected double initialTemp = 200; /** * Temperature to limit displacement at later stages of layout. */ protected double temperature = 0; /** * Total number of iterations to run the layout though. */ protected double maxIterations = 0; /** * Current iteration count. */ protected double iteration = 0; /** * An array of all vertex to be laid out. */ protected List<VertexVis> vertexArray; /** * An array of locally stored X co-ordinate displacements for the vertex. */ protected double[] dispX; /** * An array of locally stored Y co-ordinate displacements for the vertex. */ protected double[] dispY; /** * An array of locally stored co-ordinate positions for the vertex. */ protected double[][] cellLocation; /** * The approximate radius of each cell, nodes only. */ protected double[] radius; /** * The approximate radius squared of each cell, nodes only. */ protected double[] radiusSquared; /** * Array of booleans representing the movable states of the vertex. */ protected boolean[] isMoveable; /** * Local copy of cell neighbours. */ protected int[][] neighbors; /** * 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. */ protected boolean allowedToRun = true; /** * Maps from vertex to indices. */ protected Map<V, Integer> indices; /** * Constructs a new fast organic layout for the specified graph. */ public FastOrganicLayout() { setInitialTemp(13f); setMinDistanceLimit(1f); setMaxDistanceLimit(200f); setForceConstant(100f); setMaxIterations(1); } /** * Returns a boolean indicating if the given <mxCell> should be ignored as a * vertex. This returns true if the cell has no connections. * * @param vertex Object that represents the vertex to be tested. * @return Returns true if the vertex should be ignored. */ public boolean isVertexIgnored(V vertex) { return false; //return super.isVertexIgnored(vertex) // graph.getConnections(vertex).length == 0; } /** * */ public boolean isUseInputOrigin() { return useInputOrigin; } /** * * @param value */ public void setUseInputOrigin(boolean value) { useInputOrigin = value; } /** * */ public boolean isResetEdges() { return resetEdges; } /** * * @param value */ public void setResetEdges(boolean value) { resetEdges = value; } /** * */ public boolean isDisableEdgeStyle() { return disableEdgeStyle; } /** * * @param value */ public void setDisableEdgeStyle(boolean value) { disableEdgeStyle = value; } /** * */ public double getMaxIterations() { return maxIterations; } /** * * @param value */ public void setMaxIterations(double value) { maxIterations = value; } /** * */ public double getForceConstant() { return forceConstant; } /** * * @param value */ public void setForceConstant(double value) { forceConstant = value; } /** * */ public double getMinDistanceLimit() { return minDistanceLimit; } /** * * @param value */ public void setMinDistanceLimit(double value) { minDistanceLimit = value; } /** * @return the maxDistanceLimit */ public double getMaxDistanceLimit() { return maxDistanceLimit; } /** * @param maxDistanceLimit the maxDistanceLimit to set */ public void setMaxDistanceLimit(double maxDistanceLimit) { this.maxDistanceLimit = maxDistanceLimit; } /** * */ public double getInitialTemp() { return initialTemp; } /** * * @param value */ public void setInitialTemp(double value) { initialTemp = value; } /** * Reduces the temperature of the layout from an initial setting in a linear * fashion to zero. */ protected void reduceTemperature() { temperature = initialTemp * (1.0 - iteration / maxIterations); } @Override public boolean postUpdate(AbstractGraphVis<V,E> g) { Graph<V,E> graph = g.getGraph(); if (indices == null) indices = new HashMap<>(); else indices.clear(); // Finds the relevant vertex for the layout if (vertexArray == null) vertexArray = new ArrayList(); else vertexArray.clear(); for (V v : graph.vertexSet()) { VertexVis vd = g.getVertexDisplay(v); if (vd == null) continue; if (vd.getRadius() == 0) continue; vertexArray.add(vd); } mxRectangle initialBounds = null; //new mxRectangle(-100, -50, 100, 50); //? graph.getBoundsForCells(vertexArray, false, false, true) : null; int n = vertexArray.size(); if ((cellLocation == null) || (cellLocation.length!=n)) { dispX = new double[n]; dispY = new double[n]; cellLocation = new double[n][]; isMoveable = new boolean[n]; neighbors = new int[n][]; radius = new double[n]; radiusSquared = new double[n]; } minDistanceLimitSquared = minDistanceLimit * minDistanceLimit; if (forceConstant < 0.001) { forceConstant = 0.001; } forceConstantSquared = forceConstant * forceConstant; // Create a map of vertex 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 (int i = 0; i < n; i++) { VertexVis<V,E> vd = vertexArray.get(i); //TODO is this necessary? /*if (!graph.containsVertex(vd.getVertex())) continue;*/ /*if (vd == null) { vd = new VertexVis(vertex); displayed.put(vertex, vd); }*/ if (cellLocation[i]==null) cellLocation[i] = new double[2]; // Set up the mapping from array indices to cells indices.put(vd.vertex, i); //mxRectangle bounds = getVertexBounds(vertex); // Set the X,Y value of the internal version of the cell to // the center point of the vertex for better positioning double width = vd.getRadius()*2f; //bounds.getWidth(); double height = vd.getRadius()*2f; //bounds.getHeight(); // Randomize (0, 0) locations //TODO re-use existing location double x, y; if (vd==null) { x = 0;//Math.random() * 100.0;//Math.random() * 100; //bounds.getX(); y = 0;//Math.random() * 100.0; } else { x = vd.getX(); y = vd.getY(); } cellLocation[i][0] = x + width / 2.0; cellLocation[i][1] = y + height / 2.0; radius[i] = Math.min(width, height); radiusSquared[i] = radius[i] * radius[i]; // Moves cell location back to top-left from center locations used in // algorithm, resetting the edge points is part of the transaction dispX[i] = 0; dispY[i] = 0; isMoveable[i] = true; //isVertexMovable(vertexArray[i]); // Get lists of neighbours to all vertex, translate the cells // obtained in indices into vertexArray and store as an array // against the original cell index //V v = vertexArray.get(i).getVertex(); //ProcessingGraphCanvas.VertexVis vd = displayed.get(v); //TODO why does a vertex disappear from the graph... make this unnecessary V v = vd.getVertex(); Set<E> edges = vd.getEdges(); if (edges!=null) { List<V> cells = new ArrayList(edges.size()); for (E e : edges) { if (isResetEdges()) { //graph.resetEdge(edge[k]); } if (isDisableEdgeStyle()) { //setEdgeStyleEnabled(edge[k], false); } V source = graph.getEdgeSource(e); V target = graph.getEdgeTarget(e); if (source!=v) cells.add(source); else if (target!=v) cells.add(target); } neighbors[i] = new int[cells.size()]; for (int j = 0; j < cells.size(); j++) { Integer index = indices.get(cells.get(j)); // Check the connected cell in part of the vertex list to be // acted on by this layout if (index != null) { neighbors[i][j] = index.intValue(); } // 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 { neighbors[i][j] = i; } } } } temperature = initialTemp; // If max number of iterations has not been set, guess it if (maxIterations == 0) { maxIterations = 20.0 * Math.sqrt(n); } // Main iteration loop try { for (iteration = 0; iteration < maxIterations; iteration++) { if (!allowedToRun) { return false; } // Calculate repulsive forces on all vertex calcRepulsion(); // Calculate attractive forces through edge calcAttraction(); calcPositions(); reduceTemperature(); } } catch (Exception e) { } double minx = 0, miny = 0, maxx = 0, maxy = 0; for (int i = 0; i < vertexArray.size(); i++) { VertexVis vd = vertexArray.get(i); if (vd != null) { //cellLocation[i][0] -= 1/2.0; //geo.getWidth() / 2.0; //cellLocation[i][1] -= 1/2.0; //geo.getHeight() / 2.0; float r = vd.getRadius(); double x = /*graph.snap*/(cellLocation[i][0] - r); double y = /*graph.snap*/(cellLocation[i][1] - r); vd.setPosition((float)x, (float)y); if (i == 0) { minx = maxx = x; miny = maxy = y; } else { if (x < minx) minx = x; if (y < miny) miny = y; if (x > maxx) maxx = x; if (y > maxy) maxy = y; } } } // Modifies the cloned geometries in-place. Not needed // to clone the geometries again as we're in the same // undoable change. double dx = -(maxx+minx)/2f; double dy = -(maxy+miny)/2f; if (initialBounds != null) { dx += initialBounds.getX(); dy += initialBounds.getY(); } for (int i = 0; i < vertexArray.size(); i++) { VertexVis vd = vertexArray.get(i); vd.movePosition((float)dx, (float)dy); } return true; } /** * Takes the displacements calculated for each cell and applies them to the * local cache of cell positions. Limits the displacement to the current * temperature. */ protected void calcPositions() { for (int index = 0; index < vertexArray.size(); index++) { if (isMoveable[index]) { // Get the distance of displacement for this node for this // iteration double deltaLength = Math.sqrt(dispX[index] * dispX[index] + dispY[index] * dispY[index]); if (deltaLength < 0.001) { deltaLength = 0.001; } // Scale down by the current temperature if less than the // displacement distance double newXDisp = dispX[index] / deltaLength * Math.min(deltaLength, temperature); double newYDisp = dispY[index] / deltaLength * Math.min(deltaLength, temperature); // reset displacements dispX[index] = 0; dispY[index] = 0; // Update the cached cell locations cellLocation[index][0] += newXDisp; cellLocation[index][1] += newYDisp; } } } /** * Calculates the attractive forces between all laid out nodes linked by edge */ protected void calcAttraction() { // Check the neighbours of each vertex and calculate the attractive // force of the edge connecting them for (int i = 0; i < vertexArray.size(); i++) { if (neighbors[i]==null) continue; if (cellLocation[i] == null) continue; for (int k = 0; k < neighbors[i].length; k++) { // Get the index of the othe cell in the vertex array int j = neighbors[i][k]; if (cellLocation[j] == null) continue; // Do not proceed self-loops if (i != j) { double xDelta = cellLocation[i][0] - cellLocation[j][0]; double yDelta = cellLocation[i][1] - cellLocation[j][1]; // The distance between the nodes double deltaLengthSquared = xDelta * xDelta + yDelta * yDelta - radiusSquared[i] - radiusSquared[j]; if (deltaLengthSquared < minDistanceLimitSquared) { deltaLengthSquared = minDistanceLimitSquared; } double deltaLength = Math.sqrt(deltaLengthSquared); double force = (deltaLengthSquared) / forceConstant; double displacementX = (xDelta / deltaLength) * force; double displacementY = (yDelta / deltaLength) * force; if (isMoveable[i]) { this.dispX[i] -= displacementX; this.dispY[i] -= displacementY; } if (isMoveable[j]) { dispX[j] += displacementX; dispY[j] += displacementY; } } } } } /** * Calculates the repulsive forces between all laid out nodes */ protected void calcRepulsion() { int vertexCount = vertexArray.size(); for (int i = 0; i < vertexCount; i++) { for (int j = i; j < vertexCount; j++) { // Exits if the layout is no longer allowed to run if (!allowedToRun) { return; } if ((j != i) && (cellLocation[i]!=null) && (cellLocation[j]!=null)) { double xDelta = cellLocation[i][0] - cellLocation[j][0]; double yDelta = cellLocation[i][1] - cellLocation[j][1]; if (xDelta == 0) { xDelta = 0.01 + Math.random(); } if (yDelta == 0) { yDelta = 0.01 + Math.random(); } // Distance between nodes double deltaLength = Math.sqrt((xDelta * xDelta) + (yDelta * yDelta)); double deltaLengthWithRadius = deltaLength - radius[i] - radius[j]; if (deltaLengthWithRadius > maxDistanceLimit) { // Ignore vertex too far apart continue; } if (deltaLengthWithRadius < minDistanceLimit) { deltaLengthWithRadius = minDistanceLimit; } double force = forceConstantSquared / deltaLengthWithRadius; double displacementX = (xDelta / deltaLength) * force; double displacementY = (yDelta / deltaLength) * force; if (isMoveable[i]) { dispX[i] += displacementX; dispY[i] += displacementY; } if (isMoveable[j]) { dispX[j] -= displacementX; dispY[j] -= displacementY; } } } } } @Override public boolean preUpdate(AbstractGraphVis<V,E> g) { return true; } @Override public void vertex(AbstractGraphVis<V, E> g, VertexVis<V, E> v) { } @Override public void edge(AbstractGraphVis<V, E> g, EdgeVis<V, E> e) { } }