/** * */ package org.hyperdata.scute.graph; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; import java.util.Iterator; import javax.swing.JPanel; import javax.swing.SwingUtilities; /** * The Class GraphLayout. * * @author danja * * FIXME threading isn't quite right, buttons in a row flash between start/stop * * TODO add edge uncrossing * * TODO check other algorithms */ public class GraphLayout implements Runnable { /** The Constant EDGE_MIN_LENGTH. */ private static final double EDGE_MIN_LENGTH = .1; /** The Constant CONSTANT2. */ private static final double CONSTANT2 = 1000; /** The Constant CONSTANT3. */ private static final double CONSTANT3 = 10000; /** The Constant CONSTANT4. */ private static final int CONSTANT4 = 500; /** The Constant RANDOMIZE_NODE_PROBABILITY. */ private static final double RANDOMIZE_NODE_PROBABILITY = 0.1; private boolean running = false; /** The pick. */ private Node pick; /** The pickfixed. */ private boolean pickfixed; /** The off screen. */ private Image offScreen; /** The off screen size. */ private Dimension offScreenSize; /** The off graphics. */ private Graphics2D offGraphics; /** The edge color. */ final Color edgeColor = Color.black; /** The arc color1. */ final Color arcColor1 = Color.black; /** The arc color2. */ final Color arcColor2 = Color.pink; /** The arc color3. */ final Color arcColor3 = Color.red; /** The springs. */ private Thread springs = null; /** The stress. */ boolean stress; /** The random. */ boolean random = true; /** The graph set. */ private final GraphSet graphSet; /** The panel. */ private final JPanel panel; /** The arrow scale. */ private final double arrowScale = 10; /** The origin arrow. */ private final Path2D.Double originArrow = getOriginArrow(); /** * Instantiates a new graph layout. * * @param panel * the panel * @param graphSet * the graph set */ public GraphLayout(JPanel panel, GraphSet graphSet) { this.graphSet = graphSet; this.panel = panel; } /** * Inits the. */ public void init() { scramble(); } /** * Checks if is running. * * @return true, if is running */ public boolean isRunning() { return this.running; } /** * Sets the running. * * @param run the new running */ public void setRunning(boolean run) { if (run) { scramble(); start(); } else { stop(); } this.running = run; } /** * Scramble. */ public void scramble() { Dimension d = panel.getSize(); Iterator<Node> nIterator = graphSet.nodeIterator(); while (nIterator.hasNext()) { Node n = nIterator.next(); if (!n.isFixed()) { n.setX(10 + (d.width - 20) * Math.random()); n.setY(10 + (d.height - 20) * Math.random()); } } } private Runnable cleanup = new Runnable() { @Override public void run() { relax(); } }; /* * (non-Javadoc) * * @see java.lang.Runnable#run() */ @Override public void run() { while (this.running) { relax(); // if (random && (Math.random() < RANDOMIZE_NODE_PROBABILITY)) { // Node n = graphSet.getRandomNode(); // randomizeNode(n); // } try { Thread.sleep(100); } catch (InterruptedException e) { // SwingUtilities.invokeLater(cleanupRunnable); this.running = false; break; } finally { // relax(); // meh } } SwingUtilities.invokeLater(cleanup); // one last relax } /** * Randomize node. * * @param node * the node */ private void randomizeNode(Node node) { if (!node.isFixed()) { node.setX(node.getX() + (panel.getWidth() * Math.random() - panel.getWidth() / 2)); node.setY(node.getY() + (panel.getHeight() * Math.random() - panel.getHeight() / 2)); } } /** * Relax. */ private void relax() { doEdges(); // TODO rename these doNodes(); doNodes2(); } /** * Do edges. */ private void doEdges() { // TODO rename Iterator<Edge> eIterator = graphSet.edgeIterator(); while (eIterator.hasNext()) { Edge e = eIterator.next(); double vx = e.to.getX() - e.from.getX(); double vy = e.to.getY() - e.from.getY(); double len = Math.sqrt(vx * vx + vy * vy); len = (len == 0) ? EDGE_MIN_LENGTH : len; double f = (e.len - len) / (len * CONSTANT2); double dx = f * vx; double dy = f * vy; e.to.setDx(e.to.getDx() + dx); e.to.setDy(e.to.getDy() + dy); e.from.setDx(e.from.getDx() + (-dx)); e.from.setDy(e.from.getDy() + (-dy)); } } /** * Do nodes. */ private void doNodes() { // TODO rename Iterator<Node> nIterator1 = graphSet.nodeIterator(); while (nIterator1.hasNext()) { Node n1 = nIterator1.next(); double dx = 0; double dy = 0; Iterator<Node> nIterator2 = graphSet.nodeIterator(); while (nIterator2.hasNext()) { Node n2 = nIterator2.next(); if (n1.equals(n2)) { continue; } double vx = n1.getX() - n2.getX(); double vy = n1.getY() - n2.getY(); double len = vx * vx + vy * vy; if (len == 0) { dx += Math.random(); dy += Math.random(); } else if (len < CONSTANT3) { dx += vx / len; dy += vy / len; } } double dlen = dx * dx + dy * dy; if (dlen > 0) { dlen = Math.sqrt(dlen) / 2; n1.setDx(n1.getDx() + (dx / dlen)); n1.setDy(n1.getDy() + (dy / dlen)); } } } /** * Do nodes2. */ private void doNodes2() { // TODO rename Dimension d = panel.getSize(); Iterator<Node> nIterator = graphSet.nodeIterator(); while (nIterator.hasNext()) { Node n = nIterator.next(); if (!n.isFixed()) { n.setX(n.getX() + Math.max(-CONSTANT4, Math.min(CONSTANT4, n.getDx()))); n.setY(n.getY() + Math.max(-CONSTANT4, Math.min(CONSTANT4, n.getDy()))); } if (n.getX() < 0) { n.setX(0); } else if (n.getX() > d.width) { n.setX(d.width); } if (n.getY() < 0) { n.setY(0); } else if (n.getY() > d.height) { n.setY(d.height); } n.setDx(n.getDx() / 2); n.setDy(n.getDy() / 2); } } /** * Gets the image. * * @return the image */ public Image getImage() { Dimension d = panel.getSize(); if ((offScreen == null) || (d.width != offScreenSize.width) || (d.height != offScreenSize.height)) { offScreen = panel.createImage(d.width, d.height); offScreenSize = d; if (offGraphics != null) { offGraphics.dispose(); } offGraphics = (Graphics2D) offScreen.getGraphics(); offGraphics.setFont(panel.getFont()); } offGraphics.setColor(panel.getBackground()); offGraphics.fillRect(0, 0, d.width, d.height); Iterator<Edge> eIterator = graphSet.edgeIterator(); while (eIterator.hasNext()) { Edge e = eIterator.next(); double xx1 = e.from.getX(); double yy1 = e.from.getY(); double xx2 = e.to.getX(); double yy2 = e.to.getY(); offGraphics.setColor(Color.blue); offGraphics.drawLine((int) xx1, (int) yy1, (int) xx2, (int) yy2); drawArrows(xx1, yy1, xx2, yy2); double centerX = ((xx1 + xx2) / 2); double centerY = ((yy1 + yy2) / 2); e.setCenter((int) centerX, (int) centerY); } // FontMetrics fm = offgraphics.getFontMetrics(); return offScreen; } /** * Gets the origin arrow. * * @return the origin arrow */ private Path2D.Double getOriginArrow() { // arrow shape, point at origin Path2D.Double arrow = new Path2D.Double(); arrow.moveTo(-1 * arrowScale, 0.5 * arrowScale); arrow.lineTo(0, 0); arrow.lineTo(-1 * arrowScale, -0.5 * arrowScale); return arrow; } /** * Draw arrows. * * @param xx1 * the xx1 * @param yy1 * the yy1 * @param xx2 * the xx2 * @param yy2 * the yy2 */ public void drawArrows(double xx1, double yy1, double xx2, double yy2) { // Decided against having two arrows on the line, left here in just case // double arrowOneX = xx1 + (xx2 - xx1)/4; // double arrowOneY = yy1 + (yy2 - yy1)/4; // // AffineTransform rotateOne = // AffineTransform.getRotateInstance(xx2-xx1, yy2-yy1, // arrowOneX,arrowOneY); // AffineTransform translateOne = // AffineTransform.getTranslateInstance(arrowOneX, arrowOneY); // rotateOne.concatenate(translateOne); // Shape arrowOne = rotateOne.createTransformedShape(arrow); // offGraphics.draw(arrowOne); double arrowTwoX = xx1 + 3 * (xx2 - xx1) / 4; double arrowTwoY = yy1 + 3 * (yy2 - yy1) / 4; AffineTransform rotateTwo = AffineTransform.getRotateInstance( xx2 - xx1, yy2 - yy1, arrowTwoX, arrowTwoY); AffineTransform translateTwo = AffineTransform.getTranslateInstance( arrowTwoX, arrowTwoY); rotateTwo.concatenate(translateTwo); Shape arrowTwo = rotateTwo.createTransformedShape(originArrow); offGraphics.draw(arrowTwo); } // unused, left here for the moment just in case /** * Draw arrows old. * * @param xx1 * the xx1 * @param yy1 * the yy1 * @param xx2 * the xx2 * @param yy2 * the yy2 */ public void drawArrowsOld(double xx1, double yy1, double xx2, double yy2) { offGraphics.setColor(Color.GREEN); double angle = 30 * Math.PI / 180; // 30' double cos = Math.cos(angle); double sin = Math.sin(angle); double p = xx2 - xx1; double q = yy2 - yy1; double len = Math.sqrt((xx2 - xx1) * (xx2 - xx1) + (yy2 - yy1) * (yy2 - yy1)); double ax = xx2 - (p * cos - q * sin) * 50 / len; double ay = yy2 - (q * cos + p * sin) * 50 / len; offGraphics.drawLine((int) xx2, (int) yy2, (int) ax, (int) ay); } /** * Start. */ public void start() { // if(springs == null){ springs = new Thread(this); // } springs.start(); } /** * Stop. */ public void stop() { springs.interrupt(); springs = null; } /** * Gets the pick. * * @return the pick */ public Node getPick() { return pick; } /** * Sets the pick fixed. * * @param fixed * the new pick fixed */ public void setPickFixed(boolean fixed) { pickfixed = fixed; } /** * Checks if is pick fixed. * * @return true, if is pick fixed */ public boolean isPickFixed() { return pickfixed; } }