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