View Javadoc

1   package de.fzi.wim.guibase.graphview.layout;
2   
3   import java.util.Map;
4   import java.util.HashMap;
5   import java.util.Collection;
6   import java.util.Iterator;
7   
8   import de.fzi.wim.guibase.graphview.graph.*;
9   
10  /***
11   * The layout strategy that models edges as springs. This algorithm is a refactoring of the graph layout algorithm originally implemented
12   * in the <a href="http://www.touchgraph.com/">TouchGraph</a> library.
13   */
14  public class SpringLayoutStrategy implements LayoutStrategy {
15      /*** Specifies how long a node remains in a special state after it is inserted into the graph. */
16      protected static final long TIME_NODE_REMAINS_CHANGED=200;
17  
18      /*** The graph being manipulated by this strategy. */
19      protected Graph m_graph;
20      /*** The the graph manager. */
21      protected GraphManager m_graphManager;
22      /*** The damper value. A low damper value causes the graph to move slowly. A value of 1 means no damping. */
23      protected double m_damper=0.0;
24      /*** The maximum motion value after the last move. Used to see when the graph is stabilizinh. */
25      protected double m_maxMotion=0;
26      /*** The motion ration after the last move. */
27      protected double m_motionRatio=0;
28      /*** Then dampoing is true, the damper value decreases. */
29      protected boolean m_damping=true;
30      /*** The rigidity has the same effect as the damper, except that it is a constant. */
31      protected double m_rigidity=1;
32  
33      /***
34       * Creates an instance of this class.
35       *
36       * @param graph                         the graph that is active
37       */
38      public SpringLayoutStrategy(Graph graph) {
39          m_graph=graph;
40          m_graphManager=new GraphManager();
41      }
42      /***
43       * Returns the graph of the strategy.
44       *
45       * @return                              the graph of the strategy
46       */
47      public Graph getGraph() {
48          return m_graph;
49      }
50      /***
51       * Notifies the strategy that elements were inserted into the graph.
52       *
53       * @param nodes                         the inserted nodes (may be <code>null</code>)
54       * @param edges                         the inserted edges (may be <code>null</code>)
55       */
56      public void elementsAdded(Collection nodes,Collection edges) {
57          m_graphManager.elementsAdded(nodes,edges);
58      }
59      /***
60       * Notifies the strategy that elements were removed from the graph.
61       *
62       * @param nodes                         the removed nodes (may be <code>null</code>)
63       * @param edges                         the removed edges (may be <code>null</code>)
64       */
65      public void elementsRemoved(Collection nodes,Collection edges) {
66          m_graphManager.elementsRemoved(nodes,edges);
67      }
68      /***
69       * Called when the graph is completely changed.
70       */
71      public void graphContentsChanged() {
72          m_graphManager.graphContentsChanged();
73      }
74      /***
75       * Notifies the strategy that the graph has changed due to an event that is not caused by
76       * the strategy itself.
77       */
78      public void notifyGraphLayoutUpdated() {
79          m_damping=true;
80          m_damper=1.0;
81      }
82      /***
83       * Executes one step in the layout.
84       */
85      public void executeGraphLayoutStep() {
86          for (int i=0;i<10;i++) {
87              relaxEdges();
88              adjustNodePairs();
89              moveNodes();
90              damp();
91          }
92      }
93      /***
94       * Returns <code>true</code> if more steps in the layout should be executed.
95       *
96       * @return                              <code>true</code> if more steps should be executed
97       */
98      public boolean shouldExecuteStep() {
99          return !(m_damper<0.1 && m_damping && m_maxMotion<0.001);
100     }
101     /***
102      * Relaxes the edges.
103      */
104     protected void relaxEdges() {
105         Iterator edges=m_graph.getEdges().iterator();
106         while (edges.hasNext()) {
107             Edge edge=(Edge)edges.next();
108             Node from=edge.getFrom();
109             Node to=edge.getTo();
110             double deltaX=to.getX()-from.getX();
111             double deltaY=to.getY()-from.getY();
112             double currentLength=Math.sqrt(deltaX*deltaX+deltaY*deltaY);
113             double factor=m_rigidity*currentLength/(edge.getLength()*100.0);
114             double dx=deltaX*factor;
115             double dy=deltaY*factor;
116             // Edges pull directly in proportion to the distance between the nodes. This is good,
117             // because we want the edges to be stretchy.  The edges are ideal rubberbands.
118             // They don't become springs when they are too short.  That only causes the graph to oscillate.
119             applyDelta(from,m_graphManager.getNodeMovement(from),dx,dy);
120             applyDelta(to,m_graphManager.getNodeMovement(to),-dx,-dy);
121         }
122     }
123     /***
124      * Adjusts the pairs of nodes.
125      */
126     private void adjustNodePairs() {
127         SubGraph currentSubGraph=m_graphManager.getFirstSubGraph();
128         while (currentSubGraph!=null) {
129             NodeMovement node1Movement=currentSubGraph.getFirstNodeMovement();
130             while (node1Movement!=null) {
131                 Node node1=node1Movement.m_node;
132                 NodeMovement node2Movement=currentSubGraph.getFirstNodeMovement();
133                 while (node2Movement!=null) {
134                     Node node2=node2Movement.m_node;
135                     if (node1!=node2) {
136                         double dx=0;
137                         double dy=0;
138                         double deltaX=node1.getX()-node2.getX();
139                         double deltaY=node1.getY()-node2.getY();
140                         double currentLengthSquared=deltaX*deltaX+deltaY*deltaY; //so it's length squared
141                         if (Math.abs(currentLengthSquared)<0.1) {
142                             dx=Math.random(); //If two nodes are right on top of each other, randomly separate
143                             dy=Math.random();
144                         }
145                         else if (currentLengthSquared<600*600) {    // 600, because we don't want deleted nodes to fly too far away
146                             dx=deltaX/currentLengthSquared;         // If it was sqrt(len) then a single node surrounded by many others will
147                             dy=deltaY/currentLengthSquared;         // always look like a circle.  This might look good at first, but I think
148                                                                     // it makes large graphs look ugly+it contributes to oscillation.  A
149                                                                     // linear function does not fall off fast enough, so you get rough edges
150                                                                     // in the 'force field'
151                         }
152                         double factor=100.0*node1.getRepulsion()*node2.getRepulsion()*m_rigidity;
153                         dx*=factor;
154                         dy*=factor;
155                         applyDelta(node1,node1Movement,dx,dy);
156                         applyDelta(node2,node2Movement,-dx,-dy);
157                     }
158                     node2Movement=(NodeMovement)node2Movement.m_next;
159                 }
160                 node1Movement=(NodeMovement)node1Movement.m_next;
161             }
162             currentSubGraph=(SubGraph)currentSubGraph.m_next;
163         }
164     }
165     /***
166      * Moves nodes for the computed delta.
167      */
168     protected void moveNodes() {
169         double lastMaxMotion=m_maxMotion;
170         m_maxMotion=0;
171         Iterator iterator=m_graph.getNodes().iterator();
172         while (iterator.hasNext()) {
173             Node node=(Node)iterator.next();
174             NodeMovement movement=m_graphManager.getNodeMovement(node);
175             double dx=movement.m_dx;
176             double dy=movement.m_dy;
177             dx*=m_damper;   // The damper slows things down.  It cuts down jiggling at the last moment, and optimizes
178             dy*=m_damper;   // layout.  As an experiment, get rid of the damper in these lines, and make a
179                             // long straight line of nodes.  It wiggles too much and doesn't straighten out.
180             // Slow down, but don't stop.  Nodes in motion store momentum.  This helps when the force
181             // on a node is very low, but you still want to get optimal layout.
182             movement.m_dx=dx/2;
183             movement.m_dy=dy/2;
184             if (!node.isFixed()) {
185                 double distanceMoved=Math.sqrt(dx*dx+dy*dy);
186                 // Don't move faster then 30 units at a time. Important in order toprevent severed nodes from flying away.
187                 double x=node.getX()+Math.max(-30,Math.min(30,dx));
188                 double y=node.getY()+Math.max(-30,Math.min(30,dy));
189                 node.setLocation(x,y);
190                 m_maxMotion=Math.max(distanceMoved,m_maxMotion);
191             }
192             if (movement.m_justChanged) {
193                 if (System.currentTimeMillis()>movement.m_timeWhenNodeBecomesNormal)
194                     movement.m_justChanged=false;
195             }
196         }
197         if (m_maxMotion>0)
198             m_motionRatio=lastMaxMotion/m_maxMotion-1; //subtract 1 to make a positive value mean that things are moving faster
199         else
200             m_motionRatio=0;
201     }
202     /***
203      * Applies a delta to a node.
204      *
205      * @param node                      the node to which the delta is added
206      * @param nodeMovement              the node movement
207      * @param dx                        delta in the X direction
208      * @param dy                        delta in the Y direction
209      */
210     protected void applyDelta(Node node,NodeMovement nodeMovement,double dx,double dy) {
211         if (nodeMovement.m_justChanged) {
212             nodeMovement.m_dx+=dx/10;
213             nodeMovement.m_dy+=dy/10;
214         }
215         else {
216             nodeMovement.m_dx+=dx;
217             nodeMovement.m_dy+=dy;
218         }
219     }
220     /***
221      * Turns damping off.
222      */
223     public void stopDamper() {
224         m_damping=false;
225         m_damper=1.0;
226     }
227     /***
228      * Stabilizes the graph gently, by setting the damper to a low value.
229      */
230     public void stopMotion() {
231         m_damping=true;
232         if (m_damper>0.3)
233             m_damper=0.3;
234         else
235             m_damper=0;
236     }
237     /***
238      * Applies the damping.
239      */
240     public void damp() {
241         if (m_damping) {
242             // This is important.  Only damp when the graph starts to move faster
243             // When there is noise, you damp roughly half the time. (Which is a lot)
244             // If things are slowing down, then you can let them do so on their own,
245             // without damping.
246             if (m_motionRatio<=0.001) {
247                 //If max motion<0.2, damp away
248                 //If by the time the damper has ticked down to 0.9, maxMotion is still>1, damp away
249                 //We never want the damper to be negative though
250                 if ((m_maxMotion<0.2 || (m_maxMotion>1 && m_damper<0.9)) && m_damper > 0.01)
251                     m_damper-=0.01;
252                 //If we've slowed down significanly, damp more aggresively (then the line two below)
253                 else if (m_maxMotion<0.4 && m_damper > 0.003)
254                     m_damper-=0.003;
255                 //If max motion is pretty high, and we just started damping, then only damp slightly
256                 else if(m_damper>0.0001)
257                     m_damper -=0.0001;
258             }
259         }
260         if(m_maxMotion<0.001 && m_damping)
261             m_damper=0;
262     }
263 
264     /***
265      * The abstract list node.
266      */
267     protected static class ListNode {
268         public ListNode m_next;
269         public ListNode m_previous;
270         public ListOfNodes m_list;
271     }
272 
273     /***
274      * The list.
275      */
276     protected static class ListOfNodes {
277         public ListNode m_head;
278         public ListNode m_tail;
279         public int m_size;
280 
281         public void add(ListNode node) {
282             if (m_head==null) {
283                 m_head=node;
284                 m_tail=node;
285                 node.m_previous=node.m_next=null;
286             }
287             else {
288                 m_tail.m_next=node;
289                 node.m_previous=m_tail;
290                 node.m_next=null;
291                 m_tail=node;
292             }
293             node.m_list=this;
294         }
295         public void remove(ListNode node) {
296             if (m_head==node)
297                 m_head=node.m_next;
298             else
299                 node.m_previous.m_next=node.m_next;
300             if (m_tail==node)
301                 m_tail=node.m_previous;
302             else
303                 node.m_next.m_previous=node.m_previous;
304             node.m_next=node.m_previous=null;
305             node.m_list=null;
306         }
307         public void clear() {
308             m_head=m_tail=null;
309             m_size=0;
310         }
311         public void mergeWith(ListOfNodes other) {
312             if (m_tail==null) {
313                 m_head=other.m_head;
314                 m_tail=other.m_head;
315             }
316             else if (other.m_head!=null) {
317                 m_tail.m_next=other.m_head;
318                 other.m_head.m_previous=m_tail;
319                 m_tail=other.m_tail;
320             }
321             ListNode pointer=other.m_head;
322             while (pointer!=null) {
323                 pointer.m_list=this;
324                 pointer=pointer.m_next;
325             }
326             m_size+=other.m_size;
327             other.m_head=other.m_tail=null;
328             other.m_size=0;
329         }
330     }
331 
332     /***
333      * Contains information about the node movement.
334      */
335     protected static class NodeMovement extends ListNode {
336         public Node m_node;
337         public double m_dx=0.0;
338         public double m_dy=0.0;
339         public boolean m_justChanged=false;
340         public long m_timeWhenNodeBecomesNormal;
341 
342         public NodeMovement(Node node) {
343             m_node=node;
344         }
345         public SubGraph getSubGraph() {
346             return ((SubGraphList)m_list).m_subGraph;
347         }
348     }
349 
350     /***
351      * Contains information about the subgraphs in the overall graph.
352      */
353     protected class GraphManager extends ListOfNodes {
354         /*** The node movement objects indexed by the node. */
355         protected Map m_nodeMovement;
356 
357         public GraphManager() {
358             m_nodeMovement=new HashMap();
359         }
360         public SubGraph getFirstSubGraph() {
361             return (SubGraph)m_head;
362         }
363         public NodeMovement getNodeMovement(Node node) {
364             return (NodeMovement)m_nodeMovement.get(node);
365         }
366         public void nodeChanged(Node node) {
367             NodeMovement nodeMovement=getNodeMovement(node);
368             if (nodeMovement!=null) {
369                 nodeMovement.m_justChanged=true;
370                 nodeMovement.m_timeWhenNodeBecomesNormal=System.currentTimeMillis()+TIME_NODE_REMAINS_CHANGED;
371             }
372         }
373         public void addNode(Node node) {
374             if (!m_nodeMovement.containsKey(node)) {
375                 NodeMovement nodeMovement=new NodeMovement(node);
376                 m_nodeMovement.put(node,nodeMovement);
377                 add(new SubGraph(nodeMovement));
378             }
379             nodeChanged(node);
380         }
381         public void addEdge(Edge edge) {
382             NodeMovement nodeMovement1=getNodeMovement(edge.getFrom());
383             NodeMovement nodeMovement2=getNodeMovement(edge.getTo());
384             if (nodeMovement1.getSubGraph()!=nodeMovement2.getSubGraph())
385                 mergeSubGraphs(nodeMovement1.getSubGraph(),nodeMovement2.getSubGraph());
386         }
387         public void graphContentsChanged() {
388             m_nodeMovement.clear();
389             clear();
390             Iterator nodes=m_graph.getNodes().iterator();
391             while (nodes.hasNext()) {
392                 Node node=(Node)nodes.next();
393                 addNode(node);
394             }
395             Iterator edges=m_graph.getEdges().iterator();
396             while (edges.hasNext()) {
397                 Edge edge=(Edge)edges.next();
398                 addEdge(edge);
399             }
400         }
401         public void elementsAdded(Collection nodes,Collection edges) {
402             if (nodes!=null) {
403                 Iterator iterator=nodes.iterator();
404                 while (iterator.hasNext()) {
405                     Node node=(Node)iterator.next();
406                     addNode(node);
407                 }
408             }
409             if (edges!=null) {
410                 Iterator iterator=edges.iterator();
411                 while (iterator.hasNext()) {
412                     Edge edge=(Edge)iterator.next();
413                     addEdge(edge);
414                 }
415             }
416         }
417         public void elementsRemoved(Collection nodes,Collection edges) {
418             if (nodes!=null) {
419                 Iterator iterator=nodes.iterator();
420                 while (iterator.hasNext()) {
421                     Node node=(Node)iterator.next();
422                     m_nodeMovement.remove(node);
423                 }
424             }
425             if (nodes!=null || edges!=null) {
426                 m_head=m_tail=null;
427                 Iterator iterator=m_nodeMovement.values().iterator();
428                 while (iterator.hasNext()) {
429                     NodeMovement nodeMovement=(NodeMovement)iterator.next();
430                     add(new SubGraph(nodeMovement));
431                 }
432                 if (edges!=null) {
433                     iterator=edges.iterator();
434                     while (iterator.hasNext()) {
435                         Edge edge=(Edge)iterator.next();
436                         nodeChanged(edge.getFrom());
437                         nodeChanged(edge.getTo());
438                     }
439                 }
440                 iterator=m_graph.getEdges().iterator();
441                 while (iterator.hasNext()) {
442                     Edge edge=(Edge)iterator.next();
443                     addNode(edge.getFrom());
444                     addNode(edge.getTo());
445                     addEdge(edge);
446                 }
447             }
448         }
449         public void mergeSubGraphs(SubGraph subGraph1,SubGraph subGraph2) {
450             if (subGraph1.getSize()<subGraph2.getSize()) {
451                 subGraph2.mergeWith(subGraph1);
452                 remove(subGraph1);
453             }
454             else {
455                 subGraph1.mergeWith(subGraph2);
456                 remove(subGraph2);
457             }
458         }
459     }
460 
461     /***
462      * Information about a subgraph.
463      */
464     protected static class SubGraph extends ListNode {
465         protected SubGraphList m_nodes;
466 
467         public SubGraph(NodeMovement nodeMovement) {
468             m_nodes=new SubGraphList(this);
469             m_nodes.add(nodeMovement);
470             nodeMovement.m_list=m_nodes;
471         }
472         public NodeMovement getFirstNodeMovement() {
473             return (NodeMovement)m_nodes.m_head;
474         }
475         public void mergeWith(SubGraph subGraph) {
476             m_nodes.mergeWith(subGraph.m_nodes);
477         }
478         public void removeNodeMovement(NodeMovement nodeMovement) {
479             m_nodes.remove(nodeMovement);
480         }
481         public int getSize() {
482             return m_nodes.m_size;
483         }
484     }
485 
486     protected static class SubGraphList extends ListOfNodes {
487         public SubGraph m_subGraph;
488 
489         public SubGraphList(SubGraph subGraph) {
490             m_subGraph=subGraph;
491         }
492     }
493 }