1 /* 2 * TouchGraph LLC. Apache-Style Software License 3 * 4 * 5 * Copyright (c) 2001-2002 Alexander Shapiro. All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in 16 * the documentation and/or other materials provided with the 17 * distribution. 18 * 19 * 3. The end-user documentation included with the redistribution, 20 * if any, must include the following acknowledgment: 21 * "This product includes software developed by 22 * TouchGraph LLC (http://www.touchgraph.com/)." 23 * Alternately, this acknowledgment may appear in the software itself, 24 * if and wherever such third-party acknowledgments normally appear. 25 * 26 * 4. The names "TouchGraph" or "TouchGraph LLC" must not be used to endorse 27 * or promote products derived from this software without prior written 28 * permission. For written permission, please contact 29 * alex@touchgraph.com 30 * 31 * 5. Products derived from this software may not be called "TouchGraph", 32 * nor may "TouchGraph" appear in their name, without prior written 33 * permission of alex@touchgraph.com. 34 * 35 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED 36 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 37 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 38 * DISCLAIMED. IN NO EVENT SHALL TOUCHGRAPH OR ITS CONTRIBUTORS BE 39 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 40 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 41 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 42 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 43 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 44 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 45 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 46 * ==================================================================== 47 * 48 */ 49 50 package com.touchgraph.graphlayout; 51 52 import com.touchgraph.graphlayout.graphelements.*; 53 54 /*** TGLayout is the thread responsible for graph layout. It updates 55 * the real coordinates of the nodes in the graphEltSet object. 56 * TGPanel sends it resetDamper commands whenever the layout needs 57 * to be adjusted. After every adjustment cycle, TGLayout triggers 58 * a repaint of the TGPanel. 59 * 60 * ******************************************************************** 61 * This is the heart of the TouchGraph application. Please provide a 62 * Reference to TouchGraph.com if you are influenced by what you see 63 * below. Your cooperation will insure that this code remains 64 * opensource 65 * ******************************************************************** 66 * 67 * <p><b> 68 * Parts of this code build upon Sun's Graph Layout example. 69 * http://java.sun.com/applets/jdk/1.1/demo/GraphLayout/Graph.java 70 * </b></p> 71 * 72 * @author Alexander Shapiro 73 * @version 1.21 $Id: TGLayout.java,v 1.1.1.1 2004/02/06 08:44:05 keesj Exp $ 74 */ 75 public class TGLayout implements Runnable { 76 77 //private ImmutableGraphEltSet graphEltSet; 78 private TGPanel tgPanel; 79 private Thread relaxer; 80 private double damper=0.0; // A low damper value causes the graph to move slowly 81 private double maxMotion=0; // Keep an eye on the fastest moving node to see if the graph is stabilizing 82 private double lastMaxMotion=0; 83 private double motionRatio = 0; // It's sort of a ratio, equal to lastMaxMotion/maxMotion-1 84 private boolean damping = true; // When damping is true, the damper value decreases 85 86 private double rigidity = 1; // Rigidity has the same effect as the damper, except that it's a constant 87 // a low rigidity value causes things to go slowly. 88 // a value that's too high will cause oscillation 89 private double newRigidity = 1; 90 91 Node dragNode=null; 92 93 // ............ 94 95 /*** Constructor with a supplied TGPanel <tt>tgp</tt>. 96 */ 97 public TGLayout( TGPanel tgp ) { 98 tgPanel = tgp; 99 //graphEltSet = tgPanel.getGES(); 100 relaxer = null; 101 } 102 103 void setRigidity(double r) { 104 newRigidity = r; //update rigidity at the end of the relax() thread 105 } 106 107 void setDragNode(Node n) { 108 dragNode = n; 109 } 110 111 112 //relaxEdges is more like tense edges up. All edges pull nodes closes together; 113 private synchronized void relaxEdges() { 114 TGForEachEdge fee = new TGForEachEdge() { 115 public void forEachEdge(Edge e) { 116 117 double vx = e.to.x - e.from.x; 118 double vy = e.to.y - e.from.y; 119 double len = Math.sqrt(vx * vx + vy * vy); 120 121 double dx=vx*rigidity; //rigidity makes edges tighter 122 double dy=vy*rigidity; 123 124 dx /=(e.getLength()*100); 125 dy /=(e.getLength()*100); 126 127 // Edges pull directly in proportion to the distance between the nodes. This is good, 128 // because we want the edges to be stretchy. The edges are ideal rubberbands. They 129 // They don't become springs when they are too short. That only causes the graph to 130 // oscillate. 131 132 if (e.to.justMadeLocal || !e.from.justMadeLocal) { 133 e.to.dx -= dx*len; 134 e.to.dy -= dy*len; 135 } else { 136 e.to.dx -= dx*len/10; 137 e.to.dy -= dy*len/10; 138 } 139 if (e.from.justMadeLocal || !e.to.justMadeLocal) { 140 e.from.dx += dx*len; 141 e.from.dy += dy*len; 142 } else { 143 e.from.dx += dx*len/10; 144 e.from.dy += dy*len/10; 145 } 146 } 147 }; 148 149 tgPanel.getGES().forAllEdges(fee); 150 } 151 152 /* 153 private synchronized void avoidLabels() { 154 for (int i = 0 ; i < graphEltSet.nodeNum() ; i++) { 155 Node n1 = graphEltSet.nodeAt(i); 156 double dx = 0; 157 double dy = 0; 158 159 for (int j = 0 ; j < graphEltSet.nodeNum() ; j++) { 160 if (i == j) { 161 continue; // It's kind of dumb to do things this way. j should go from i+1 to nodeNum. 162 } 163 Node n2 = graphEltSet.nodeAt(j); 164 double vx = n1.x - n2.x; 165 double vy = n1.y - n2.y; 166 double len = vx * vx + vy * vy; // so it's length squared 167 if (len == 0) { 168 dx += Math.random(); // If two nodes are right on top of each other, randomly separate 169 dy += Math.random(); 170 } else if (len <600*600) { //600, because we don't want deleted nodes to fly too far away 171 dx += vx / len; // If it was sqrt(len) then a single node surrounded by many others will 172 dy += vy / len; // always look like a circle. This might look good at first, but I think 173 // it makes large graphs look ugly + it contributes to oscillation. A 174 // linear function does not fall off fast enough, so you get rough edges 175 // in the 'force field' 176 177 } 178 } 179 n1.dx += dx*100*rigidity; // rigidity makes nodes avoid each other more. 180 n1.dy += dy*100*rigidity; // I was surprised to see that this exactly balances multiplying edge tensions 181 // by the rigidity, and thus has the effect of slowing the graph down, while 182 // keeping it looking identical. 183 184 } 185 } 186 */ 187 188 private synchronized void avoidLabels() { 189 TGForEachNodePair fenp = new TGForEachNodePair() { 190 public void forEachNodePair(Node n1, Node n2) { 191 double dx=0; 192 double dy=0; 193 double vx = n1.x - n2.x; 194 double vy = n1.y - n2.y; 195 double len = vx * vx + vy * vy; //so it's length squared 196 if (len == 0) { 197 dx = Math.random(); //If two nodes are right on top of each other, randomly separate 198 dy = Math.random(); 199 } else if (len <600*600) { //600, because we don't want deleted nodes to fly too far away 200 dx = vx / len; // If it was sqrt(len) then a single node surrounded by many others will 201 dy = vy / len; // always look like a circle. This might look good at first, but I think 202 // it makes large graphs look ugly + it contributes to oscillation. A 203 // linear function does not fall off fast enough, so you get rough edges 204 // in the 'force field' 205 } 206 207 int repSum = n1.repulsion * n2.repulsion/100; 208 209 if(n1.justMadeLocal || !n2.justMadeLocal) { 210 n1.dx += dx*repSum*rigidity; 211 n1.dy += dy*repSum*rigidity; 212 } 213 else { 214 n1.dx += dx*repSum*rigidity/10; 215 n1.dy += dy*repSum*rigidity/10; 216 } 217 if (n2.justMadeLocal || !n1.justMadeLocal) { 218 n2.dx -= dx*repSum*rigidity; 219 n2.dy -= dy*repSum*rigidity; 220 } 221 else { 222 n2.dx -= dx*repSum*rigidity/10; 223 n2.dy -= dy*repSum*rigidity/10; 224 } 225 } 226 }; 227 228 tgPanel.getGES().forAllNodePairs(fenp); 229 } 230 231 public void startDamper() { 232 damping = true; 233 } 234 235 public void stopDamper() { 236 damping = false; 237 damper = 1.0; //A value of 1.0 means no damping 238 } 239 240 public void resetDamper() { //reset the damper, but don't keep damping. 241 damping = true; 242 damper = 1.0; 243 } 244 245 public void stopMotion() { // stabilize the graph, but do so gently by setting the damper to a low value 246 damping = true; 247 if (damper>0.3) 248 damper = 0.3; 249 else 250 damper = 0; 251 } 252 253 public void damp() { 254 if (damping) { 255 if(motionRatio<=0.001) { //This is important. Only damp when the graph starts to move faster 256 //When there is noise, you damp roughly half the time. (Which is a lot) 257 // 258 //If things are slowing down, then you can let them do so on their own, 259 //without damping. 260 261 //If max motion<0.2, damp away 262 //If by the time the damper has ticked down to 0.9, maxMotion is still>1, damp away 263 //We never want the damper to be negative though 264 if ((maxMotion<0.2 || (maxMotion>1 && damper<0.9)) && damper > 0.01) damper -= 0.01; 265 //If we've slowed down significanly, damp more aggresively (then the line two below) 266 else if (maxMotion<0.4 && damper > 0.003) damper -= 0.003; 267 //If max motion is pretty high, and we just started damping, then only damp slightly 268 else if(damper>0.0001) damper -=0.0001; 269 } 270 } 271 if(maxMotion<0.001 && damping) { 272 damper=0; 273 } 274 } 275 276 277 private synchronized void moveNodes() { 278 lastMaxMotion = maxMotion; 279 final double[] maxMotionA = new double[1]; 280 maxMotionA[0]=0; 281 282 TGForEachNode fen = new TGForEachNode() { 283 public void forEachNode(Node n) { 284 double dx = n.dx; 285 double dy = n.dy; 286 dx*=damper; //The damper slows things down. It cuts down jiggling at the last moment, and optimizes 287 dy*=damper; //layout. As an experiment, get rid of the damper in these lines, and make a 288 //long straight line of nodes. It wiggles too much and doesn't straighten out. 289 290 n.dx=dx/2; //Slow down, but don't stop. Nodes in motion store momentum. This helps when the force 291 n.dy=dy/2; //on a node is very low, but you still want to get optimal layout. 292 293 double distMoved = Math.sqrt(dx*dx+dy*dy); //how far did the node actually move? 294 295 if (!n.fixed && !(n==dragNode) ) { 296 n.x += Math.max(-30, Math.min(30, dx)); //don't move faster then 30 units at a time. 297 n.y += Math.max(-30, Math.min(30, dy)); //I forget when this is important. Stopping severed nodes from 298 //flying away? 299 } 300 maxMotionA[0]=Math.max(distMoved,maxMotionA[0]); 301 } 302 }; 303 304 tgPanel.getGES().forAllNodes(fen); 305 306 maxMotion=maxMotionA[0]; 307 if (maxMotion>0) motionRatio = lastMaxMotion/maxMotion-1; //subtract 1 to make a positive value mean that 308 else motionRatio = 0; //things are moving faster 309 310 damp(); 311 312 } 313 314 private synchronized void relax() { 315 for (int i=0;i<10;i++) { 316 relaxEdges(); 317 avoidLabels(); 318 moveNodes(); 319 } 320 if(rigidity!=newRigidity) rigidity= newRigidity; //update rigidity 321 tgPanel.repaintAfterMove(); 322 } 323 324 private void myWait() { //I think it was Netscape that caused me not to use Wait, or was it java 1.1? 325 try { 326 Thread.sleep(100); 327 } catch (InterruptedException e) { 328 //break; 329 } 330 } 331 332 public void run() { 333 Thread me = Thread.currentThread(); 334 // me.setPriority(1); //Makes standard executable look better, but the applet look worse. 335 while (relaxer == me) { 336 relax(); 337 try { 338 Thread.sleep(20); //Delay to wait for the prior repaint command to finish. 339 while(damper<0.1 && damping && maxMotion<0.001) myWait(); 340 //System.out.println("damping " + damping + " damp " + damper + " maxM " + maxMotion + " motR " + motionRatio ); 341 } catch (InterruptedException e) { 342 break; 343 } 344 } 345 } 346 347 public void start() { 348 relaxer = new Thread(this); 349 relaxer.start(); 350 } 351 352 public void stop() { 353 relaxer = null; 354 } 355 356 } // end com.touchgraph.graphlayout.TGLayout