1   /****************************************************************************
2    * This demo file is part of yFiles for Java 2.14.
3    * Copyright (c) 2000-2017 by yWorks GmbH, Vor dem Kreuzberg 28,
4    * 72070 Tuebingen, Germany. All rights reserved.
5    * 
6    * yFiles demo files exhibit yFiles for Java functionalities. Any redistribution
7    * of demo files in source code or binary form, with or without
8    * modification, is not permitted.
9    * 
10   * Owners of a valid software license for a yFiles for Java version that this
11   * demo is shipped with are allowed to use the demo source code as basis
12   * for their own yFiles for Java powered applications. Use of such programs is
13   * governed by the rights and conditions as set out in the yFiles for Java
14   * license agreement.
15   * 
16   * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
17   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18   * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
19   * NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
21   * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   *
27   ***************************************************************************/
28  package demo.layout;
29  
30  import demo.view.DemoBase;
31  import y.anim.AnimationFactory;
32  import y.anim.AnimationObject;
33  import y.anim.AnimationPlayer;
34  import y.anim.CompositeAnimationObject;
35  import y.base.Edge;
36  import y.base.Node;
37  import y.layout.AbstractLayoutStage;
38  import y.layout.BufferedLayouter;
39  import y.layout.CanonicMultiStageLayouter;
40  import y.layout.ComponentLayouter;
41  import y.layout.FixNodeLayoutStage;
42  import y.layout.GraphLayout;
43  import y.layout.LayoutGraph;
44  import y.layout.LayoutStage;
45  import y.layout.Layouter;
46  import y.layout.OrientationLayouter;
47  import y.layout.PartitionLayouter;
48  import y.layout.PortConstraint;
49  import y.layout.PortConstraintKeys;
50  import y.layout.circular.CircularLayouter;
51  import y.layout.hierarchic.IncrementalHierarchicLayouter;
52  import y.layout.hierarchic.incremental.RoutingStyle;
53  import y.layout.hierarchic.incremental.SimplexNodePlacer;
54  import y.layout.organic.SmartOrganicLayouter;
55  import y.layout.orthogonal.CompactOrthogonalLayouter;
56  import y.layout.orthogonal.OrthogonalLayouter;
57  import y.layout.radial.RadialLayouter;
58  import y.layout.router.OrganicEdgeRouter;
59  import y.layout.router.polyline.EdgeRouter;
60  import y.layout.seriesparallel.SeriesParallelLayouter;
61  import y.layout.tree.BalloonLayouter;
62  import y.layout.tree.DendrogramPlacer;
63  import y.layout.tree.GenericTreeLayouter;
64  import y.layout.tree.TreeLayouter;
65  import y.layout.tree.TreeReductionStage;
66  import y.util.DataProviderAdapter;
67  import y.view.CreateEdgeMode;
68  import y.view.Drawable;
69  import y.view.EditMode;
70  import y.view.Graph2D;
71  import y.view.Graph2DLayoutExecutor;
72  import y.view.Graph2DView;
73  import y.view.Graph2DViewMouseWheelZoomListener;
74  import y.view.GraphicsContext;
75  import y.view.LayoutMorpher;
76  import y.view.ShapeNodeRealizer;
77  import y.view.ViewAnimationFactory;
78  import y.view.ViewMode;
79  import y.view.YRenderingHints;
80  
81  import javax.swing.JMenuBar;
82  import javax.swing.JToolBar;
83  import javax.swing.Timer;
84  import java.awt.AlphaComposite;
85  import java.awt.BasicStroke;
86  import java.awt.Color;
87  import java.awt.Composite;
88  import java.awt.Dimension;
89  import java.awt.EventQueue;
90  import java.awt.Font;
91  import java.awt.FontMetrics;
92  import java.awt.Graphics2D;
93  import java.awt.Insets;
94  import java.awt.Rectangle;
95  import java.awt.Stroke;
96  import java.awt.event.ActionEvent;
97  import java.awt.event.ActionListener;
98  import java.awt.font.FontRenderContext;
99  import java.awt.font.TextLayout;
100 import java.awt.geom.AffineTransform;
101 import java.awt.geom.GeneralPath;
102 import java.awt.geom.Rectangle2D;
103 import java.util.Locale;
104 
105 /**
106  * This demo shows the main layout styles provided by yFiles for Java:
107  * <ul>
108  *   <li>Hierarchic Layout</li>
109  *   <li>Organic Layout</li>
110  *   <li>Orthogonal Layout</li>
111  *   <li>Tree Layout</li>
112  *   <li>Circular Layout</li>
113  *   <li>Balloon Layout</li>
114  *   <li>Radial Layout</li>
115  * </ul>
116  *
117  * The demo calculates and displays these layouts one after the other for a given graph. The user can interrupt and
118  * continue the animation in order to change the graph.
119  */
120 public class LayoutDemo extends DemoBase {
121   private static final int DURATION_CYCLE = 6000;
122   private static final int DURATION_LAYOUT_CHANGE = DURATION_CYCLE / 8;
123   private static final int DURATION_INITIAL_DELAY = 2000;
124   static final int CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT = 0;
125   static final int CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT_LEFT_TO_RIGHT = 1;
126   static final int CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT_EDGE_GROUPING = 2;
127   static final int CONFIG_SMART_ORGANIC_LAYOUT = 0;
128   static final int CONFIG_SMART_ORGANIC_LAYOUT_CLUSTER = 1;
129   static final int CONFIG_ORTHOGONAL_LAYOUT = 0;
130   static final int CONFIG_ORTHOGONAL_LAYOUT_FACE_MAXIMIZATION = 1;
131   static final int CONFIG_ORTHOGONAL_LAYOUT_COMPACT = 2;
132   static final int CONFIG_TREE_LAYOUT = 0;
133   static final int CONFIG_BALLOON_LAYOUT = 1;
134   static final int CONFIG_CIRCULAR_LAYOUT = 0;
135   static final int CONFIG_CIRCULAR_LAYOUT_ONE_CIRCLE = 1;
136 
137   private final EditMode editMode;
138   private final Timer timer;
139   private final AnimationPlayer player;
140   private JMenuBar menuBar;
141   private JToolBar toolBar;
142   private boolean isVideoMode;
143   private final PlayButton playButton;
144   private final RelayoutButton relayoutButton;
145   private BufferedLayouter layouter;
146   private Graph2DViewMouseWheelZoomListener wheelZoomListener;
147 
148   public LayoutDemo() {
149     this(null);
150   }
151 
152   public LayoutDemo(final String helpFile) {
153     addHelpPane(helpFile);
154     view.setPreferredSize(new Dimension(1200, 900));
155 
156     wheelZoomListener = new Graph2DViewMouseWheelZoomListener();
157     wheelZoomListener.setCenterZooming(false);
158 
159     editMode = createEditMode();
160     isVideoMode = true;
161 
162     // initialize layouters
163     final Layouter[] layouts = {
164         createHierarchicLayouter(CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT),
165         createSmartOrganicLayouter(CONFIG_SMART_ORGANIC_LAYOUT),
166         createOrthogonalLayouter(CONFIG_ORTHOGONAL_LAYOUT),
167         createTreeLayouter(CONFIG_TREE_LAYOUT),
168         createCircularLayouter(CONFIG_CIRCULAR_LAYOUT),
169         createHierarchicLayouter(CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT_LEFT_TO_RIGHT),
170         createSmartOrganicLayouter(CONFIG_SMART_ORGANIC_LAYOUT_CLUSTER),
171         createOrthogonalLayouter(CONFIG_ORTHOGONAL_LAYOUT_FACE_MAXIMIZATION),
172         createTreeLayouter(CONFIG_BALLOON_LAYOUT),
173         createHierarchicLayouter(CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT_EDGE_GROUPING),
174         createCircularLayouter(CONFIG_CIRCULAR_LAYOUT_ONE_CIRCLE),
175         createDendrogramLayouter(),
176         createOrthogonalLayouter(CONFIG_ORTHOGONAL_LAYOUT_COMPACT),
177         createRadialLayouter(),
178         createSeriesParallelLayouter()
179     };
180 
181     // initialize layout titles
182     final String[] titles = {
183       "Hierarchic Layout",
184       "Organic Layout",
185       "Orthogonal Layout",
186       "Tree Layout",
187       "Circular Layout",
188       "Hierarchic Layout - Left to Right",
189       "Organic Layout - Clustering",
190       "Orthogonal Layout - Face Maximization",
191       "Balloon Layout",
192       "Hierarchic Layout - Edge Grouping",
193       "Circular Layout - One Circle",
194       "Dendrogram Layout",
195       "Compact Orthogonal Layout",
196       "Radial Layout",
197       "Series-Parallel Layout"
198     };
199 
200     // load example graph
201     loadGraph("resource/layoutdemograph.graphml");
202 
203     // add data provider for FixNodeLayoutStage to fixate the layouts in the center of their bounds
204     view.getGraph2D().addDataProvider(FixNodeLayoutStage.FIXED_NODE_DPKEY, new DataProviderAdapter() {
205       public boolean getBool(Object dataHolder) {
206         return dataHolder instanceof Node;
207       }
208     });
209 
210     // initialize a drawable for the title showing the name of the current layout
211     final LayoutTitle title = new LayoutTitle(DURATION_LAYOUT_CHANGE, view);
212     view.addDrawable(title);
213 
214     // initialize a button with which the user can pause and continue the layout animation
215     playButton = new PlayButton();
216     view.addDrawable(playButton);
217     // initialize a button with which the user can relayout the graph in edit mode
218     relayoutButton = new RelayoutButton();
219 
220     // initialize animation player and drawables
221     player = new ViewAnimationFactory(view).createConfiguredPlayer();
222     player.setBlocking(false);
223 
224     // start update layout cycle
225     timer = new Timer(DURATION_INITIAL_DELAY, new ActionListener() {
226       private int index;
227 
228       public void actionPerformed(ActionEvent e) {
229         // create concurrent animation of title display and layout calculation
230         final CompositeAnimationObject concurrency = AnimationFactory.createConcurrency();
231         title.setNextText(titles[index]);
232         concurrency.addAnimation(title);
233         layouter = new BufferedLayouter(new FixNodeLayoutStage(layouts[index]));
234         final GraphLayout graphLayout = layouter.calcLayout(view.getGraph2D());
235         final LayoutMorpher morpher = new LayoutMorpher(view, graphLayout);
236         morpher.setPreferredDuration(DURATION_LAYOUT_CHANGE);
237         concurrency.addAnimation(morpher);
238 
239         // start layout
240         player.animate(concurrency);
241 
242         // next layout index
243         index = (index + 1) % layouts.length;
244       }
245     });
246     timer.setDelay(DURATION_CYCLE);
247     timer.start();
248   }
249 
250   /**
251    * Overwritten to prevent node creation for mouse clicks on buttons.
252    */
253   protected EditMode createEditMode() {
254     final EditMode mode = new EditMode() {
255       protected void paperClicked(final Graph2D graph, final double x, final double y, final boolean modifierSet) {
256         if (!playButton.getBounds2D().contains(x, y)
257             && !relayoutButton.getBounds2D().contains(x, y)) {
258           super.paperClicked(graph, x, y, modifierSet);
259         }
260       }
261     };
262 
263     if (mode.getCreateEdgeMode() instanceof CreateEdgeMode) {
264       ((CreateEdgeMode) mode.getCreateEdgeMode()).setIndicatingTargetNode(true);
265     }
266     return mode;
267   }
268 
269   /**
270    * Creates a configured {@link IncrementalHierarchicLayouter} instance.
271    *
272    * @param configuration number of the configuration to use.
273    */
274   private static Layouter createHierarchicLayouter(int configuration) {
275     final IncrementalHierarchicLayouter layouter = new IncrementalHierarchicLayouter();
276     switch (configuration) {
277       case CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT:
278         ((SimplexNodePlacer) layouter.getNodePlacer()).setBaryCenterModeEnabled(true);
279         layouter.getEdgeLayoutDescriptor().setRoutingStyle(new RoutingStyle(RoutingStyle.EDGE_STYLE_ORTHOGONAL));
280         break;
281       case CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT_LEFT_TO_RIGHT:
282         ((SimplexNodePlacer) layouter.getNodePlacer()).setBaryCenterModeEnabled(true);
283         layouter.getEdgeLayoutDescriptor().setRoutingStyle(new RoutingStyle(RoutingStyle.EDGE_STYLE_OCTILINEAR));
284         layouter.setLayoutOrientation(OrientationLayouter.LEFT_TO_RIGHT);
285         break;
286       case CONFIG_INCREMENTAL_HIERARCHIC_LAYOUT_EDGE_GROUPING:
287         ((SimplexNodePlacer) layouter.getNodePlacer()).setBaryCenterModeEnabled(true);
288         layouter.getEdgeLayoutDescriptor().setRoutingStyle(new RoutingStyle(RoutingStyle.EDGE_STYLE_ORTHOGONAL));
289         layouter.setLayoutOrientation(OrientationLayouter.BOTTOM_TO_TOP);
290         layouter.setAutomaticEdgeGroupingEnabled(true);
291         break;
292     }
293     return layouter;
294   }
295 
296   /**
297    * Creates a configured {@link SmartOrganicLayouter} instance.
298    *
299    * @param configuration number of the configuration to use.
300    */
301   private static Layouter createSmartOrganicLayouter(int configuration) {
302     final SmartOrganicLayouter layouter = new SmartOrganicLayouter();
303     switch (configuration) {
304       case CONFIG_SMART_ORGANIC_LAYOUT:
305         layouter.setPreferredEdgeLength(40);
306         layouter.setNodeSizeAware(true);
307         break;
308       case CONFIG_SMART_ORGANIC_LAYOUT_CLUSTER:
309         layouter.setPreferredEdgeLength(60);
310         layouter.setNodeSizeAware(true);
311         layouter.setAutoClusteringEnabled(true);
312         layouter.setAutoClusteringQuality(0.5);
313         layouter.setLayoutOrientation(OrientationLayouter.RIGHT_TO_LEFT);
314     }
315     return layouter;
316   }
317 
318   /**
319    * Creates a configured {@link OrthogonalLayouter} instance.
320    *
321    * @param configuration number of the configuration to use.
322    */
323   private static Layouter createOrthogonalLayouter(int configuration) {
324     final OrthogonalLayouter ol;
325     switch (configuration) {
326       default:
327       case CONFIG_ORTHOGONAL_LAYOUT:
328         ol = new OrthogonalLayouter();
329         ol.setUseRandomization(false);
330         ol.setPerceivedBendsOptimizationEnabled(true);
331         ol.setGrid(10);
332         return ol;
333       case CONFIG_ORTHOGONAL_LAYOUT_FACE_MAXIMIZATION:
334         ol = new OrthogonalLayouter();
335         ol.setUseRandomization(false);
336         ol.setUseFaceMaximization(true);
337         ol.setLayoutOrientation(OrientationLayouter.LEFT_TO_RIGHT);
338         ol.setGrid(10);
339         return ol;
340       case CONFIG_ORTHOGONAL_LAYOUT_COMPACT:
341         final CompactOrthogonalLayouter col = new CompactOrthogonalLayouter();
342         col.setGridSpacing(10);
343         ol = (OrthogonalLayouter) col.getCoreLayouter();
344         ol.setLayoutStyle(OrthogonalLayouter.NORMAL_STYLE);
345         final PartitionLayouter.ComponentPartitionPlacer placer = (PartitionLayouter.ComponentPartitionPlacer) col.getPartitionPlacer();
346         placer.getComponentLayouter().setStyle(ComponentLayouter.STYLE_PACKED_COMPACT_RECTANGLE);
347         return col;
348     }
349   }
350 
351   /**
352    * Creates a configured {@link GenericTreeLayouter} instance.
353    *
354    * @param configuration number of the configuration to use.
355    */
356   private static Layouter createTreeLayouter(int configuration) {
357     final Layouter layouter;
358     switch (configuration) {
359       default:
360       case CONFIG_TREE_LAYOUT:
361         layouter = new TreeLayouter();
362         break;
363       case CONFIG_BALLOON_LAYOUT:
364         layouter = new BalloonLayouter();
365     }
366     final TreeReductionStage treeReductionStage = new TreeReductionStage();
367     treeReductionStage.setNonTreeEdgeRouter(new OrganicEdgeRouter());
368     ((CanonicMultiStageLayouter) layouter).appendStage(treeReductionStage);
369     return layouter;
370   }
371 
372   /**
373    * Creates a configured dendrogram layouter. In a dendrogram, all subtrees of a single local root align at their
374    * bottom border.
375    */
376   private static Layouter createDendrogramLayouter() {
377     final GenericTreeLayouter layouter = new GenericTreeLayouter();
378     layouter.setDefaultNodePlacer(new DendrogramPlacer());
379     EdgeRouter nonTreeEdgeRouter = new EdgeRouter();
380     nonTreeEdgeRouter.setSphereOfAction(EdgeRouter.ROUTE_SELECTED_EDGES);
381     final TreeReductionStage treeReductionStage = new TreeReductionStage();
382     treeReductionStage.setNonTreeEdgeRouter(nonTreeEdgeRouter);
383     treeReductionStage.setNonTreeEdgeSelectionKey(EdgeRouter.SELECTED_EDGES);
384     layouter.appendStage(treeReductionStage);
385 
386     // as DendrogramPlacer cannot route edges orthogonally, use EdgeRouter to get orthogonal edges
387     // to ensure that tree edges always leave their source at the bottom and enter their target at the top, temporarily
388     // add port constraints before invoking the edge router
389     LayoutStage stage = new AbstractLayoutStage(new EdgeRouter()) {
390       public boolean canLayout(LayoutGraph graph) {
391         return canLayoutCore(graph);
392       }
393 
394       public void doLayout(LayoutGraph graph) {
395         // Note: adding and removing data providers like this is possible because the graph has no previous port
396         //       constraints or edge groupings assigned because they would be replaced. In that case this layout stage
397         //       can either be removed to use the assigned constraints or it has to store them before doLayoutCore and
398         //       restore them afterwards.
399 
400         // add source port constraint SOUTH
401         graph.addDataProvider(PortConstraintKeys.SOURCE_PORT_CONSTRAINT_KEY, new DataProviderAdapter() {
402           public Object get(Object dataHolder) {
403             return PortConstraint.create(PortConstraint.SOUTH, true);
404           }
405         });
406         // add target port constraint NORTH
407         graph.addDataProvider(PortConstraintKeys.TARGET_PORT_CONSTRAINT_KEY, new DataProviderAdapter() {
408           public Object get(Object dataHolder) {
409             return PortConstraint.create(PortConstraint.NORTH, true);
410           }
411         });
412         // add source node as group id to group all its outgoing edges
413         graph.addDataProvider(PortConstraintKeys.SOURCE_GROUPID_KEY, new DataProviderAdapter() {
414           public Object get(Object dataHolder) {
415             return ((Edge) dataHolder).source();
416           }
417         });
418 
419         // route edges
420         doLayoutCore(graph);
421 
422         // remove data providers
423         graph.removeDataProvider(PortConstraintKeys.SOURCE_PORT_CONSTRAINT_KEY);
424         graph.removeDataProvider(PortConstraintKeys.TARGET_PORT_CONSTRAINT_KEY);
425         graph.removeDataProvider(PortConstraintKeys.SOURCE_GROUPID_KEY);
426       }
427     };
428     layouter.appendStage(stage);
429     return layouter;
430   }
431 
432   /**
433    * Creates a configured {@link CircularLayouter} instance.
434    *
435    * @param configuration number of the configuration to use.
436    */
437   private static Layouter createCircularLayouter(int configuration) {
438     final CircularLayouter layouter = new CircularLayouter();
439     switch (configuration) {
440       case CONFIG_CIRCULAR_LAYOUT:
441         break;
442       case CONFIG_CIRCULAR_LAYOUT_ONE_CIRCLE:
443         layouter.setLayoutStyle(CircularLayouter.SINGLE_CYCLE);
444     }
445     return layouter;
446   }
447   
448   /**
449    * Creates a configured {@link y.layout.radial.RadialLayouter} instance.
450    */
451   private static Layouter createRadialLayouter() {
452     return new RadialLayouter();
453   }
454 
455   /**
456    * Creates a configured {@link SeriesParallelLayouter} instance.
457    */
458   private static Layouter createSeriesParallelLayouter() {
459     final SeriesParallelLayouter layouter = new SeriesParallelLayouter();
460     layouter.setGeneralGraphHandlingEnabled(true);
461     return layouter;
462   }
463 
464   /**
465    * Creates a menu bar that can be hidden when the layout animation is playing.
466    */
467   protected JMenuBar createMenuBar() {
468     menuBar = super.createMenuBar();
469     menuBar.setVisible(false);
470     return menuBar;
471   }
472 
473   /**
474    * Creates a tool bar that can be hidden when the layout animation is playing.
475    */
476   protected JToolBar createToolBar() {
477     toolBar = super.createToolBar();
478     toolBar.setVisible(false);
479     return toolBar;
480   }
481 
482   /**
483    * Overwritten to use the {@link ShapeNodeRealizer} as default node realizer.
484    */
485   protected void configureDefaultRealizers() {
486     final ShapeNodeRealizer realizer = new ShapeNodeRealizer(ShapeNodeRealizer.ROUND_RECT);
487     realizer.setSize(37.5, 37.5);
488     realizer.setFillColor(new Color(205, 2, 2));
489     realizer.removeLabel(0);
490     view.getGraph2D().setDefaultNodeRealizer(realizer);
491   }
492 
493   /**
494    * Overwritten to register a {@link ViewMode} that handles all control buttons.
495    */
496   protected void registerViewModes() {
497     view.addViewMode(new ViewMode() {
498       /**
499        * Overwritten to invoke the action of the button that contains the current mouse position.
500        *
501        * @param x the x-coordinate of the mouse event in world coordinates.
502        * @param y the y-coordinate of the mouse event in world coordinates.
503        */
504       public void mouseClicked(final double x, final double y) {
505         if (playButton.getBounds2D().contains(x, y)) {
506           playButton.action();
507         } else if (relayoutButton.isVisible() && relayoutButton.getBounds2D().contains(x, y)) {
508           relayoutButton.action();
509         }
510       }
511 
512       /**
513        * Overwritten to change the state of the buttons into a (not-)hovered state when the button contains the current
514        * mouse position.
515        *
516        * @param x the x-coordinate of the mouse event in world coordinates.
517        * @param y the y-coordinate of the mouse event in world coordinates.
518        */
519       public void mouseMoved(final double x, final double y) {
520         playButton.setHovered(playButton.getBounds2D().contains(x, y));
521         relayoutButton.setHovered(relayoutButton.getBounds2D().contains(x, y));
522       }
523     });
524   }
525 
526   /**
527    * Overwritten to avoid that the mouse wheel listener is registered automatically. The mouse wheel listener makes
528    * zooming available for the user. Since we only want zooming in edit mode we add and remove the listener manually.
529    */
530   protected void registerViewListeners() {
531   }
532 
533   /**
534    * Starts the {@link LayoutDemo}.
535    */
536   public static void main(final String[] args) {
537     EventQueue.invokeLater(new Runnable() {
538       public void run() {
539         Locale.setDefault(Locale.ENGLISH);
540         initLnF();
541         (new LayoutDemo("resource/layoutdemohelp.html")).start();
542       }
543     });
544   }
545 
546   /**
547    * A {@link Drawable} that displays a string on a rectangular background.
548    * <p>
549    *   The drawable is placed at the upper left corner of the view and is zoom-invariant. As it is also an
550    *   {@link AnimationObject} it will change its opacity when played by an {@link AnimationPlayer}.
551    * </p>
552    */
553   private static class LayoutTitle implements Drawable, AnimationObject {
554     private static final Color COLOR_BACKGROUND = new Color(0, 139, 139);
555     private static final Color COLOR_TEXT = Color.WHITE;
556     private static final Font FONT_TEXT = new Font("Arial", Font.PLAIN, 36);
557     private static final Insets INSETS_TEXT = new Insets(15, 15, 15, 15);
558     private static final int INSET = 10;
559     private static final int ARC = 30;
560 
561     private final Graph2DView view;
562     private final long preferredDuration;
563 
564     private String text;
565     private String nextText;
566     private float opacity;
567 
568     private LayoutTitle(long preferredDuration, Graph2DView view) {
569       this.preferredDuration = preferredDuration;
570       this.view = view;
571     }
572 
573     public void paint(Graphics2D graphics) {
574       // store graphics
575       final Color oldColor = graphics.getColor();
576       final Font oldFont = graphics.getFont();
577       final AffineTransform oldTransform = graphics.getTransform();
578       final Composite oldComposite = graphics.getComposite();
579       try {
580         if (text != null && text.length() > 0) {
581           // because this title is zoom invariant we get the view's transform to be able to paint in view-coordinates
582           final GraphicsContext context = YRenderingHints.getGraphicsContext(graphics);
583           graphics.setTransform(context.getViewTransform());
584 
585           // get the fonts measurements to paint the background accordingly
586           graphics.setFont(FONT_TEXT);
587           final FontMetrics fontMetrics = graphics.getFontMetrics();
588           final int stringWidth = (int) fontMetrics.getStringBounds(text, graphics).getWidth();
589           final int stringHeight = fontMetrics.getMaxAscent() + fontMetrics.getMaxDescent();
590 
591           // background bounds in view-coordinates
592           final int x = INSET;
593           final int y = INSET;
594           final int width = stringWidth + INSETS_TEXT.left + INSETS_TEXT.right;
595           final int height = stringHeight + INSETS_TEXT.top + INSETS_TEXT.bottom;
596 
597           // paint background
598           graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
599           graphics.setColor(COLOR_BACKGROUND);
600           graphics.fillRoundRect(x, y, width, height, ARC, ARC);
601 
602           // paint text
603           graphics.setComposite(oldComposite);
604           graphics.setColor(COLOR_TEXT);
605           graphics.drawString(text, x + INSETS_TEXT.left, y + INSETS_TEXT.top + fontMetrics.getMaxAscent());
606         }
607       } finally {
608         // restore graphics
609         graphics.setColor(oldColor);
610         graphics.setFont(oldFont);
611         graphics.setTransform(oldTransform);
612         graphics.setComposite(oldComposite);
613       }
614     }
615 
616     public Rectangle getBounds() {
617       final TextLayout textLayout =
618           new TextLayout(text, FONT_TEXT, new FontRenderContext(FONT_TEXT.getTransform(), true, true));
619       final double backgroundWidth = textLayout.getBounds().getWidth() + INSETS_TEXT.left + INSETS_TEXT.right;
620       final double backgroundHeight = textLayout.getBounds().getHeight() + INSETS_TEXT.top + INSETS_TEXT.bottom;
621 
622       final int minX = (int) Math.floor(view.toWorldCoordX(INSET));
623       final int maxX = (int) Math.ceil(view.toWorldCoordX((int) (INSET + backgroundWidth)));
624       final int minY = (int) Math.floor(view.toWorldCoordY(INSET));
625       final int maxY = (int) Math.ceil(view.toWorldCoordY((int) (INSET + backgroundHeight)));
626       return new Rectangle(minX, minY, maxX - minX, maxY - minY);
627     }
628 
629     public void setNextText(String text) {
630       nextText = text;
631     }
632 
633     public void initAnimation() {
634       opacity = 0;
635     }
636 
637     public void calcFrame(double time) {
638       if (time < 0.5) {
639         if (text != null) {
640           opacity = (float) (1 - 2 *time);
641         }
642       } else if (time > 0.5) {
643         if (!nextText.equals(text)) {
644           text = nextText;
645         }
646         opacity = (float) (2 * time -1);
647       }
648     }
649 
650     public void disposeAnimation() {
651     }
652 
653     public long preferredDuration() {
654       return preferredDuration;
655     }
656   }
657 
658   /**
659    * A {@link Drawable} that describes a play button. The button is placed at the lower left corner of the view and
660    * is zoom-invariant. A mouse click on the button toggles between video and edit mode.
661    */
662   private class PlayButton implements Drawable {
663     private static final int HEIGHT = 70;
664     private static final int WIDTH = 100;
665     private static final int INSET = 10;
666     private static final int ARC = 30;
667 
668     private boolean isHovered;
669 
670     /**
671      * Returns whether or not the button contains the current mouse position.
672      *
673      * @return <code>true</code> if the button contains the current mouse position, <code>false</code> otherwise.
674      */
675     boolean isHovered() {
676       return isHovered;
677     }
678 
679     /**
680      * Specifies whether or not the button contains the current mouse position.
681      *
682      * @param isHovered <code>true</code> if the button contains the current mouse position, <code>false</code>
683      *                  otherwise.
684      */
685     void setHovered(final boolean isHovered) {
686       if (this.isHovered != isHovered) {
687         view.updateView();
688       }
689       this.isHovered = isHovered;
690     }
691 
692     public Rectangle getBounds() {
693       final Rectangle2D r = getBounds2D();
694       final int minX = (int) Math.floor(r.getX());
695       final int maxX = (int) Math.ceil(r.getMaxX());
696       final int minY = (int) Math.floor(r.getY());
697       final int maxY = (int) Math.ceil(r.getMaxY());
698       return new Rectangle(minX, minY, maxX - minX, maxY - minY);
699     }
700 
701     /**
702      * Returns the bounds of the {@link Drawable} in world-coordinates. Should be used for hit and contains tests
703      * because the rounding errors are less.
704      *
705      * @return the bounds of the <code>Drawable</code> in world-coordinates.
706      */
707     public Rectangle2D getBounds2D() {
708       final double minX = view.toWorldCoordX(INSET);
709       final double maxX = view.toWorldCoordX(INSET + WIDTH);
710       final int y = view.getHeight() - INSET;
711       final double minY = view.toWorldCoordY(y - HEIGHT);
712       final double maxY = view.toWorldCoordY(y);
713       return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
714     }
715 
716     public void paint(final Graphics2D graphics) {
717       final Color oldColor = graphics.getColor();
718       final AffineTransform oldTransform = graphics.getTransform();
719 
720       // because this button is zoom invariant we get the view's transform to be able to paint in view-coordinates.
721       final GraphicsContext context = YRenderingHints.getGraphicsContext(graphics);
722       graphics.setTransform(context.getViewTransform());
723 
724       // bounds in view coordinates
725       final int x = INSET;
726       final int y = view.getHeight() - INSET - HEIGHT;
727       final int w = WIDTH;
728       final int h = HEIGHT;
729 
730       // paint background
731       graphics.setColor(isHovered() ? Color.GRAY.brighter() : Color.GRAY);
732       graphics.fillRoundRect(x, y, WIDTH, HEIGHT, ARC, ARC);
733 
734       // paint icon
735       graphics.setColor(Color.WHITE);
736       if (!isVideoMode) {
737         final GeneralPath icon = new GeneralPath();
738         icon.moveTo(x + w / 3, y + h / 5);
739         icon.lineTo(x + w / 3, y + 4 * h / 5);
740         icon.lineTo(x + 2 * w / 3, y + h / 2);
741         icon.closePath();
742         graphics.fill(icon);
743       } else {
744         graphics.fillRect(x + w / 3, y + h / 5, w / 9, 3 * h / 5);
745         graphics.fillRect(x + 5 * w / 9, y + h / 5, w / 9, 3 * h / 5);
746       }
747 
748       graphics.setColor(oldColor);
749       graphics.setTransform(oldTransform);
750     }
751 
752     /**
753      * Switches between video mode and edit mode.
754      */
755     void action() {
756       if (isVideoMode) {
757         view.addViewMode(editMode);
758         wheelZoomListener.addToCanvas(view);
759         menuBar.setVisible(true);
760         toolBar.setVisible(true);
761         view.addDrawable(relayoutButton);
762         timer.stop();
763       } else {
764         view.removeViewMode(editMode);
765         wheelZoomListener.removeFromCanvas(view);
766         view.getGraph2D().unselectAll();
767         menuBar.setVisible(false);
768         toolBar.setVisible(false);
769         view.removeDrawable(relayoutButton);
770         timer.start();
771       }
772       isVideoMode = !isVideoMode;
773     }
774   }
775 
776   /**
777    * A {@link Drawable} that describes a layout button. The button is placed beside the play
778    * button when the edit mode is running. A mouse click on the button recalculates the current layout.
779    */
780   private class RelayoutButton implements Drawable {
781     private static final int HEIGHT = 70;
782     private static final int WIDTH = 100;
783     private static final int INSET = 10;
784     private static final int ARC = 30;
785 
786     private final BasicStroke strokeArc = new BasicStroke(10);
787     private boolean isHovered;
788 
789     /**
790      * Returns whether or not the button contains the current mouse position.
791      *
792      * @return <code>true</code> if the button contains the current mouse position, <code>false</code> otherwise.
793      */
794     boolean isHovered() {
795       return isHovered;
796     }
797 
798     /**
799      * Specifies whether or not the button contains the current mouse position.
800      *
801      * @param isHovered <code>true</code> if the button contains the current mouse position, <code>false</code>
802      *                  otherwise.
803      */
804     void setHovered(final boolean isHovered) {
805       if (this.isHovered != isHovered) {
806         view.updateView();
807       }
808       this.isHovered = isHovered;
809     }
810 
811     public Rectangle getBounds() {
812       final Rectangle2D r = getBounds2D();
813       final int minX = (int) Math.floor(r.getX());
814       final int maxX = (int) Math.ceil(r.getMaxX());
815       final int minY = (int) Math.floor(r.getY());
816       final int maxY = (int) Math.ceil(r.getMaxY());
817       return new Rectangle(minX, minY, maxX - minX, maxY - minY);
818     }
819 
820     /**
821      * Returns the bounds of the {@link Drawable} in world-coordinates. Should be used for hit and contains tests
822      * because the rounding errors are less.
823      *
824      * @return the bounds of the <code>Drawable</code> in world-coordinates.
825      */
826     public Rectangle2D getBounds2D() {
827       final double minX = view.toWorldCoordX(2 * INSET + PlayButton.WIDTH);
828       final double maxX = view.toWorldCoordX(2 * INSET + PlayButton.WIDTH + WIDTH);
829       final int y = view.getHeight() - INSET;
830       final double minY = view.toWorldCoordY(y - HEIGHT);
831       final double maxY = view.toWorldCoordY(y);
832       return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
833     }
834 
835     public void paint(final Graphics2D graphics) {
836       final Color oldColor = graphics.getColor();
837       final AffineTransform oldTransform = graphics.getTransform();
838       final Stroke oldStroke = graphics.getStroke();
839 
840       // because this button is zoom invariant we get the view's transform to be able to paint in view-coordinates
841       final GraphicsContext context = YRenderingHints.getGraphicsContext(graphics);
842       graphics.setTransform(context.getViewTransform());
843 
844        // bounds in view coordinates
845        final int x = 2 * INSET + PlayButton.WIDTH;
846        final int y = view.getHeight() - INSET - HEIGHT;
847        final int w = WIDTH;
848        final int h = HEIGHT;
849 
850        // paint background
851        graphics.setColor(isHovered() ? Color.GRAY.brighter() : Color.GRAY);
852        graphics.fillRoundRect(x, y, WIDTH, HEIGHT, ARC, ARC);
853 
854       // paint icon
855       graphics.setColor(Color.WHITE);
856       graphics.setStroke(strokeArc);
857       final int iconSize = w / 3;
858       final int iconX = x + (w - iconSize) / 2;
859       final int iconY = y + (h - iconSize) / 2;
860       graphics.drawArc(iconX, iconY, iconSize, iconSize, 135, 270);
861 
862       final GeneralPath arrow = new GeneralPath();
863       arrow.moveTo((float) (iconX + iconSize * 0.6), (float) (iconY - iconSize * 0.1));
864       arrow.lineTo((float) (iconX + iconSize), (float) (iconY - iconSize * 0.1));
865       arrow.lineTo((float) (iconX + iconSize * 0.6), (float) (iconY + iconSize * 0.3));
866       arrow.closePath();
867       graphics.fill(arrow);
868 
869       graphics.setColor(oldColor);
870       graphics.setTransform(oldTransform);
871       graphics.setStroke(oldStroke);
872     }
873 
874     boolean isVisible() {
875       return !isVideoMode;
876     }
877 
878     /**
879      * Recalculates the layout.
880      */
881     void action() {
882       if (layouter != null) {
883         final Graph2DLayoutExecutor executor = new Graph2DLayoutExecutor();
884         final LayoutMorpher morpher = executor.getLayoutMorpher();
885         morpher.setPreferredDuration(DURATION_LAYOUT_CHANGE);
886         morpher.setKeepZoomFactor(true);
887         executor.doLayout(view, layouter);
888       }
889     }
890   }
891 }
892