View Javadoc
1   /*
2    * Copyright (c) 2003, The JUNG Authors
3    * All rights reserved.
4    *
5    * This software is open-source under the BSD license; see either "license.txt"
6    * or https://github.com/jrtom/jung/blob/master/LICENSE for a description.
7    */
8   package edu.uci.ics.jung.algorithms.layout;
9   
10  import java.awt.Dimension;
11  import java.awt.event.ComponentAdapter;
12  import java.awt.event.ComponentEvent;
13  import java.awt.geom.Point2D;
14  import java.util.ConcurrentModificationException;
15  
16  import com.google.common.base.Function;
17  import com.google.common.base.Functions;
18  import com.google.common.cache.CacheBuilder;
19  import com.google.common.cache.CacheLoader;
20  import com.google.common.cache.LoadingCache;
21  
22  import edu.uci.ics.jung.algorithms.layout.util.RandomLocationTransformer;
23  import edu.uci.ics.jung.algorithms.util.IterativeContext;
24  import edu.uci.ics.jung.graph.Graph;
25  import edu.uci.ics.jung.graph.util.Pair;
26  
27  /**
28   * The SpringLayout package represents a visualization of a set of nodes. The
29   * SpringLayout, which is initialized with a Graph, assigns X/Y locations to
30   * each node. When called <code>relax()</code>, the SpringLayout moves the
31   * visualization forward one step.
32   *
33   * @author Danyel Fisher
34   * @author Joshua O'Madadhain
35   */
36  public class SpringLayout<V, E> extends AbstractLayout<V,E> implements IterativeContext {
37  
38      protected double stretch = 0.70;
39      protected Function<? super E, Integer> lengthFunction;
40      protected int repulsion_range_sq = 100 * 100;
41      protected double force_multiplier = 1.0 / 3.0;
42  
43      protected LoadingCache<V, SpringVertexData> springVertexData =
44      	CacheBuilder.newBuilder().build(new CacheLoader<V, SpringVertexData>() {
45  	    	public SpringVertexData load(V vertex) {
46  	    		return new SpringVertexData();
47  	    	}
48      });
49  //    protected Map<V, SpringVertexData> springVertexData =
50  //    	new MapMaker().makeComputingMap(new Function<V,SpringVertexData>(){
51  //			public SpringVertexData apply(V arg0) {
52  //				return new SpringVertexData();
53  //			}});
54  
55      /**
56       * Constructor for a SpringLayout for a raw graph with associated
57       * dimension--the input knows how big the graph is. Defaults to the unit
58       * length function.
59   	 * @param g the graph on which the layout algorithm is to operate
60      */
61      @SuppressWarnings("unchecked")
62      public SpringLayout(Graph<V,E> g) {
63          this(g, (Function<E,Integer>)Functions.<Integer>constant(30));
64      }
65  
66      /**
67       * Constructor for a SpringLayout for a raw graph with associated component.
68       *
69  	 * @param g the graph on which the layout algorithm is to operate
70       * @param length_function provides a length for each edge
71       */
72      public SpringLayout(Graph<V,E> g, Function<? super E, Integer> length_function)
73      {
74          super(g);
75          this.lengthFunction = length_function;
76      }
77  
78      /**
79       * @return the current value for the stretch parameter
80       */
81      public double getStretch() {
82          return stretch;
83      }
84  
85  	@Override
86  	public void setSize(Dimension size) {
87  		if(initialized == false)
88  			setInitializer(new RandomLocationTransformer<V>(size));
89  		super.setSize(size);
90  	}
91  
92      /**
93       * <p>Sets the stretch parameter for this instance.  This value
94       * specifies how much the degrees of an edge's incident vertices
95       * should influence how easily the endpoints of that edge
96       * can move (that is, that edge's tendency to change its length).
97       *
98       * <p>The default value is 0.70.  Positive values less than 1 cause
99       * high-degree vertices to move less than low-degree vertices, and
100      * values &gt; 1 cause high-degree vertices to move more than
101      * low-degree vertices.  Negative values will have unpredictable
102      * and inconsistent results.
103      * @param stretch the stretch parameter
104      */
105     public void setStretch(double stretch) {
106         this.stretch = stretch;
107     }
108 
109     public int getRepulsionRange() {
110         return (int)(Math.sqrt(repulsion_range_sq));
111     }
112 
113     /**
114      * Sets the node repulsion range (in drawing area units) for this instance.
115      * Outside this range, nodes do not repel each other.  The default value
116      * is 100.  Negative values are treated as their positive equivalents.
117      * @param range the maximum repulsion range
118      */
119     public void setRepulsionRange(int range) {
120         this.repulsion_range_sq = range * range;
121     }
122 
123     public double getForceMultiplier() {
124         return force_multiplier;
125     }
126 
127     /**
128      * Sets the force multiplier for this instance.  This value is used to
129      * specify how strongly an edge "wants" to be its default length
130      * (higher values indicate a greater attraction for the default length),
131      * which affects how much its endpoints move at each timestep.
132      * The default value is 1/3.  A value of 0 turns off any attempt by the
133      * layout to cause edges to conform to the default length.  Negative
134      * values cause long edges to get longer and short edges to get shorter; use
135      * at your own risk.
136      * @param force an energy field created by all living things that binds the galaxy together
137      */
138     public void setForceMultiplier(double force) {
139         this.force_multiplier = force;
140     }
141 
142     public void initialize() {
143     }
144 
145     /**
146      * Relaxation step. Moves all nodes a smidge.
147      */
148     public void step() {
149     	try {
150     		for(V v : getGraph().getVertices()) {
151     			SpringVertexData svd = springVertexData.getUnchecked(v);
152     			if (svd == null) {
153     				continue;
154     			}
155     			svd.dx /= 4;
156     			svd.dy /= 4;
157     			svd.edgedx = svd.edgedy = 0;
158     			svd.repulsiondx = svd.repulsiondy = 0;
159     		}
160     	} catch(ConcurrentModificationException cme) {
161     		step();
162     	}
163 
164     	relaxEdges();
165     	calculateRepulsion();
166     	moveNodes();
167     }
168 
169     protected void relaxEdges() {
170     	try {
171     		for(E e : getGraph().getEdges()) {
172     		    Pair<V> endpoints = getGraph().getEndpoints(e);
173     			V v1 = endpoints.getFirst();
174     			V v2 = endpoints.getSecond();
175 
176     			Point2D p1 = apply(v1);
177     			Point2D p2 = apply(v2);
178     			if(p1 == null || p2 == null) continue;
179     			double vx = p1.getX() - p2.getX();
180     			double vy = p1.getY() - p2.getY();
181     			double len = Math.sqrt(vx * vx + vy * vy);
182 
183     			double desiredLen = lengthFunction.apply(e);
184 
185     			// round from zero, if needed [zero would be Bad.].
186     			len = (len == 0) ? .0001 : len;
187 
188     			double f = force_multiplier * (desiredLen - len) / len;
189 
190     			f = f * Math.pow(stretch, (getGraph().degree(v1) + getGraph().degree(v2) - 2));
191 
192     			// the actual movement distance 'dx' is the force multiplied by the
193     			// distance to go.
194     			double dx = f * vx;
195     			double dy = f * vy;
196     			SpringVertexData v1D, v2D;
197     			v1D = springVertexData.getUnchecked(v1);
198     			v2D = springVertexData.getUnchecked(v2);
199 
200     			v1D.edgedx += dx;
201     			v1D.edgedy += dy;
202     			v2D.edgedx += -dx;
203     			v2D.edgedy += -dy;
204     		}
205     	} catch(ConcurrentModificationException cme) {
206     		relaxEdges();
207     	}
208     }
209 
210     protected void calculateRepulsion() {
211         try {
212         for (V v : getGraph().getVertices()) {
213             if (isLocked(v)) continue;
214 
215             SpringVertexData svd = springVertexData.getUnchecked(v);
216             if(svd == null) continue;
217             double dx = 0, dy = 0;
218 
219             for (V v2 : getGraph().getVertices()) {
220                 if (v == v2) continue;
221                 Point2D p = apply(v);
222                 Point2D p2 = apply(v2);
223                 if(p == null || p2 == null) continue;
224                 double vx = p.getX() - p2.getX();
225                 double vy = p.getY() - p2.getY();
226                 double distanceSq = p.distanceSq(p2);
227                 if (distanceSq == 0) {
228                     dx += Math.random();
229                     dy += Math.random();
230                 } else if (distanceSq < repulsion_range_sq) {
231                     double factor = 1;
232                     dx += factor * vx / distanceSq;
233                     dy += factor * vy / distanceSq;
234                 }
235             }
236             double dlen = dx * dx + dy * dy;
237             if (dlen > 0) {
238                 dlen = Math.sqrt(dlen) / 2;
239                 svd.repulsiondx += dx / dlen;
240                 svd.repulsiondy += dy / dlen;
241             }
242         }
243         } catch(ConcurrentModificationException cme) {
244             calculateRepulsion();
245         }
246     }
247 
248     protected void moveNodes()
249     {
250         synchronized (getSize()) {
251             try {
252                 for (V v : getGraph().getVertices()) {
253                     if (isLocked(v)) continue;
254                     SpringVertexData vd = springVertexData.getUnchecked(v);
255                     if(vd == null) continue;
256                     Point2D xyd = apply(v);
257 
258                     vd.dx += vd.repulsiondx + vd.edgedx;
259                     vd.dy += vd.repulsiondy + vd.edgedy;
260 
261                     // keeps nodes from moving any faster than 5 per time unit
262                     xyd.setLocation(xyd.getX()+Math.max(-5, Math.min(5, vd.dx)),
263                     		xyd.getY()+Math.max(-5, Math.min(5, vd.dy)));
264 
265                     Dimension d = getSize();
266                     int width = d.width;
267                     int height = d.height;
268 
269                     if (xyd.getX() < 0) {
270                         xyd.setLocation(0, xyd.getY());
271                     } else if (xyd.getX() > width) {
272                         xyd.setLocation(width, xyd.getY());
273                     }
274                     if (xyd.getY() < 0) {
275                         xyd.setLocation(xyd.getX(), 0);
276                     } else if (xyd.getY() > height) {
277                         xyd.setLocation(xyd.getX(), height);
278                     }
279 
280                 }
281             } catch(ConcurrentModificationException cme) {
282                 moveNodes();
283             }
284         }
285     }
286 
287     protected static class SpringVertexData {
288         protected double edgedx;
289         protected double edgedy;
290         protected double repulsiondx;
291         protected double repulsiondy;
292 
293         /** movement speed, x */
294         protected double dx;
295 
296         /** movement speed, y */
297         protected double dy;
298     }
299 
300 
301     /**
302      * Used for changing the size of the layout in response to a component's size.
303      */
304     public class SpringDimensionChecker extends ComponentAdapter {
305         @Override
306         public void componentResized(ComponentEvent e) {
307             setSize(e.getComponent().getSize());
308         }
309     }
310 
311     /**
312      * @return true
313      */
314     public boolean isIncremental() {
315         return true;
316     }
317 
318     /**
319      * @return false
320      */
321     public boolean done() {
322         return false;
323     }
324 
325     /**
326      * No effect.
327      */
328 	public void reset() {
329 	}
330 }