package org.trianacode.gui.util.organize; import org.trianacode.gui.main.TrianaLayoutConstants; import java.awt.*; import java.awt.geom.Point2D; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Map; /** * @author Andrew Harrison * @version 1.0.0 Nov 20, 2010 */ public class DagOrganizer { /** * Each vertex has a minimumLevel. Any vertex with no successors has * minimumLevel of zero. The minimumLevel of any vertex must be strictly * greater than the minimumLevel of its parents. (Vertex A is a parent of * Vertex B iff there is an edge from B to A.) Typically, a vertex will * have a minimumLevel which is one greater than the minimumLevel of its * parent's. However, if the vertex has two parents, its minimumLevel will * be one greater than the maximum of the parents'. We need to calculate * the minimumLevel for each vertex. When we layout the graph, vertices * cannot be drawn any higher than the minimumLevel. The graphHeight of a * graph is the greatest minimumLevel that is used. We will modify the * SpringLayout calculations so that nodes cannot move above their assigned * minimumLevel. */ private Map<OrganizableTask, Integer> minLevels = new HashMap<OrganizableTask, Integer>(); private OrganizableTaskGraph g; // Simpler than the "pair" technique. private int graphWidth; private int numRoots; final double SPACEFACTOR = 1.3; // How much space do we allow for additional floating at the bottom. final double LEVELATTRACTIONRATE = 0.1; private Dimension size; protected double stretch = 0.70; protected double lengthFunction = TrianaLayoutConstants.DEFAULT_TOOL_SIZE.width; protected int repulsion_range_sq = 100 * 100; protected double force_multiplier = 1.0 / 3.0; /** * A bunch of parameters to help work out when to stop quivering. * <p/> * If the MeanSquareVel(ocity) ever gets below the MSV_THRESHOLD, then we * will start a final cool-down phase of COOL_DOWN_INCREMENT increments. If * the MeanSquareVel ever exceeds the threshold, we will exit the cool down * phase, and continue looking for another opportunity. */ final double MSV_THRESHOLD = 10.0; double meanSquareVel; boolean stoppingIncrements = false; int incrementsLeft; final int COOL_DOWN_INCREMENTS = 200; /** * Creates an instance for the specified graph. */ public DagOrganizer(OrganizableTaskGraph g) { this.g = g; setRoot(); setSize(g.getSize()); } /** * setRoot calculates the level of each vertex in the graph. Level 0 is * allocated to any vertex with no successors. Level n+1 is allocated to * any vertex whose successors' maximum level is n. */ public void setRoot() { numRoots = 0; java.util.List<OrganizableTask> tasks = g.getTasks(); for (OrganizableTask task : tasks) { java.util.List<OrganizableTask> suc = g.getPredecessors(task); if (suc.size() == 0) { setRoot(task); numRoots++; } } } /** * Set vertex v to be level 0. */ public void setRoot(OrganizableTask t) { System.out.println("DagOrganizer.setRoot adding min level:" + t); minLevels.put(t, 0); // set all the levels. propagateMinimumLevel(t); } /** * A recursive method for allocating the level for each vertex. Ensures * that all predecessors of v have a level which is at least one greater * than the level of v. * * @param t */ public void propagateMinimumLevel(OrganizableTask t) { int level = minLevels.get(t); java.util.List<OrganizableTask> predecessors = g.getSuccessors(t); for (OrganizableTask child : predecessors) { int oldLevel, newLevel; Integer o = minLevels.get(child); if (o != null) oldLevel = o.intValue(); else oldLevel = 0; newLevel = Math.max(oldLevel, level + 1); minLevels.put(child, newLevel); if (newLevel > graphWidth) graphWidth = newLevel; propagateMinimumLevel(child); } } /** * Sets random locations for a vertex within the dimensions of the space. * This overrides the method in AbstractLayout * * @param coord * @param d */ private void initializeLocation(OrganizableTask t, Point2D coord, Dimension d) { int level = minLevels.get(t); int minX = (int) (level * d.getWidth() / (graphWidth * SPACEFACTOR)); double y = Math.random() * d.getWidth(); double x = Math.random() * (d.getHeight() - minX) + minX; coord.setLocation(x, y); } public void setSize(Dimension size) { if (size != null) { Dimension oldSize = this.size; this.size = size; if (oldSize != null) { adjustLocations(oldSize, size); } } this.size = size; java.util.List<OrganizableTask> tasks = g.getTasks(); for (OrganizableTask task : tasks) { //initializeLocation(task, task.getPoint(), getSize()); } } private void adjustLocations(Dimension oldSize, Dimension size) { int xOffset = (size.width - oldSize.width) / 2; int yOffset = (size.height - oldSize.height) / 2; // now, move each vertex to be at the new screen center while (true) { try { for (OrganizableTask v : g.getTasks()) { offsetVertex(v, xOffset, yOffset); } break; } catch (ConcurrentModificationException cme) { } } } protected void offsetVertex(OrganizableTask v, double xOffset, double yOffset) { Point2D c = v.getPoint(); c.setLocation(c.getX() + xOffset, c.getY() + yOffset); setLocation(v, c); } public Dimension getSize() { return size; } /** * Override the moveNodes() method from SpringLayout. The only change we * need to make is to make sure that nodes don't float higher than the minY * coordinate, as calculated by their minimumLevel. */ protected void moveNodes() { // Dimension d = currentSize; double oldMSV = meanSquareVel; meanSquareVel = 0; synchronized (getSize()) { java.util.List<OrganizableTask> tasks = g.getTasks(); for (OrganizableTask task : tasks) { Point2D xyd = task.getPoint(); System.out.println("DagOrganizer.moveNodes before x:" + xyd.getX() + " y:" + xyd.getY()); int width = getSize().width; int height = getSize().height; // (JY addition: three lines are new) int level = minLevels.get(task); int minX = (int) (level * width / (graphWidth * SPACEFACTOR)); int maxX = level == 0 ? (int) (width / (graphWidth * SPACEFACTOR * 2)) : width; // JY added 2* - double the sideways repulsion. task.dx += 2 * task.repulsiondx + task.edgedx; task.dy += task.repulsiondy;// + task.edgedy; // JY Addition: Attract the vertex towards it's minimumLevel // width. double delta = xyd.getX() - minX; task.dx -= delta * LEVELATTRACTIONRATE; if (level == 0) task.dx -= delta * LEVELATTRACTIONRATE; // twice as much at the top. // JY addition: meanSquareVel += (task.dx * task.dx + task.dy * task.dy); // keeps nodes from moving any faster than 5 per time unit xyd.setLocation(xyd.getX() + Math.max(-5, Math.min(5, task.dx)), xyd.getY() + Math.max(-1, Math.min(1, task.dy))); if (xyd.getX() < 0) { xyd.setLocation(0, xyd.getY()); } // } else if (xyd.getX() > width) { // xyd.setLocation(width, xyd.getY()); // } // (JY addition: These two lines replaced 0 with minY) if (xyd.getX() < minX) { xyd.setLocation(minX, xyd.getY()); } // (JY addition: replace height with maxY) // } else if (xyd.getX() > maxX) { // xyd.setLocation(maxX, xyd.getY()); // } // (JY addition: if there's only one root, anchor it in the // middle-top of the screen) if (numRoots == 1 && level == 0) { xyd.setLocation(xyd.getX(), height / 2); } System.out.println("DagOrganizer.moveNodes after x:" + xyd.getX() + " y:" + xyd.getY()); } } //System.out.println("MeanSquareAccel="+meanSquareVel); if (!stoppingIncrements && Math.abs(meanSquareVel - oldMSV) < MSV_THRESHOLD) { stoppingIncrements = true; incrementsLeft = COOL_DOWN_INCREMENTS; } else if ( stoppingIncrements && Math.abs(meanSquareVel - oldMSV) <= MSV_THRESHOLD) { incrementsLeft--; if (incrementsLeft <= 0) incrementsLeft = 0; } } public boolean done() { if (stoppingIncrements && incrementsLeft == 0) { System.out.println("DagOrganizer.done DONE"); return true; } else return false; } /** * Override forceMove so that if someone moves a node, we can re-layout * everything. */ public void setLocation(OrganizableTask picked, double x, double y) { Point2D coord = picked.getPoint(); coord.setLocation(x, y); stoppingIncrements = false; } /** * Override forceMove so that if someone moves a node, we can re-layout * everything. */ public void setLocation(OrganizableTask picked, Point2D p) { Point2D coord = picked.getPoint(); coord.setLocation(p); stoppingIncrements = false; } /** * Overridden relaxEdges. This one reduces the effect of edges between * greatly different levels. */ protected void relaxEdges() { for (OrganizableTaskGraph.Edge e : g.getEdges()) { OrganizableTask v1 = e.getSource(); OrganizableTask v2 = e.getDest(); Point2D p1 = v1.getPoint(); Point2D p2 = v2.getPoint(); double vx = p1.getX() - p2.getX(); double vy = p1.getY() - p2.getY(); double len = Math.sqrt(vx * vx + vy * vy); // JY addition. int level1 = minLevels.get(v1); int level2 = minLevels.get(v2); // desiredLen *= Math.pow( 1.1, (v1.degree() + v2.degree()) ); // double desiredLen = getLength(e); double desiredLen = lengthFunction; // round from zero, if needed [zero would be Bad.]. len = (len == 0) ? .0001 : len; // force factor: optimal length minus actual length, // is made smaller as the current actual length gets larger. // why? // System.out.println("Desired : " + getLength( e )); double f = force_multiplier * (desiredLen - len) / len; f = f * Math.pow(stretch / 100.0, (v1.getConnectionCount() + v2.getConnectionCount() - 2)); // JY addition. If this is an edge which stretches a long way, // don't be so concerned about it. if (level1 != level2) f = f / Math.pow(Math.abs(level2 - level1), 1.5); // f= Math.min( 0, f ); // the actual movement distance 'dx' is the force multiplied by the // distance to go. double dx = f * vx; double dy = f * vy; // SpringEdgeData<E> sed = getSpringEdgeData(e); // sed.f = f; v1.edgedx += dx; v1.edgedy += dy; v2.edgedx += -dx; v2.edgedy += -dy; } } private int stepCount = 0; public void step() { stepCount++; System.out.println("DagOrganizer.step " + stepCount); try { java.util.List<OrganizableTask> tasks = g.getTasks(); for (OrganizableTask task : tasks) { if (task == null) { continue; } task.dx /= 4; task.dy /= 4; task.edgedx = task.edgedy = 0; task.repulsiondx = task.repulsiondy = 0; } } catch (ConcurrentModificationException cme) { cme.printStackTrace(); step(); } relaxEdges(); calculateRepulsion(); moveNodes(); g.repaint(); } protected void calculateRepulsion() { try { java.util.List<OrganizableTask> tasks = g.getTasks(); for (OrganizableTask task : tasks) { if (task == null) continue; double dx = 0, dy = 0; for (OrganizableTask task2 : g.getTasks()) { if (task == task2) continue; Point2D p = task.getPoint(); Point2D p2 = task2.getPoint(); if (p == null || p2 == null) continue; double vx = p.getX() - p2.getX(); double vy = p.getY() - p2.getY(); double distanceSq = p.distanceSq(p2); if (distanceSq == 0) { dx += Math.random(); dy += Math.random(); } else if (distanceSq < repulsion_range_sq) { double factor = 1; dx += factor * vx / distanceSq; dy += factor * vy / distanceSq; } } double dlen = dx * dx + dy * dy; if (dlen > 0) { dlen = Math.sqrt(dlen) / 2; task.repulsiondx += dx / dlen; task.repulsiondy += (dy / dlen); } } } catch (ConcurrentModificationException cme) { calculateRepulsion(); } } }