1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.view.anim;
15  
16  import demo.view.DemoBase;
17  import demo.view.DemoDefaults;
18  
19  import y.anim.AnimationEvent;
20  import y.anim.AnimationFactory;
21  import y.anim.AnimationListener;
22  import y.anim.AnimationObject;
23  import y.anim.AnimationPlayer;
24  import y.anim.CompositeAnimationObject;
25  import y.base.DataMap;
26  import y.base.Edge;
27  import y.base.EdgeCursor;
28  import y.base.Node;
29  import y.base.NodeCursor;
30  import y.io.GraphMLIOHandler;
31  import y.layout.BufferedLayouter;
32  import y.layout.GraphLayout;
33  import y.layout.hierarchic.IncrementalHierarchicLayouter;
34  import y.layout.hierarchic.incremental.IncrementalHintsFactory;
35  import y.util.Comparators;
36  import y.util.Maps;
37  import y.view.EdgeRealizer;
38  import y.view.EditMode;
39  import y.view.Graph2D;
40  import y.view.Graph2DViewRepaintManager;
41  import y.view.LayoutMorpher;
42  import y.view.NodeRealizer;
43  import y.view.ViewAnimationFactory;
44  
45  import javax.swing.JMenu;
46  import javax.swing.JMenuBar;
47  import javax.swing.JToolBar;
48  import java.awt.Dimension;
49  import java.awt.EventQueue;
50  import java.awt.event.ComponentAdapter;
51  import java.awt.event.ComponentEvent;
52  import java.io.IOException;
53  import java.net.URL;
54  import java.util.ArrayList;
55  import java.util.Collection;
56  import java.util.Comparator;
57  import java.util.HashSet;
58  import java.util.Iterator;
59  import java.util.Locale;
60  import java.util.Random;
61  import java.util.Set;
62  import java.util.WeakHashMap;
63  
64  /**
65   * Demonstrates how to combine animation effects for structural graph changes
66   * with animated graph layout changes.
67   * The demonstrated effects will start automatically and loop until the user
68   * ends the demo.
69   *
70   * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/animation.html">Section Animations for Graph Elements</a> in the yFiles for Java Developer's Guide
71   */
72  public class AnimatedStructuralChangesDemo extends DemoBase {
73    /**
74     * Preferred duration for all animation effects.
75     */
76    private static final int PREFERRED_DURATION = 500;
77  
78    /**
79     * Maximum edge count when randomizing the graph structure.
80     */
81    private static final int MAX_EDGE_COUNT = 75;
82  
83    /**
84     * Maximum node count when randomizing the graph structure.
85     */
86    private static final int MAX_NODE_COUNT = 50;
87  
88  
89    private final Random random;
90    private final ViewAnimationFactory factory;
91    private final Graph2D graph;
92  
93    private boolean disposed;
94  
95    public AnimatedStructuralChangesDemo() {
96      random = new Random(42);
97      factory = new ViewAnimationFactory(new Graph2DViewRepaintManager(view));
98      graph = view.getGraph2D();
99      view.setPreferredSize(new Dimension(800, 600));
100     view.addComponentListener(new ComponentAdapter() {
101       public void componentResized(final ComponentEvent e) {
102         if (e.getSource() == view) {
103           view.removeComponentListener(this);
104 
105           // finally view has been assigned a valid size which allows
106           // fitContent to work correctly
107           view.fitContent();
108 
109           showInitialGraph();
110         }
111       }
112     });
113 
114     configureRealizers();
115     prepareInitialGraph();
116   }
117 
118   private void configureRealizers() {
119     // painting shadows is expensive and therefore not well suited for animations
120     DemoDefaults.registerDefaultNodeConfiguration(false);
121     DemoDefaults.configureDefaultRealizers(view);
122   }
123 
124   /**
125    * Overridden to disable user interaction.
126    */
127   protected EditMode createEditMode() {
128     return null;
129   }
130 
131   /**
132    * Overridden to disable user interaction.
133    */
134   protected JMenuBar createMenuBar() {
135     final JMenu file = new JMenu("File");
136     file.add(new ExitAction());
137 
138     final JMenuBar jmb = new JMenuBar();
139     jmb.add(file);
140     return jmb;
141   }
142 
143   /**
144    * Overridden to disable user interaction.
145    */
146   protected JToolBar createToolBar() {
147     return null;
148   }
149 
150   private void prepareInitialGraph() {
151     // try to load an initial graph
152     final URL resource = getClass().getResource("resource/hierarchic.graphml");
153     
154     if (resource != null) {
155       final GraphMLIOHandler ioh = new GraphMLIOHandler();
156       try {
157         ioh.read(graph, resource);
158       } catch (IOException ioe) {
159         System.err.println(ioe.getMessage());
160         graph.clear();
161       }
162     } else {
163       System.err.println("Could not load \"resource/hierarchic.graphml\".");
164       graph.clear();
165     }
166 
167     DemoDefaults.applyRealizerDefaults(graph);
168     
169     if (graph.nodeCount() > 0) {
170       graph.setDefaultNodeRealizer(graph.getRealizer(graph.firstNode()).createCopy());
171     }
172     // by default newly created nodes are invisible
173     // animation effects will make new nodes visible later
174     graph.getDefaultNodeRealizer().setVisible(false);
175 
176     if (graph.edgeCount() > 0) {
177       graph.setDefaultEdgeRealizer(graph.getRealizer(graph.firstEdge()).createCopy());
178     }
179     // by default newly created edges are invisible
180     // animation effects will make new edges visible later
181     graph.getDefaultEdgeRealizer().setVisible(false);
182 
183     // set all graph elements to invisible initially
184     // the first create animation will make these elements visible later on
185     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
186       graph.getRealizer(nc.node()).setVisible(false);
187     }
188 
189     for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
190       graph.getRealizer(ec.edge()).setVisible(false);
191     }
192   }
193 
194   private void showInitialGraph() {
195     final ArrayList newNodes = new ArrayList(graph.nodeCount());
196     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
197       newNodes.add(nc.node());
198     }
199     final ArrayList newEdges = new ArrayList(graph.edgeCount());
200     for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
201       newEdges.add(ec.edge());
202     }
203 
204     final AnimationPlayer player = new AnimationPlayer(false);
205 
206     // register the ViewAnimationFactory's repaint manager as animation
207     // listener to prevent repaints for the complete Graph2DView and
208     // thereby improving animation performance if possible
209     player.addAnimationListener(factory.getRepaintManager());
210 
211     player.addAnimationListener(new Command() {
212       void execute() {
213         // start the main execution loop
214         AnimatedStructuralChangesDemo.this.execute();
215       }
216     });
217 
218     // start the animation and idle for some time at the end
219     player.animate(AnimationFactory.createSequence(
220         createCreateAnimation(newNodes, newEdges),
221         AnimationFactory.createPause(PREFERRED_DURATION)));
222   }
223 
224   public void dispose() {
225     disposed = true;
226   }
227 
228   /**
229    * Randomizes the graph structure, calculates a new graph layout, and finally
230    * animates the structural and layout changes.
231    */
232   private void execute() {
233     if (disposed) {
234       return;
235     }
236 
237     // determine nodes and edges that should be deleted
238     final HashSet nodesToBeDeleted = new HashSet();
239     final HashSet edgesToBeDeleted = new HashSet();
240     markNodesForDeletion(nodesToBeDeleted, edgesToBeDeleted);
241     markEdgesForDeletion(edgesToBeDeleted);
242 
243     // temporarily remove the elements that will be deleted later on
244     // these elements are removed for two reasons:
245     // 1. to prevent new edges being created for nodes that are marked for
246     //    deletion
247     // 2. to prevent these elements from being considered when calculating
248     //    a new graph layout
249     for (Iterator it = edgesToBeDeleted.iterator(); it.hasNext();) {
250       graph.hide((Edge) it.next());
251     }
252     for (Iterator it = nodesToBeDeleted.iterator(); it.hasNext();) {
253       graph.hide((Node) it.next());
254     }
255 
256     // create some new nodes and edges
257     final HashSet newNodes = new HashSet();
258     createNodes(newNodes);
259     final HashSet newEdges = new HashSet();
260     createEdges(newNodes, newEdges);
261 
262     // calculate a new graph layout for the new graph structure
263     // i.e. all elements marked for deletion have been removed at this point
264     // and all new elements have been created already (so new elements will
265     // appear at the correct location later)
266     final GraphLayout gl = calcLayout(newNodes, newEdges);
267 
268     // now reinsert the elements marked for deletion, so the animation effects
269     // will work properly
270     // the actual deletion will be done by the animation effect, see also
271     // the documentation for ViewAnimationFactory's APPLY_EFFECT
272     for (Iterator it = nodesToBeDeleted.iterator(); it.hasNext();) {
273       graph.unhide((Node) it.next());
274     }
275     for (Iterator it = edgesToBeDeleted.iterator(); it.hasNext();) {
276       graph.unhide((Edge) it.next());
277     }
278 
279     // create a shared, non-blocking AnimationPlayer
280     // non-blocking, so a user can still interact with the SWING GUI
281     // (even if it is only to quit the demo)
282     final AnimationPlayer player = new AnimationPlayer(false);
283 
284     // now chain several animation effects
285     // this is done because animations such as LayoutMorpher and
286     // ViewAnimation.extract/ViewAnimation.retract are rather expensive to
287     // create (which could severly hamper the animation frame rate) and
288     // more important these animations use the state of their targets
289     // at *instantiation* time
290 
291     // triggers re-execution of this method at the end of the final animation
292     final Command loop = new Command() {
293       void execute() {
294         // cleanup
295         player.removeAnimationListener(this);
296         player.removeAnimationListener(factory.getRepaintManager());
297 
298         // loop
299         AnimatedStructuralChangesDemo.this.execute();
300       }
301     };
302 
303     // triggers creating new nodes by fade in and new edges by extract
304     final Command animateCreate = new Command() {
305       void execute() {
306         // cleanup
307         player.removeAnimationListener(this);
308         player.removeAnimationListener(view);
309 
310         // register looping for execution
311         player.addAnimationListener(loop);
312 
313         // register the ViewAnimationFactory's repaint manager as animation
314         // listener to prevent repaints for the complete Graph2DView and
315         // thereby improving animation performance if possible
316         player.addAnimationListener(factory.getRepaintManager());
317 
318         // start the animation and idle for some time at the end
319         player.animate(AnimationFactory.createSequence(
320             createCreateAnimation(newNodes, newEdges),
321             AnimationFactory.createPause(PREFERRED_DURATION)));
322       }
323     };
324 
325     // triggers applying the new graph layout in an animated fashion
326     final Command animateMorphing = new Command() {
327       void execute() {
328         // cleanup
329         player.removeAnimationListener(this);
330         player.removeAnimationListener(factory.getRepaintManager());
331 
332         // register the next animation effect for execution
333         player.addAnimationListener(animateCreate);
334 
335         // register the complete Graph2DView as animation listener because
336         // LayoutMorpher does not support repaint managers
337         player.addAnimationListener(view);
338 
339         // start the animation
340         player.animate(createMorphingAnimation(gl));
341       }
342     };
343 
344     // triggers deleting marked elements
345     final Command animateDelete = new Command() {
346       void execute() {
347         // register the next animation effect for execution
348         player.addAnimationListener(animateMorphing);
349 
350         // register the ViewAnimationFactory's repaint manager as animation
351         // listener to prevent repaints for the complete Graph2DView and
352         // thereby improving animation performance if possible
353         player.addAnimationListener(factory.getRepaintManager());
354 
355         // start the animation
356         player.animate(createDeleteAnimation(nodesToBeDeleted, edgesToBeDeleted));
357       }
358     };
359 
360     animateDelete.execute();
361   }
362 
363   /*
364   * #####################################################################
365   * methods for randomized structural changes
366   * #####################################################################
367   */
368 
369   /**
370    * Randomly determine edges to be deleted from the graph.
371    * @param edgesToBeDeleted   will store the edges to be deleted.
372    */
373   private void markEdgesForDeletion(
374       final Set edgesToBeDeleted
375   ) {
376     for (EdgeCursor ec = graph.edges();
377          ec.ok() && graph.edgeCount() - edgesToBeDeleted.size() > 4;
378          ec.next()) {
379       if (!edgesToBeDeleted.contains(ec.edge()) && random.nextDouble() < 0.05) {
380         edgesToBeDeleted.add(ec.edge());
381       }
382     }
383   }
384 
385   /**
386    * Randomly determines nodes to be deleted from the graph.
387    * @param nodesToBeDeleted   will store the nodes to be deleted.
388    * @param edgesToBeDeleted   will store all edges incident to nodes to be
389    * deleted. (When removing nodes from a graph, incident edges are
390    * automatically removed, too. However, by collecting these edges, they can
391    * be deleted in an automated fashion.)
392    */
393   private void markNodesForDeletion(
394       final Set nodesToBeDeleted,
395       final Set edgesToBeDeleted
396   ) {
397     for (NodeCursor nc = graph.nodes();
398          nc.ok() &&
399              graph.nodeCount() - nodesToBeDeleted.size() > 4 &&
400              graph.edgeCount() - edgesToBeDeleted.size() > 4;
401          nc.next()) {
402       if (random.nextDouble() < 0.05) {
403         nodesToBeDeleted.add(nc.node());
404         for (EdgeCursor ec = nc.node().edges(); ec.ok(); ec.next()) {
405           edgesToBeDeleted.add(ec.edge());
406         }
407       }
408     }
409   }
410 
411   /**
412    * Creates a random number of new nodes.
413    * @param newNodes   will store the newly created nodes.
414    */
415   private void createNodes(
416       final Set newNodes
417   ) {
418     if (graph.nodeCount() < MAX_NODE_COUNT + 1) {
419       for (int i = 0, n = random.nextInt(MAX_NODE_COUNT + 1 - graph.nodeCount()); i < n; ++i) {
420         final Node node = graph.createNode();
421         newNodes.add(node);
422       }
423     }
424   }
425 
426   /**
427    * Creates a random number of new edges between randomly chosen new nodes.
428    * New edges are created preferably between an old node and a new node.
429    * Nodes are considered to be <em>new</em>, iff <code>newNodes.contains</code>
430    * returns <code>true</code> and to be <em>old</em> otherwise.
431    * <p>
432    * Note, the implementation of this method relies on the fact that it is
433    * called right after {@link #createNodes(java.util.Set)}.
434    * @param newNodes   nodes marked as new.
435    * @param newEdges   will store the newly created edges.
436    */
437   private void createEdges(
438       final HashSet newNodes,
439       final HashSet newEdges
440   ) {
441     if (graph.edgeCount() < MAX_EDGE_COUNT + 1) {
442       final Node[] nodes = graph.getNodeArray();
443       final int newCount = newNodes.size();
444       final int oldCount = nodes.length - newCount;
445 
446       if (newCount > 1 && oldCount > 1) {
447         // sort old nodes from upper left to lower right
448         // this will result in new edges between old nodes being in hierarchic
449         // flow direction
450         Comparators.sort(nodes, 0, oldCount, new Comparator() {
451           public int compare(final Object n1, final Object n2) {
452             final double dy = graph.getCenterY((Node) n1) - graph.getCenterY((Node) n2);
453             if (dy < 0) {
454               return -1;
455             } else if (dy > 0) {
456               return 1;
457             } else {
458               final double dx = graph.getCenterX((Node) n1) - graph.getCenterX((Node) n2);
459               if (dx < 0) {
460                 return -1;
461               } else if (dx > 0) {
462                 return 1;
463               } else {
464                 return 0;
465               }
466             }
467           }
468         });
469 
470         for (int i = 0, n = random.nextInt(MAX_EDGE_COUNT + 1 - graph.edgeCount()); i < n; ++i) {
471           final double d = random.nextDouble();
472           final Edge edge;
473           if (d < 0.1) {
474             // create an edge between two old nodes
475             final int n1 = random.nextInt(oldCount);
476             final int n2 = n1 + random.nextInt(oldCount - n1);
477             edge = n1 != n2 ? graph.createEdge(nodes[n1], nodes[n2]) : null;
478           } else if (d < 0.5) {
479             // create an edge between an old and a new node
480             edge = graph.createEdge(nodes[random.nextInt(oldCount)], nodes[oldCount + random.nextInt(newCount)]);
481           } else if (d < 0.9) {
482             // create an edge between a new and an old node
483             edge = graph.createEdge(nodes[oldCount + random.nextInt(newCount)], nodes[random.nextInt(oldCount)]);
484           } else {
485             // create an edge between two new nodes
486             final int n1 = oldCount + random.nextInt(newCount);
487             final int n2 = oldCount + random.nextInt(newCount);
488             edge = n1 != n2 ? graph.createEdge(nodes[n1], nodes[n2]) : null;
489           }
490           if (edge != null) {
491             newEdges.add(edge);
492           }
493         }
494       } else if (oldCount > 1) {
495         // create edges between old nodes only (there are no new nodes)
496         for (int i = 0, n = random.nextInt(MAX_EDGE_COUNT + 1 - graph.edgeCount()); i < n; ++i) {
497           final int n1 = random.nextInt(oldCount);
498           final int n2 = n1 + random.nextInt(oldCount - n1);
499           if (n1 != n2) {
500             newEdges.add(graph.createEdge(nodes[n1], nodes[n2]));
501           }
502         }
503       } else if (newCount > 1) {
504         // create edges between new nodes only (there are no old nodes)
505         for (int i = 0, n = random.nextInt(MAX_EDGE_COUNT + 1 - graph.edgeCount()); i < n; ++i) {
506           final int n1 = random.nextInt(newCount);
507           final int n2 = random.nextInt(newCount);
508           if (n1 != n2) {
509             newEdges.add(graph.createEdge(nodes[n1], nodes[n2]));
510           }
511         }
512       }
513     }
514   }
515 
516   /*
517   * #####################################################################
518   * factory methods for animations
519   * #####################################################################
520   */
521 
522   /**
523    * Creates an animation for fading in new nodes and extracting new edges.
524    * As a side effect, this animation will result in the new nodes and new
525    * edges being visible.
526    * @param newNodes   the nodes that should be faded in.
527    * @param newEdges   the edges that should be extracted.
528    * @return an animation for fading in new nodes and extracting new edges.
529    */
530   private AnimationObject createCreateAnimation(
531       final Collection newNodes,
532       final Collection newEdges
533   ) {
534     // create fade in animations for the new nodes and set them up to
535     // play simultaneously
536     final CompositeAnimationObject addNodes = AnimationFactory.createConcurrency();
537     for (Iterator it = newNodes.iterator(); it.hasNext();) {
538       final NodeRealizer nr = graph.getRealizer((Node) it.next());
539       addNodes.addAnimation(factory.fadeIn(nr, PREFERRED_DURATION * 2));
540     }
541 
542     // create extract animations for the new edges and set them up to
543     // play simultaneously
544     final CompositeAnimationObject addEdges = AnimationFactory.createConcurrency();
545     for (Iterator it = newEdges.iterator(); it.hasNext();) {
546       final EdgeRealizer er = graph.getRealizer((Edge) it.next());
547       addEdges.addAnimation(factory.extract(er, PREFERRED_DURATION));
548     }
549 
550     // create an animation that will first fade in nodes and then extract edges
551     //
552     // note that initAnimation for *both* addNodes and addEdges will happen
553     // before both addNodes and addEdges are played and disposeAnimation
554     // for *both* addNodes and addEdges will happen after addNodes and
555     // addEdges are played
556     // see also the API documentation for createSequence
557     return AnimationFactory.createSequence(addNodes, addEdges);
558   }
559 
560   /**
561    * Creates an animation that applies the specified graph layout to the
562    * graph structure.
563    * <p>
564    * Note that the graph may not be structurally altered in between creating
565    * and disposing (at the end of playing) of it.
566    * </p>
567    * @param gl   the new graph layout to be applied in an animated fashion.
568    * @return an animation that applies the specified graph layout to the
569    * graph structure.
570    */
571   private AnimationObject createMorphingAnimation(
572       final GraphLayout gl
573   ) {
574     final LayoutMorpher morphing = new LayoutMorpher(view, gl) {
575       public void disposeAnimation() {
576         super.disposeAnimation();
577         view.fitContent();
578       }
579     };
580     morphing.setPreferredDuration(PREFERRED_DURATION);
581     morphing.setSmoothViewTransform(true);
582     return AnimationFactory.createEasedAnimation(morphing);
583   }
584 
585   /**
586    * Creates an animation for retracting edges and fading out nodes.
587    * As a side effect, this animation will result in said edges and nodes being
588    * removed from the graph.
589    * @param nodesToBeDeleted   the nodes to fade out
590    * @param edgesToBeDeleted   the edges to retract
591    * @return an animation for retracting edges and fading out nodes.
592    */
593   private AnimationObject createDeleteAnimation(
594       final Set nodesToBeDeleted,
595       final Set edgesToBeDeleted
596   ) {
597     // create retract animations for the edges and set them up to play
598     // simultaneously
599     // note, the specified APPLY_EFFECT will result in the edges being actually
600     // removed at the end of the animation
601     final CompositeAnimationObject deleteEdges = AnimationFactory.createConcurrency();
602     for (Iterator it = edgesToBeDeleted.iterator(); it.hasNext();) {
603       final EdgeRealizer er = graph.getRealizer((Edge) it.next());
604       deleteEdges.addAnimation(factory.retract(
605           er, ViewAnimationFactory.APPLY_EFFECT, PREFERRED_DURATION));
606     }
607 
608     // create fade out animations for the nodes and set them up to play
609     // simultaneously
610     // note, the specified APPLY_EFFECT will result in the nodes being actually
611     // removed at the end of the animation
612     final CompositeAnimationObject deleteNodes = AnimationFactory.createConcurrency();
613     for (Iterator it = nodesToBeDeleted.iterator(); it.hasNext();) {
614       final NodeRealizer nr = graph.getRealizer((Node) it.next());
615       deleteNodes.addAnimation(factory.fadeOut(
616           nr, ViewAnimationFactory.APPLY_EFFECT, PREFERRED_DURATION));
617     }
618 
619     // create an animation that will first retract edges and then fade out nodes
620     //
621     // note that initAnimation for *both* deleteEdges and deleteNodes will
622     // happen before both deleteEdges and deleteNodes are played and
623     // disposeAnimation for *both* deleteEdges and deleteNodes will happen
624     // after deleteEdges and deleteNodes are played
625     // see also the API documentation for createSequence
626     return AnimationFactory.createSequence(deleteEdges, deleteNodes);
627   }
628 
629 
630   /**
631    * Calculates a new hierarchic layout.
632    * @param newNodes   nodes to be incrementally inserted into the existing
633    * layout.
634    * @param newEdges   edges to be incrementally inserted into the existing
635    * layout.
636    * @return a new hierarchic layout.
637    */
638   private GraphLayout calcLayout(
639       final Set newNodes,
640       final Set newEdges
641   ) {
642     final DataMap hints = Maps.createDataMap(new WeakHashMap());
643     final IncrementalHierarchicLayouter ihl = new IncrementalHierarchicLayouter();
644     ihl.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_INCREMENTAL);
645     ihl.setOrthogonallyRouted(true);
646     final IncrementalHintsFactory hf = ihl.createIncrementalHintsFactory();
647     for (Iterator it = newNodes.iterator(); it.hasNext();) {
648       final Object node = it.next();
649       hints.set(node, hf.createLayerIncrementallyHint(node));
650     }
651     for (Iterator it = newEdges.iterator(); it.hasNext();) {
652       final Object edge = it.next();
653       hints.set(edge, hf.createSequenceIncrementallyHint(edge));
654       if (((Edge) edge).source().degree() == 1) {
655         final Node node = ((Edge) edge).source();
656         hints.set(node, hf.createLayerIncrementallyHint(node));
657       }
658       if (((Edge) edge).target().degree() == 1) {
659         final Node node = ((Edge) edge).target();
660         hints.set(node, hf.createLayerIncrementallyHint(node));
661       }
662     }
663     graph.addDataProvider(IncrementalHierarchicLayouter.INCREMENTAL_HINTS_DPKEY, hints);
664     try {
665       return (new BufferedLayouter(ihl)).calcLayout(graph);
666     } finally {
667       graph.removeDataProvider(IncrementalHierarchicLayouter.INCREMENTAL_HINTS_DPKEY);
668     }
669   }
670 
671 
672   public static void main(String[] args) {
673     EventQueue.invokeLater(new Runnable() {
674       public void run() {
675         Locale.setDefault(Locale.ENGLISH);
676         initLnF();
677         (new AnimatedStructuralChangesDemo()).start();
678       }
679     });
680   }
681 
682 
683   private abstract static class Command implements AnimationListener {
684     public void animationPerformed(final AnimationEvent e) {
685       if (e.getHint() == AnimationEvent.END) {
686         execute();
687       }
688     }
689 
690     abstract void execute();
691   }
692 }
693