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.geom.Point2D;
12  import java.awt.geom.Rectangle2D;
13  import java.util.ConcurrentModificationException;
14  
15  import com.google.common.base.Preconditions;
16  import com.google.common.cache.CacheBuilder;
17  import com.google.common.cache.CacheLoader;
18  import com.google.common.cache.LoadingCache;
19  
20  import edu.uci.ics.jung.algorithms.layout.util.RandomLocationTransformer;
21  import edu.uci.ics.jung.algorithms.util.IterativeContext;
22  import edu.uci.ics.jung.graph.Graph;
23  import edu.uci.ics.jung.graph.util.Pair;
24  
25  /**
26   * Implements the Fruchterman-Reingold force-directed algorithm for node layout.
27   * This is an experimental attempt at optimizing {@code FRLayout}; if it is successful
28   * it will be folded back into {@code FRLayout} (and this class will disappear).
29   * 
30   * <p>Behavior is determined by the following settable parameters:
31   * <ul>
32   * <li>attraction multiplier: how much edges try to keep their vertices together
33   * <li>repulsion multiplier: how much vertices try to push each other apart
34   * <li>maximum iterations: how many iterations this algorithm will use before stopping
35   * </ul>
36   * Each of the first two defaults to 0.75; the maximum number of iterations defaults to 700.
37  
38   * 
39   * @see "Fruchterman and Reingold, 'Graph Drawing by Force-directed Placement'"
40   * @see "http://i11www.ilkd.uni-karlsruhe.de/teaching/SS_04/visualisierung/papers/fruchterman91graph.pdf"
41   * 
42   * @author Tom Nelson
43   * @author Scott White, Yan-Biao Boey, Danyel Fisher
44   */
45  public class FRLayout2<V, E> extends AbstractLayout<V, E> implements IterativeContext {
46  
47      private double forceConstant;
48  
49      private double temperature;
50  
51      private int currentIteration;
52  
53      private int maxIterations = 700;
54      
55      protected LoadingCache<V, Point2D> frVertexData =
56      	CacheBuilder.newBuilder().build(new CacheLoader<V, Point2D>() {
57  	    	public Point2D load(V vertex) {
58  	    		return new Point2D.Double();
59  	    	}
60      });
61  
62      private double attraction_multiplier = 0.75;
63      
64      private double attraction_constant;
65      
66      private double repulsion_multiplier = 0.75;
67      
68      private double repulsion_constant;
69      
70      private double max_dimension;
71      
72      private Rectangle2D innerBounds = new Rectangle2D.Double();
73      
74      private boolean checked = false;
75      
76      public FRLayout2(Graph<V, E> g) {
77          super(g);
78      }
79      
80      public FRLayout2(Graph<V, E> g, Dimension d) {
81          super(g, new RandomLocationTransformer<V>(d), d);
82          max_dimension = Math.max(d.height, d.width);
83          initialize();
84      }
85      
86  	@Override
87  	public void setSize(Dimension size) {
88  		if(initialized == false) 
89  			setInitializer(new RandomLocationTransformer<V>(size));
90  		super.setSize(size);
91  		double t = size.width/50.0;
92  		innerBounds.setFrameFromDiagonal(t,t,size.width-t,size.height-t);
93          max_dimension = Math.max(size.height, size.width);
94  	}
95  
96  	public void setAttractionMultiplier(double attraction) {
97          this.attraction_multiplier = attraction;
98      }
99      
100     public void setRepulsionMultiplier(double repulsion) {
101         this.repulsion_multiplier = repulsion;
102     }
103     
104 	public void reset() {
105 		doInit();
106 	}
107     
108     public void initialize() {
109     	doInit();
110     }
111 
112     private void doInit() {
113     	Graph<V,E> graph = getGraph();
114     	Dimension d = getSize();
115     	if(graph != null && d != null) {
116     		currentIteration = 0;
117     		temperature = d.getWidth() / 10;
118 
119     		forceConstant = 
120     			Math
121     			.sqrt(d.getHeight()
122     					* d.getWidth()
123     					/ graph.getVertexCount());
124 
125     		attraction_constant = attraction_multiplier * forceConstant;
126     		repulsion_constant = repulsion_multiplier * forceConstant;
127     	}
128     }
129 
130     private double EPSILON = 0.000001D;
131 
132     /**
133      * Moves the iteration forward one notch, calculation attraction and
134      * repulsion between vertices and edges and cooling the temperature.
135      */
136     public synchronized void step() {
137         currentIteration++;
138 
139         /**
140          * Calculate repulsion
141          */
142         while(true) {
143             
144             try {
145                 for(V v1 : getGraph().getVertices()) {
146                     calcRepulsion(v1);
147                 }
148                 break;
149             } catch(ConcurrentModificationException cme) {}
150         }
151 
152         /**
153          * Calculate attraction
154          */
155         while(true) {
156             try {
157                 for(E e : getGraph().getEdges()) {
158                     calcAttraction(e);
159                 }
160                 break;
161             } catch(ConcurrentModificationException cme) {}
162         }
163 
164 
165         while(true) {
166             try {    
167                 for(V v : getGraph().getVertices()) {
168                     if (isLocked(v)) continue;
169                     calcPositions(v);
170                 }
171                 break;
172             } catch(ConcurrentModificationException cme) {}
173         }
174         cool();
175     }
176 
177     protected synchronized void calcPositions(V v) {
178         Point2D fvd = this.frVertexData.getUnchecked(v);
179         if(fvd == null) return;
180         Point2D xyd = apply(v);
181         double deltaLength = Math.max(EPSILON, 
182         		Math.sqrt(fvd.getX()*fvd.getX()+fvd.getY()*fvd.getY()));
183 
184         double newXDisp = fvd.getX() / deltaLength
185                 * Math.min(deltaLength, temperature);
186 
187         Preconditions.checkState(!Double.isNaN(newXDisp),
188             "Unexpected mathematical result in FRLayout:calcPositions [xdisp]");
189 
190         double newYDisp = fvd.getY() / deltaLength
191                 * Math.min(deltaLength, temperature);
192         double newX = xyd.getX()+Math.max(-5, Math.min(5,newXDisp));
193         double newY = xyd.getY()+Math.max(-5, Math.min(5,newYDisp));
194         
195         newX = Math.max(innerBounds.getMinX(), Math.min(newX, innerBounds.getMaxX()));
196         newY = Math.max(innerBounds.getMinY(), Math.min(newY, innerBounds.getMaxY()));
197         
198         xyd.setLocation(newX, newY);
199 
200     }
201 
202     protected void calcAttraction(E e) {
203     	Pair<V> endpoints = getGraph().getEndpoints(e);
204         V v1 = endpoints.getFirst();
205         V v2 = endpoints.getSecond();
206         boolean v1_locked = isLocked(v1);
207         boolean v2_locked = isLocked(v2);
208         
209         if(v1_locked && v2_locked) {
210         	// both locked, do nothing
211         	return;
212         }
213         Point2D p1 = apply(v1);
214         Point2D p2 = apply(v2);
215         if(p1 == null || p2 == null) return;
216         double xDelta = p1.getX() - p2.getX();
217         double yDelta = p1.getY() - p2.getY();
218 
219         double deltaLength = Math.max(EPSILON, p1.distance(p2));
220 
221         double force = deltaLength  / attraction_constant;
222 
223         Preconditions.checkState(!Double.isNaN(force),
224             "Unexpected mathematical result in FRLayout:calcPositions [force]");
225 
226         double dx = xDelta * force;
227         double dy = yDelta * force;
228         Point2D fvd1 = frVertexData.getUnchecked(v1);
229         Point2D fvd2 = frVertexData.getUnchecked(v2);
230         if(v2_locked) {
231         	// double the offset for v1, as v2 will not be moving in
232         	// the opposite direction
233         	fvd1.setLocation(fvd1.getX()-2*dx, fvd1.getY()-2*dy);
234         } else {
235         fvd1.setLocation(fvd1.getX()-dx, fvd1.getY()-dy);
236         }
237         if(v1_locked) {
238         	// double the offset for v2, as v1 will not be moving in
239         	// the opposite direction
240         	fvd2.setLocation(fvd2.getX()+2*dx, fvd2.getY()+2*dy);
241         } else {
242         fvd2.setLocation(fvd2.getX()+dx, fvd2.getY()+dy);
243     }
244     }
245 
246     protected void calcRepulsion(V v1) {
247         Point2D fvd1 = frVertexData.getUnchecked(v1);
248         if(fvd1 == null) return;
249         fvd1.setLocation(0, 0);
250         boolean v1_locked = isLocked(v1);
251 
252         try {
253             for(V v2 : getGraph().getVertices()) {
254 
255                 boolean v2_locked = isLocked(v2);
256             	if (v1_locked && v2_locked) continue;
257                 if (v1 != v2) {
258                     Point2D p1 = apply(v1);
259                     Point2D p2 = apply(v2);
260                     if(p1 == null || p2 == null) continue;
261                     double xDelta = p1.getX() - p2.getX();
262                     double yDelta = p1.getY() - p2.getY();
263                     
264                     double deltaLength = Math.max(EPSILON, p1.distanceSq(p2));
265                     
266                     double force = (repulsion_constant * repulsion_constant);// / deltaLength;
267                     
268                     double forceOverDeltaLength = force / deltaLength;
269                     
270                     Preconditions.checkState(!Double.isNaN(force),
271                         "Unexpected mathematical result in FRLayout:calcPositions [repulsion]");
272                     
273                     if(v2_locked) {
274                     	// double the offset for v1, as v2 will not be moving in
275                     	// the opposite direction
276                     	fvd1.setLocation(fvd1.getX()+2 * xDelta * forceOverDeltaLength,
277                     		fvd1.getY()+ 2 * yDelta * forceOverDeltaLength);
278                     } else {
279                     	fvd1.setLocation(fvd1.getX()+xDelta * forceOverDeltaLength,
280                         		fvd1.getY()+yDelta * forceOverDeltaLength);
281                 }
282             }
283             }
284         } catch(ConcurrentModificationException cme) {
285             calcRepulsion(v1);
286         }
287     }
288 
289     private void cool() {
290         temperature *= (1.0 - currentIteration / (double) maxIterations);
291     }
292 
293     public void setMaxIterations(int maxIterations) {
294         this.maxIterations = maxIterations;
295     }
296 
297     /**
298      * @return true
299      */
300     public boolean isIncremental() {
301         return true;
302     }
303 
304     /**
305      * @return true once the current iteration has passed the maximum count.
306      */
307     public boolean done() {
308         if (currentIteration > maxIterations || temperature < 1.0/max_dimension) { 
309             if (!checked)
310             {
311                 checked = true;
312             }
313             return true; 
314         } 
315         return false;
316     }
317 }