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.multipage;
29  
30  import demo.view.DemoBase;
31  import demo.view.DemoDefaults;
32  import y.base.DataProvider;
33  import y.base.Edge;
34  import y.base.Graph;
35  import y.base.Node;
36  import y.base.NodeCursor;
37  import y.geom.YDimension;
38  import y.io.IOHandler;
39  import y.io.ZipGraphMLIOHandler;
40  import y.layout.LayoutTool;
41  import y.layout.Layouter;
42  import y.layout.hierarchic.IncrementalHierarchicLayouter;
43  import y.layout.multipage.LayoutCallback;
44  import y.layout.multipage.MultiPageLayout;
45  import y.layout.multipage.MultiPageLayouter;
46  import y.layout.multipage.NodeInfo;
47  import y.layout.organic.SmartOrganicLayouter;
48  import y.layout.orthogonal.CompactOrthogonalLayouter;
49  import y.layout.orthogonal.OrthogonalLayouter;
50  import y.option.Editor;
51  import y.option.OptionHandler;
52  import y.option.TableEditorFactory;
53  import y.util.D;
54  import y.util.DataProviderAdapter;
55  import y.util.GraphCopier;
56  import y.view.Drawable;
57  import y.view.EdgeRealizer;
58  import y.view.Graph2D;
59  import y.view.Graph2DLayoutExecutor;
60  import y.view.Graph2DTraversal;
61  import y.view.Graph2DView;
62  import y.view.Graph2DViewMouseWheelZoomListener;
63  import y.view.HitInfo;
64  import y.view.LineType;
65  import y.view.NavigationMode;
66  import y.view.NodeRealizer;
67  import y.view.ViewMode;
68  import y.view.hierarchy.HierarchyManager;
69  
70  import javax.swing.AbstractAction;
71  import javax.swing.Action;
72  import javax.swing.BorderFactory;
73  import javax.swing.JButton;
74  import javax.swing.JComponent;
75  import javax.swing.JDialog;
76  import javax.swing.JEditorPane;
77  import javax.swing.JFileChooser;
78  import javax.swing.JFrame;
79  import javax.swing.JLabel;
80  import javax.swing.JMenu;
81  import javax.swing.JMenuBar;
82  import javax.swing.JPanel;
83  import javax.swing.JProgressBar;
84  import javax.swing.JRootPane;
85  import javax.swing.JScrollPane;
86  import javax.swing.JSplitPane;
87  import javax.swing.JTextField;
88  import javax.swing.JToolBar;
89  import javax.swing.JTree;
90  import javax.swing.SwingUtilities;
91  import javax.swing.filechooser.FileFilter;
92  import javax.swing.tree.DefaultTreeCellRenderer;
93  import javax.swing.tree.TreePath;
94  import java.awt.BorderLayout;
95  import java.awt.Color;
96  import java.awt.Component;
97  import java.awt.Cursor;
98  import java.awt.Dimension;
99  import java.awt.EventQueue;
100 import java.awt.FlowLayout;
101 import java.awt.Graphics2D;
102 import java.awt.Rectangle;
103 import java.awt.Window;
104 import java.awt.event.ActionEvent;
105 import java.awt.event.MouseAdapter;
106 import java.awt.event.MouseEvent;
107 import java.awt.geom.Rectangle2D;
108 import java.beans.PropertyChangeEvent;
109 import java.beans.PropertyChangeListener;
110 import java.io.File;
111 import java.io.IOException;
112 import java.net.MalformedURLException;
113 import java.net.URI;
114 import java.net.URISyntaxException;
115 import java.net.URL;
116 import java.util.ArrayList;
117 import java.util.HashMap;
118 import java.util.Iterator;
119 import java.util.List;
120 import java.util.Locale;
121 import java.util.Map;
122 
123 /**
124  * Demonstrates how to use {@link MultiPageLayouter} to divide a large model
125  * graph into several smaller page graphs.
126  * <p>
127  * Method {@link #doMultiPageLayout()} demonstrates how to prepare
128  * the model graph for multi-page layout, how to configure and how to run
129  * the multi-page layout algorithm.
130  * </p><p>
131  * Class {@link MultiPageGraph2DBuilder} demonstrates how to create
132  * displayable {@link Graph2D} instances from a {@link MultiPageLayout}
133  * that is the result of a multi-page layout calculation.
134  * </p><p>
135  * Moreover, the demo shows different methods to navigate through the page
136  * graphs:
137  * </p>
138  * <ul>
139  * <li>
140  * Clicking on a connector, proxy, or proxy reference node will switch to
141  * the page graph holding the referenced node.
142  * </li>
143  * <li>
144  * Clicking on a page in the demo's overview component will switch to the
145  * corresponding page graph.
146  * </li>
147  * <li>
148  * Using the toolbar arrow controls it is possible to navigate sequentially
149  * through the page graphs.
150  * </li>
151  * </ul>
152  * @see NodeInfo#TYPE_CONNECTOR
153  * @see NodeInfo#TYPE_PROXY
154  * @see NodeInfo#TYPE_PROXY_REFERENCE
155  *
156  * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/multipage_layout#multipage_layout" target="_blank">Section Multi-page Layout</a> in the yFiles for Java Developer's Guide
157  */
158 public class MultiPageLayoutDemo extends DemoBase {
159   private static final Color PAGE_BACKGROUND = new Color(230, 230, 230);
160 
161   private final Graph2D baseModel;
162   private final MultiPageGraph2DBuilder pageBuilder;
163   private final MultiPageLayoutOptionHandler oh;
164   private JTextField pageNumberTextField;
165 
166   private List pageList;
167   private Map id2LocationInfo;
168 
169   private int previousPageIndex;
170   private int currentPageIndex;
171 
172   public MultiPageLayoutDemo() {
173     this(null);
174   }
175 
176   public MultiPageLayoutDemo( final String helpFilePath ) {
177     oh = new MultiPageLayoutOptionHandler();
178     oh.addDrawPageChangeListener(new PropertyChangeListener() {
179       public void propertyChange( final PropertyChangeEvent e ) {
180         view.updateView();
181       }
182     });
183     baseModel = view.getGraph2D();
184     new HierarchyManager(baseModel);
185     pageBuilder = new MultiPageGraph2DBuilder(null, null);
186     id2LocationInfo = new HashMap();
187 
188     //configure the main view that displays calculated page graphs
189     view.setGraph2D(new Graph2D());
190     view.setContentPolicy(Graph2DView.CONTENT_POLICY_BACKGROUND_DRAWABLES);
191     view.addBackgroundDrawable(new PageBorderDrawer());
192     setPageGraph(0);
193     view.setFitContentOnResize(true);
194     Graph2DViewMouseWheelZoomListener mouseWheelZoomListener = new Graph2DViewMouseWheelZoomListener();
195     mouseWheelZoomListener.setCenterZooming(false);
196     mouseWheelZoomListener.addToCanvas(view);
197 
198     final MultiPageOverview overview = new MultiPageOverview(view, pageBuilder);
199     overview.addViewMode(new OverviewViewMode());
200 
201     view.addViewMode(new PageViewMode() {
202       void setActive( final Graph2D graph, final Node node, final boolean active ) {
203         super.setActive(graph, node, active);
204         highlight(node, active);
205       }
206 
207       private void highlight( final Node node, final boolean active ) {
208         final int refPageNo = pageBuilder.getReferencedPageNo(node);
209         if (refPageNo > -1) {
210           final Graph2D g = overview.getGraph2D();
211           final Node page = find(g, Integer.toString(refPageNo + 1));
212           if (page != null) {
213             g.getRealizer(page).setFillColor(
214                     active
215                     ? DemoDefaults.DEFAULT_CONTRAST_COLOR
216                     : PAGE_BACKGROUND);
217             overview.updateView();
218           }
219         }
220       }
221 
222       private Node find( final Graph2D g, final String label ) {
223         for (NodeCursor nc = g.nodes(); nc.ok(); nc.next()) {
224           final NodeRealizer nr = g.getRealizer(nc.node());
225           if (nr.labelCount() > 0 && label.equals(nr.getLabelText())) {
226             return nc.node();
227           }
228         }
229         return null;
230       }
231     });
232     view.addViewMode(new NavigationMode());
233 
234     
235     //create help pane
236     JComponent helpPane = null;
237     if (helpFilePath != null) {
238       final URL url = getResource(helpFilePath);
239       if (url == null) {
240         System.err.println("Could not locate help file: " + helpFilePath);
241       } else {
242         helpPane = createHelpPane(url);
243       }
244     }
245 
246     //create demo gui
247     final JPanel rightPanel = new JPanel(new BorderLayout());
248     rightPanel.setMinimumSize(new Dimension(180, 240));
249     rightPanel.add(createOptionTable(oh), BorderLayout.NORTH);   
250 
251     if (helpPane != null) {
252       rightPanel.add(helpPane, BorderLayout.CENTER);
253     }
254 
255     final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, view, rightPanel);
256     splitPane.setBorder(BorderFactory.createEmptyBorder());
257     splitPane.setResizeWeight(1);
258     splitPane.setContinuousLayout(false);
259     contentPane.add(splitPane, BorderLayout.CENTER);
260 
261     final SearchableTreeViewPanel baseModelView = new SearchableTreeViewPanel(baseModel);
262     final JTree jt = baseModelView.getTree();
263     //add a navigational action to the tree
264     jt.addMouseListener(new MyDoubleClickListener());
265     jt.setCellRenderer(new MyTreeCellRenderer());
266 
267     final JPanel navPane = new JPanel(new BorderLayout());
268     navPane.add(baseModelView, BorderLayout.CENTER);
269     navPane.add(overview, BorderLayout.NORTH);
270 
271     final JSplitPane splitPane2 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, navPane, splitPane);
272     splitPane2.setBorder(BorderFactory.createEmptyBorder());
273     contentPane.add(splitPane2, BorderLayout.CENTER);
274   }
275 
276   /**
277    * Overwritten to prevent keyboard shortcuts from being registered multiple
278    * times. The demo constructor registers the demo's shortcuts.
279    */
280   protected void registerViewActions() {
281   }
282 
283   /**
284    * Overwritten to prevent the default view listeners from being registered.
285    * The demo constructor registers the demo's view listeners.
286    */
287   protected void registerViewListeners() {
288   }
289 
290   /**
291    * Overwritten to prevent the default view modes from being registered.
292    * The demo constructor registers the demo's view modes.
293    */
294   protected void registerViewModes() {
295   }
296 
297   /**
298    * Overwritten to prevent deletion of graph elements.
299    * @return <code>false</code>.
300    */
301   protected boolean isDeletionEnabled() {
302     return false;
303   }
304 
305   private JTextField getPageNumberTextField() {
306     if (pageNumberTextField == null) {
307       final JButton dummy = new JButton(new FirstPageAction());
308       final Dimension size = dummy.getPreferredSize();
309       pageNumberTextField = new JTextField();
310       pageNumberTextField.setHorizontalAlignment(JTextField.CENTER);
311       pageNumberTextField.setEditable(false);
312       pageNumberTextField.setColumns(11);
313       pageNumberTextField.setMaximumSize(new Dimension(80, size.height));
314       pageNumberTextField.setPreferredSize(new Dimension(80, size.height));
315     }
316     return pageNumberTextField;
317   }
318 
319   /**
320    * Creates a toolbar for this demo.
321    */
322   protected JToolBar createToolBar() {
323     final JToolBar toolBar = super.createToolBar();
324     toolBar.addSeparator();
325     toolBar.add(new FirstPageAction());
326     toolBar.add(new PreviousPageAction());
327     toolBar.add(getPageNumberTextField());
328     toolBar.add(new NextPageAction());
329     toolBar.add(new LastPageAction());
330     toolBar.addSeparator(new Dimension(10, 0));
331     toolBar.add(new GoBackAction());
332     toolBar.addSeparator();
333     toolBar.add(createActionControl(new AbstractAction("Layout", SHARED_LAYOUT_ICON) {
334       public void actionPerformed(final ActionEvent e) {
335         doLayoutInBackground();
336       }
337     }));
338     return toolBar;
339   }
340 
341   /**
342    * Overwritten to disable undo/redo because this is not an editable demo.
343    */
344   protected boolean isUndoRedoEnabled() {
345     return false;
346   }
347 
348   /**
349    * Overwritten to disable clipboard because this is not an editable demo.
350    */
351   protected boolean isClipboardEnabled() {
352     return false;
353   }
354 
355   protected JMenuBar createMenuBar() {
356     JMenuBar mb = new JMenuBar();
357 
358     JMenu file = new JMenu("File");
359     file.add(createLoadAction());
360     file.add(createSaveAction());
361     file.add(new SaveAction("Save Page Graph", view));
362     file.addSeparator();
363     file.add(new PrintAction());
364     file.addSeparator();
365     file.add(new ExitAction());
366     mb.add(file);
367 
368     JMenu sampleGraphs = new JMenu("Sample Graphs");
369     for (Iterator it = getLoadSampleActions(); it.hasNext();) {
370       sampleGraphs.add((Action) it.next());
371     }
372     mb.add(sampleGraphs);
373 
374     JMenu sampleSettings = new JMenu("Sample Settings");
375     sampleSettings.add(new SetOptionsAction(MultiPageLayoutOptionHandler.OPTIONS_NETWORK_SMALL_DISPLAY));
376     sampleSettings.add(new SetOptionsAction(MultiPageLayoutOptionHandler.OPTIONS_NETWORK_LARGE_DISPLAY));
377     sampleSettings.add(new SetOptionsAction(MultiPageLayoutOptionHandler.OPTIONS_CLASS_DIAGRAM_SMALL_DISPLAY));
378     sampleSettings.add(new SetOptionsAction(MultiPageLayoutOptionHandler.OPTIONS_CLASS_DIAGRAM_LARGE_DISPLAY));
379     mb.add(sampleSettings);
380 
381     return mb;
382   }
383 
384   private Iterator getLoadSampleActions() {
385     final String key = "MultiPageLayoutDemo.samples";
386     final Object samples = contentPane.getClientProperty(key);
387     if (samples instanceof List) {
388       return ((List) samples).iterator();
389     } else {
390       final ArrayList list = new ArrayList(3);
391       list.add(createLoadSampleActions(
392               "Pop Artists",
393               "resource/pop-artists.graphmlz"));
394       list.add(createLoadSampleActions(
395               "yFiles Classes",
396               "resource/yfiles-classes.graphmlz"));
397       list.add(createLoadSampleActions(
398               "yFiles Classes and Packages",
399               "resource/yfiles-classes-and-packages-nested.graphmlz"));
400       contentPane.putClientProperty(key, list);
401       return list.iterator();
402     }
403   }
404 
405   private Action createLoadSampleActions(final String name, final String resource) {
406     if (isResourceValid(resource)) {
407       return new LoadSampleGraphAction(name, resource);
408     } else {
409       throw new RuntimeException("Missing resource: " + resource);
410     }
411   }
412 
413   /**
414    * Determines whether or not the specified resource can be resolved.
415    * @param resource the name of the resource to check.
416    * @return <code>true</code> if the resource can be resolved;
417    * <code>false</code> otherwise.
418    */
419   private boolean isResourceValid(final String resource) {
420     return getResource(resource) != null;
421   }
422 
423   /**
424    * Creates a table control for the specified options.
425    * @param oh the options to display.
426    * @return a table control for the specified options.
427    */
428   private JComponent createOptionTable(OptionHandler oh) {
429     oh.setAttribute(
430             TableEditorFactory.ATTRIBUTE_INFO_POSITION,
431             TableEditorFactory.InfoPosition.NONE);
432     oh.setAttribute(
433             TableEditorFactory.ATTRIBUTE_USE_ITEM_NAME_AS_TOOLTIP_FALLBACK,
434             Boolean.TRUE);
435 
436     TableEditorFactory tef = new TableEditorFactory();
437     Editor editor = tef.createEditor(oh);
438 
439     JComponent optionComponent = editor.getComponent();
440     optionComponent.setPreferredSize(new Dimension(400, 240));
441     optionComponent.setMaximumSize(new Dimension(400, 240));
442     return optionComponent;
443   }
444 
445   /**
446    * Creates the application help pane.
447    *
448    * @param helpURL the URL of the HTML help page to display.
449    */
450   protected JComponent createHelpPane(final URL helpURL) {
451     try {
452       JEditorPane editorPane = new JEditorPane(helpURL);
453       editorPane.setEditable(false);
454       editorPane.setPreferredSize(new Dimension(250, 250));
455       return new JScrollPane(editorPane);
456     } catch (IOException e) {
457       e.printStackTrace();
458     }
459     return null;
460   }
461 
462   /**
463    * Creates an progress dialog for loading graphs in
464    * background threads.
465    * @param name the display name of the graph resource that is loaded.
466    * @return a dialog displaying a progress bar.
467    */
468   private JDialog createProgressDialog( final String name ) {
469     final JDialog jd = new JDialog(getFrame(), name, true);
470 
471     final JProgressBar jpb = new JProgressBar(0, 1);
472     jpb.setString(name);
473     jpb.setIndeterminate(true);
474 
475     final JLabel lbl = new JLabel(name);
476     final JPanel progressPane = new JPanel(new BorderLayout());
477     progressPane.add(lbl, BorderLayout.NORTH);
478     progressPane.add(jpb, BorderLayout.CENTER);
479     final JPanel contentPane = new JPanel(new FlowLayout());
480     contentPane.add(progressPane);
481 
482     jd.setContentPane(contentPane);
483     jd.pack();
484     jd.setLocationRelativeTo(null);
485     jd.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
486 
487     return jd;
488   }
489 
490   /**
491    * Retrieves the frame that displays the dmo GUI.
492    * @return the frame that displays the dmo GUI or <code>null</code> if
493    * no frame can be found.
494    */
495   private JFrame getFrame() {
496     final Window ancestor = SwingUtilities.getWindowAncestor(contentPane);
497     if (ancestor instanceof JFrame) {
498       return (JFrame) ancestor;
499     } else {
500       return null;
501     }
502   }
503 
504   /**
505    * Overwritten to load an initial multi-page graph <em>after</em> the
506    * graphical user interface has been completely created.
507    * @param rootPane the container to hold the demo's graphical user interface.
508    */
509   public void addContentTo( final JRootPane rootPane ) {
510     super.addContentTo(rootPane);
511     loadInitialGraph();
512   }
513 
514   /**
515    * Displays the page graph at the specified position in the list of pages.
516    * @param page the index of the page graph to display.
517    */
518   private void setPageGraph( final int page ) {
519     if (pageList != null && !pageList.isEmpty()) {
520       previousPageIndex = currentPageIndex;
521       currentPageIndex = Math.min(Math.max(page, 0), pageList.size() - 1);
522       final Graph2D currentPageGraph = (Graph2D) pageList.get(currentPageIndex);
523       getPageNumberTextField().setText((currentPageIndex + 1) + " / " + pageList.size());
524       view.setGraph2D(currentPageGraph);
525     } else {
526       view.setGraph2D(new Graph2D());
527     }
528     view.fitContent();
529     view.updateView();
530   }
531 
532   /**
533    * Jumps to the page graph that hold the node represented by the specified
534    * target ID.
535    * @param target the ID of a node in the destination page graph.
536    * @param focus the label of a node in the destination page graph that should
537    * be focused after the jump.
538    */
539   private void jump( final Object target, final String focus ) {
540     final LocationInfo locationInfo = getLocationInfo(target);
541     if (locationInfo != null) {
542       final double zoomLevel = view.getZoom();
543       final Graph2D oldGraph = view.getGraph2D();
544       oldGraph.setSelected(oldGraph.nodes(), false);
545       setPageGraph(locationInfo.pageNo);
546       view.getGraph2D().setSelected(locationInfo.node, true);
547 
548       //center matching node
549       final Graph2D newGraph = view.getGraph2D();
550       Node matchingNode = locationInfo.node; //default node
551       if (focus != null) {
552         for (NodeCursor nc = locationInfo.node.neighbors(); nc.ok(); nc.next()) {
553           final Node neighbor = nc.node();
554           if (focus.equals(newGraph.getLabelText(neighbor))) {
555             matchingNode = neighbor;
556             break;
557           }
558         }
559       }
560 
561       view.setCenter(newGraph.getCenterX(matchingNode), newGraph.getCenterY(matchingNode));
562       view.setZoom(zoomLevel);
563     }
564   }
565 
566   private LocationInfo getLocationInfo( final Object id ) {
567     return (LocationInfo) id2LocationInfo.get(id);
568   }
569 
570   /**
571    * Loads an initial multi-page graph.
572    */
573   protected void loadInitialGraph() {
574     EventQueue.invokeLater(new Runnable() {
575       public void run() {
576         final Iterator it = getLoadSampleActions();
577         if (it.hasNext()) {
578           ((Action) it.next()).actionPerformed(null);
579         }
580       }
581     });
582   }
583 
584   protected void loadGraph(String resourceString) {
585     loadGraph(baseModel, resourceString);
586   }
587 
588   /**
589    * Loads the specified graph structure resource in GraphML of compressed
590    * GraphML formant into the given graph instance.
591    * @param graph the graph instance to store the loaded data.
592    * @param resourceName the name of the graph resource to load.
593    */
594   private void loadGraph(final Graph2D graph, final String resourceName) {
595     URL resource = null;
596     final File file = new File(resourceName);
597     if (file.exists()) {
598       try {
599         resource = file.toURI().toURL();
600       } catch (MalformedURLException e) {
601         D.showError(e.getMessage());
602         return;
603       }
604     } else {
605       resource = getResource(resourceName);
606       if (resource == null) {
607         return;
608       }
609     }
610 
611     try {
612       final IOHandler ioh = resource.getFile().endsWith(".graphmlz") ? new ZipGraphMLIOHandler() : createGraphMLIOHandler();
613       
614       graph.clear();
615       ioh.read(graph, resource);
616     } catch (IOException ioe) {
617       D.showError("Unexpected error while loading resource \"" + resource + "\" due to " + ioe.getMessage());
618     }
619     graph.setURL(resource);
620   }
621 
622   /**
623    * Invokes the page layout and updates the view.
624    */
625   private void doLayoutInBackground() {
626     EventQueue.invokeLater(new Runnable() {
627       public void run() {
628         final JDialog pd = createProgressDialog("Do Layout");
629 
630         (new Thread(new Runnable() {
631           public void run() {
632             doLayout();
633 
634             EventQueue.invokeLater(new Runnable() {
635               public void run() {
636                 pd.setVisible(false);
637                 pd.dispose();
638 
639                 //reset page indices and set view to first page
640                 previousPageIndex = 0;
641                 setPageGraph(0);
642               }
643             });
644           }
645         })).start();
646 
647         pd.setVisible(true);
648       }
649     });
650   }
651 
652   private void doLayout() {
653     id2LocationInfo.clear();
654     if (oh.isUseSinglePageLayout()) {
655       (new Graph2DLayoutExecutor()).doLayout(baseModel, createCoreLayouter());
656       pageList = new ArrayList();
657       pageList.add((new GraphCopier(baseModel.getGraphCopyFactory())).copy(baseModel));
658     } else {
659       doMultiPageLayout();
660     }
661   }
662 
663   /**
664    * Configures and applies the multi-page layouter.
665    */
666   private void doMultiPageLayout() {
667     //map elements to ids
668     //multi-page layout requires unique, user-specified IDs for nodes, edges,
669     //node labels, and edge labels
670     final DataProvider idProvider = new DataProviderAdapter() {
671       public Object get(Object dataHolder) {
672         return dataHolder;
673       }
674     };
675     baseModel.addDataProvider(MultiPageLayouter.NODE_ID_DPKEY, idProvider);
676     baseModel.addDataProvider(MultiPageLayouter.EDGE_ID_DPKEY, idProvider);
677     baseModel.addDataProvider(MultiPageLayouter.NODE_LABEL_ID_DPKEY, idProvider);
678     baseModel.addDataProvider(MultiPageLayouter.EDGE_LABEL_ID_DPKEY, idProvider);
679 
680     final MultiPageLayouter mpl = new MultiPageLayouter(createCoreLayouter());
681 
682     //configure the layout algorithm
683     mpl.setLabelLayouterEnabled(oh.isLayout(MultiPageLayoutOptionHandler.LAYOUT_ORGANIC));
684     mpl.setPreferredMaximalDuration(oh.getMaximalDuration() * 1000);
685     mpl.setGroupMode(oh.getGroupingMode());
686     mpl.setEdgeBundleModeMask(oh.getSeparationMask());
687     final boolean addEdgeTypeDp =
688             (mpl.getEdgeBundleModeMask() &
689              MultiPageLayouter.EDGE_BUNDLE_DISTINGUISH_TYPES) != 0;
690     if (addEdgeTypeDp) {
691       baseModel.addDataProvider(
692               MultiPageLayouter.EDGE_TYPE_DPKEY,
693               new DataProviderAdapter() {
694         public Object get(Object dataHolder) {
695           return new EdgeType(baseModel.getRealizer((Edge) dataHolder));
696         }
697       });
698     }
699 
700     mpl.setMaxPageSize(new YDimension(oh.getMaximumWidth(), oh.getMaximumHeight()));
701 
702     final SimpleLayoutCallback callback = new SimpleLayoutCallback();
703     mpl.setLayoutCallback(callback);
704 
705     try {
706       //calculate a new multi-page layout
707       (new Graph2DLayoutExecutor()).doLayout(baseModel, mpl);
708 
709       //transform the layout result into a list of Graph2D instances
710       pageList = createPageViews(callback.pop());
711     } finally {
712       //clean-up: remove previously registered data providers
713       if (addEdgeTypeDp) {
714         baseModel.removeDataProvider(MultiPageLayouter.EDGE_TYPE_DPKEY);
715       }
716 
717       baseModel.removeDataProvider(MultiPageLayouter.EDGE_LABEL_ID_DPKEY);
718       baseModel.removeDataProvider(MultiPageLayouter.NODE_LABEL_ID_DPKEY);
719       baseModel.removeDataProvider(MultiPageLayouter.EDGE_ID_DPKEY);
720       baseModel.removeDataProvider(MultiPageLayouter.NODE_ID_DPKEY);
721     }
722   }
723 
724   /**
725    * Creates a configured layout algorithm to be used as core layout strategy
726    * in a multi-page layout calculation.
727    * @return a ready-to-use layout algorithm.
728    */
729   private Layouter createCoreLayouter() {
730     if (oh.isLayout(MultiPageLayoutOptionHandler.LAYOUT_HIERARCHIC)) {
731       final IncrementalHierarchicLayouter ihl = new IncrementalHierarchicLayouter();
732       ihl.setConsiderNodeLabelsEnabled(true);
733       ihl.setIntegratedEdgeLabelingEnabled(true);
734       ihl.setOrthogonallyRouted(true);
735       ihl.setConsiderNodeLabelsEnabled(true);
736       return ihl;
737     } else if (oh.isLayout(MultiPageLayoutOptionHandler.LAYOUT_ORGANIC)) {
738       final SmartOrganicLayouter sol = new SmartOrganicLayouter();
739       sol.setMinimalNodeDistance(10);
740       sol.setDeterministic(true);
741       sol.setMultiThreadingAllowed(true);
742       return sol;
743     } else if (oh.isLayout(MultiPageLayoutOptionHandler.LAYOUT_COMPACT_ORTHOGONAL)) {
744       return new CompactOrthogonalLayouter();
745     } else {
746       return new OrthogonalLayouter();
747     }
748   }
749 
750   /**
751    * Creates a page view for each specified page layout.
752    * @param layout the page layouts.
753    * @return the page views, a list of {@link Graph2D} instances.
754    */
755   private List createPageViews( final MultiPageLayout layout ) {
756     final ArrayList newPageList = new ArrayList();
757     pageBuilder.reset(baseModel, layout);
758     for (int i = 0, pc = layout.pageCount(); i < pc; ++i) {
759       final Graph2D subgraph = pageBuilder.createPageView(new Graph2D(), i);
760       for (NodeCursor nc = subgraph.nodes(); nc.ok(); nc.next()) {
761         final Node n = nc.node();
762         id2LocationInfo.put(pageBuilder.getNodeId(n), new LocationInfo(i, n));
763       }
764       newPageList.add(subgraph);
765     }
766     return newPageList;
767   }
768 
769   protected Action createLoadAction() {
770     return new LoadAction();
771   }
772 
773   protected Action createSaveAction() {
774     return new SaveAction("Save Model Graph", baseModel);
775   }
776 
777   public static void main(String[] args) {
778     EventQueue.invokeLater(new Runnable() {
779       public void run() {
780         Locale.setDefault(Locale.ENGLISH);
781         initLnF();
782         (new MultiPageLayoutDemo("resource/multipagelayouthelp.html")).start("Multi-Page Layout Demo");
783       }
784     });
785   }
786 
787 
788 
789   /**
790    * Class that represents the type of an edge.
791    * The edge type of two edges is equal if the corresponding realizers have
792    * the same line type, line color and source/target arrow type.
793    */
794   static final class EdgeType {
795     byte sourceArrowType;
796     byte targetArrowType;
797     Color lineColor;
798     LineType lineType;
799 
800     EdgeType(final EdgeRealizer realizer) {
801       sourceArrowType = realizer.getSourceArrow().getType();
802       targetArrowType = realizer.getTargetArrow().getType();
803       lineColor = realizer.getLineColor();
804       lineType = realizer.getLineType();
805     }
806 
807     public boolean equals( Object o ) {
808       if (this == o) {
809         return true;
810       }
811       if (o == null || getClass() != o.getClass()) {
812         return false;
813       }
814 
815       EdgeType edgeType = (EdgeType) o;
816 
817       if (sourceArrowType != edgeType.sourceArrowType) {
818         return false;
819       }
820       if (targetArrowType != edgeType.targetArrowType) {
821         return false;
822       }
823       if (lineColor != null ? !lineColor.equals(edgeType.lineColor) : edgeType.lineColor != null) {
824         return false;
825       }
826       if (lineType != null ? !lineType.equals(edgeType.lineType) : edgeType.lineType != null) {
827         return false;
828       }
829 
830       return true;
831     }
832 
833     public int hashCode() {
834       int result = (int) sourceArrowType;
835       result = 31 * result + (int) targetArrowType;
836       result = 31 * result + (lineColor != null ? lineColor.hashCode() : 0);
837       result = 31 * result + (lineType != null ? lineType.hashCode() : 0);
838       return result;
839     }
840   }
841 
842 
843 
844   /**
845    * Customized click listener for the tree view.
846    * A click on an element of the tree view causes a jump to the page that contains the clicked element.
847    */
848   class MyDoubleClickListener extends MouseAdapter {
849     public void mouseClicked(MouseEvent e) {
850       final JTree tree = (JTree) e.getSource();
851       if (e.getClickCount() == 2) {
852         final TreePath path = tree.getPathForLocation(e.getX(), e.getY());
853         if (path != null) {
854           final Object last = path.getLastPathComponent();
855           if (last instanceof Node) {
856             jump(last, baseModel.getLabelText((Node) last));
857           }
858         }
859       }
860     }
861   }
862 
863   class MyTreeCellRenderer extends DefaultTreeCellRenderer {
864     public Component getTreeCellRendererComponent(
865             JTree tree,
866             Object value,
867             boolean sel,
868             boolean expanded,
869             boolean leaf,
870             int row,
871             boolean hasFocus
872     ) {
873       super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
874       if (value instanceof Graph) {
875         setIcon(null);
876         setText("Nodes: " + ((Graph) value).nodeCount() + "\t Edges: " + ((Graph) value).edgeCount());
877       }
878       return this;
879     }
880   }
881 
882 
883   /**
884    * Abstract base class for loading graphs in a background thread.
885    */
886   abstract class AbstractLoadAction extends AbstractAction {
887     AbstractLoadAction( final String name ) {
888       super(name);
889     }
890 
891     /**
892      * Loads a model graph in a background thread.
893      * @param resource the graph resource to load as model graph.
894      * @param name the display name for the graph resource.
895      */
896     void load( final String resource, final String name ) {
897       EventQueue.invokeLater(new Runnable() {
898         public void run() {
899           final JDialog pd = createProgressDialog("Loading " + name);
900 
901           view.setGraph2D(new Graph2D());
902           view.fitContent();
903           view.updateView();
904 
905           (new Thread(new Runnable() {
906             public void run() {
907               loadGraph(baseModel, resource);
908               EventQueue.invokeLater(new Runnable() {
909                 public void run() {
910                   pd.setVisible(false);
911                   pd.dispose();
912 
913                   doLayoutInBackground();
914                 }
915               });
916             }
917           })).start();
918 
919           pd.setVisible(true);
920         }
921       });
922     }
923   }
924 
925   /**
926    * Loads a sample graph using the given resource.
927    */
928   protected class LoadSampleGraphAction extends AbstractLoadAction {
929     private final String name;
930     private final String resource;
931 
932     LoadSampleGraphAction(final String name, final String resource) {
933       super(name);
934       this.name = name;
935       this.resource = resource;
936     }
937 
938     public void actionPerformed(ActionEvent e) {
939       load(resource, name);
940     }
941   }
942 
943   /** Action that loads the current graph from a file in GraphML format. */
944   protected class LoadAction extends AbstractLoadAction {
945     JFileChooser chooser;
946 
947     public LoadAction() {
948       super("Load Model Graph");
949       chooser = null;
950     }
951 
952     public void actionPerformed(ActionEvent e) {
953       if (chooser == null) {
954         chooser = new JFileChooser();
955         chooser.setAcceptAllFileFilterUsed(false);
956         chooser.addChoosableFileFilter(new FileFilter() {
957           public boolean accept(File f) {
958             return f.isDirectory() || f.getName().endsWith(".graphml");
959           }
960 
961           public String getDescription() {
962             return "GraphML Format (.graphml)";
963           }
964         });
965         chooser.addChoosableFileFilter(new FileFilter() {
966           public boolean accept(File f) {
967             return f.isDirectory() || f.getName().endsWith(".graphmlz");
968           }
969 
970           public String getDescription() {
971             return "Zipped GraphML Format (.graphmlz)";
972           }
973         });
974       }
975       if (chooser.showOpenDialog(contentPane) == JFileChooser.APPROVE_OPTION) {
976         try {
977           final URL resource = chooser.getSelectedFile().toURI().toURL();
978           final String ln = resource.getFile();
979           load(ln, (new File(ln)).getName());
980         } catch (MalformedURLException mue) {
981           mue.printStackTrace();
982         }
983       }
984     }
985   }
986 
987   /** Action that saves the current graph to a file in GraphML format. */
988   protected class SaveAction extends AbstractAction {
989     private JFileChooser chooser;
990     private Graph2D graph;
991     private Graph2DView graphView;
992 
993     /**
994      * Initializes a new <code>SaveAction</code> instance.
995      * @param name the display name of the action.
996      * @param graph the graph to save.
997      */
998     public SaveAction( final String name, final Graph2D graph ) {
999       super(name);
1000      this.graph = graph;
1001      this.graphView = null;
1002    }
1003
1004    /**
1005     * Initializes a new <code>SaveAction</code> instance.
1006     * @param name the display name of the action.
1007     * @param graphView the view whose graph is saved.
1008     */
1009    public SaveAction( final String name, final Graph2DView graphView ) {
1010      super(name);
1011      this.graph = null;
1012      this.graphView = graphView;
1013    }
1014
1015    private Graph2D getGraph() {
1016      if(graph != null) {
1017        return graph;
1018      } else if(graphView != null) {
1019        return graphView.getGraph2D();
1020      } else {
1021        return null;
1022      }
1023    }
1024
1025    private void setFileFilter( final JFileChooser chooser, final File file ) {
1026      FileFilter[] filters = chooser.getChoosableFileFilters();
1027      for (int i = 0; i < filters.length; i++) {
1028        if (filters[i].accept(file)) {
1029          chooser.setFileFilter(filters[i]);
1030          return;
1031        }
1032      }
1033    }
1034
1035    public void actionPerformed( final ActionEvent e ) {
1036      if (chooser == null) {
1037        chooser = new JFileChooser();
1038        chooser.setAcceptAllFileFilterUsed(false);
1039        chooser.addChoosableFileFilter(new FileFilter() {
1040          public boolean accept(File f) {
1041            return f.isDirectory() || f.getName().endsWith(".graphml");
1042          }
1043
1044          public String getDescription() {
1045            return "GraphML Format (.graphml)";
1046          }
1047        });
1048        chooser.addChoosableFileFilter(new FileFilter() {
1049          public boolean accept(File f) {
1050            return f.isDirectory() || f.getName().endsWith(".graphmlz");
1051          }
1052
1053          public String getDescription() {
1054            return "Zipped GraphML Format (.graphmlz)";
1055          }
1056        });
1057      }
1058
1059      final URL url = view.getGraph2D().getURL();
1060      if (url != null && "file".equals(url.getProtocol())) {
1061        try {
1062          final File file = new File(new URI(url.toString()));
1063          chooser.setSelectedFile(file);
1064          setFileFilter(chooser, file);
1065        } catch (URISyntaxException use) {
1066          // ignore
1067        }
1068      }
1069
1070      if (chooser.showSaveDialog(contentPane) == JFileChooser.APPROVE_OPTION) {
1071        IOHandler ioh;
1072        String name = chooser.getSelectedFile().toString();
1073        final FileFilter filter = chooser.getFileFilter();
1074        if (filter.accept(new File("file.graphml"))) {
1075          if (!name.endsWith(".graphml")) {
1076            name += ".graphml";
1077          }
1078          ioh = createGraphMLIOHandler();
1079        } else {
1080          if (!name.endsWith(".graphmlz")) {
1081            name += ".graphmlz";
1082          }
1083          ioh = new ZipGraphMLIOHandler();
1084        }
1085
1086        try {
1087          ioh.write(getGraph(), name);
1088        } catch (IOException ioe) {
1089          D.show(ioe);
1090        }
1091      }
1092    }
1093  }
1094
1095  /**
1096   * Action that switches the view to the first page
1097   */
1098  private final class FirstPageAction extends AbstractAction {
1099    public FirstPageAction() {
1100      super("<<");
1101      putValue(SHORT_DESCRIPTION, "Go to first page");
1102    }
1103
1104    public void actionPerformed(ActionEvent e) {
1105      setPageGraph(0);
1106    }
1107  }
1108
1109  /**
1110   * Action that switches the view to the last page
1111   */
1112  private final class LastPageAction extends AbstractAction {
1113    public LastPageAction() {
1114      super(">>");
1115      putValue(SHORT_DESCRIPTION, "Go to last page");
1116    }
1117
1118    public void actionPerformed(ActionEvent e) {
1119      setPageGraph(pageList == null ? 0 : pageList.size()-1);
1120    }
1121  }
1122
1123  /**
1124   * Action that switches the view to the next page
1125   */
1126  private final class NextPageAction extends AbstractAction {
1127    public NextPageAction() {
1128      super(">");
1129      putValue(SHORT_DESCRIPTION, "Go to next page");
1130    }
1131
1132    public void actionPerformed(ActionEvent e) {
1133      setPageGraph(currentPageIndex + 1);
1134    }
1135  }
1136
1137  /**
1138   * Action that switches the view to the previous page
1139   */
1140  private final class PreviousPageAction extends AbstractAction {
1141    public PreviousPageAction() {
1142      super("<");
1143      putValue(SHORT_DESCRIPTION, "Go to previous page");
1144    }
1145
1146    public void actionPerformed(ActionEvent e) {
1147      setPageGraph(currentPageIndex - 1);
1148    }
1149  }
1150
1151  /**
1152   * Action that switches the view to the last visited page
1153   */
1154  private final class GoBackAction extends AbstractAction {
1155    public GoBackAction() {
1156      super("Go Back");
1157      putValue(SHORT_DESCRIPTION, "Go to last visited page");
1158    }
1159
1160    public void actionPerformed(ActionEvent e) {
1161      setPageGraph(previousPageIndex);
1162    }
1163  }
1164
1165  /**
1166   * Action that applies a pre-defined set of options for multi-page layout.
1167   */
1168  private final class SetOptionsAction extends AbstractAction {
1169    private final MultiPageLayoutOptionHandler.OptionSet set;
1170
1171    SetOptionsAction( final MultiPageLayoutOptionHandler.OptionSet set ) {
1172      super(set.getName());
1173      this.set = set;
1174    }
1175
1176    public void actionPerformed( final ActionEvent e ) {
1177      set.apply(oh);
1178      doLayoutInBackground();
1179    }
1180  }
1181
1182
1183
1184  /**
1185   * Background-Drawable that draws the page (a filled rectangle with size equals to the maximum page size).
1186   */
1187  class PageBorderDrawer implements Drawable {
1188    public Rectangle getBounds() {
1189      final Rectangle2D bnds = getPageBounds();
1190      if (oh.isDrawingPage()) {
1191        final int margin = 5;
1192        bnds.setFrame(
1193                bnds.getX() - margin,
1194                bnds.getY() - margin,
1195                bnds.getWidth() + 2*margin,
1196                bnds.getHeight() + 2*margin);
1197        return bnds.getBounds();
1198      } else {
1199        return new Rectangle(
1200                (int) Math.floor(bnds.getCenterX()),
1201                (int) Math.floor(bnds.getCenterY()),
1202                1,
1203                1);
1204      }
1205    }
1206
1207    public void paint(Graphics2D g) {
1208      if (oh.isDrawingPage()) {
1209        final Rectangle2D bBoxGraph = getPageBounds();
1210        final Color colorBkp = g.getColor();
1211        g.setColor(PAGE_BACKGROUND);
1212        g.fill(bBoxGraph);
1213        g.setColor(Color.DARK_GRAY);
1214        g.draw(bBoxGraph);
1215        g.setColor(colorBkp);
1216      }
1217    }
1218
1219    private Rectangle2D getPageBounds() {
1220      final Graph2D graph = view.getGraph2D();
1221      final Rectangle2D bBoxGraph = LayoutTool.getBoundingBox(graph, graph.nodes(), graph.edges(), false);
1222      final double cx = bBoxGraph.getCenterX();
1223      final double cy = bBoxGraph.getCenterY();
1224      final int maxPageWidth = oh.getMaximumWidth();
1225      final int maxPageHeight = oh.getMaximumHeight();
1226      bBoxGraph.setFrame(
1227              cx - maxPageWidth * 0.5,
1228              cy - maxPageHeight * 0.5,
1229              maxPageWidth,
1230              maxPageHeight);
1231      return bBoxGraph;
1232    }
1233  }
1234
1235
1236
1237  /**
1238   * Used to store the location (page number) for node elements.
1239   */
1240  private static class LocationInfo {
1241    int pageNo;
1242    Node node;
1243
1244    LocationInfo(final int pageNo, final Node node) {
1245      this.pageNo = pageNo;
1246      this.node = node;
1247    }
1248  }
1249
1250
1251  /**
1252   * Stores the result of a multi-page layout calculation.
1253   */
1254  private static class SimpleLayoutCallback implements LayoutCallback {
1255    private MultiPageLayout result;
1256
1257    public void layoutDone( final MultiPageLayout result ) {
1258      this.result = result;
1259    }
1260
1261    MultiPageLayout pop() {
1262      final MultiPageLayout result = this.result;
1263      this.result = null;
1264      return result;
1265    }
1266  }
1267
1268
1269  /**
1270   * Abstract base class for {@link ViewMode}s that supports jumping to other
1271   * page graphs when clicking a linked node. 
1272   */
1273  abstract static class LinkViewMode extends ViewMode {
1274    private Node hitNode;
1275
1276    /**
1277     * Provides visual feedback similar to hyperlink activation in web browsers
1278     * when the mouse is moved over a node that can be clicked to jump to
1279     * another page graph.
1280     * @param x the x-coordinate of the mouse event in world coordinates.
1281     * @param y the y-coordinate of the mouse event in world coordinates.
1282     */
1283    public void mouseMoved( final double x, final double y ) {
1284      final Graph2DView view = this.view;
1285      final Graph2D graph = view.getGraph2D();
1286      final HitInfo hitInfo = getHitInfo(x, y);
1287      final Node oldHitNode = hitNode;
1288      hitNode = hitInfo.getHitNode();
1289      if (hitNode != oldHitNode) {
1290        if (oldHitNode != null) {
1291          setActive(graph, oldHitNode, false);
1292        }
1293
1294        if (hitNode != null && isLink(hitNode)) {
1295          setToolTipText(hitNode);
1296          setActive(graph, hitNode, true);
1297          view.setViewCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
1298        } else {
1299          reset(view);
1300        }
1301        view.updateView();
1302      }
1303    }
1304
1305    public void mouseExited() {
1306      final Node oldHitNode = hitNode;
1307      if (oldHitNode != null) {
1308        hitNode = null;
1309        setActive(view.getGraph2D(), oldHitNode, false);
1310        reset(view);
1311        view.updateView();
1312      }
1313    }
1314
1315    /**
1316     * Marks the specified node as active in a sense similar to hyperlink
1317     * activation in web browsers.
1318     * @param graph the graph that holds the node to mark.
1319     * @param node the node to mark as active.
1320     * @param active the node's new active state.
1321     */
1322    void setActive( final Graph2D graph, final Node node, final boolean active ) {
1323      final NodeRealizer nr = graph.getRealizer(node);
1324      if (nr.labelCount() > 0) {
1325        nr.getLabel().setUnderlinedTextEnabled(active);
1326      }
1327    }
1328
1329    void reset( final Graph2DView view ) {
1330      view.setViewCursor(Cursor.getDefaultCursor());
1331      view.setToolTipText(null);
1332    }
1333
1334    /**
1335     * Checks the specified coordinates for a node hit.
1336     * @param x the x-coordinate of the location to check.
1337     * @param y the y-coordinate of the location to check.
1338     * @return the node hit information corresponding to the specified location.
1339     */
1340    public HitInfo getHitInfo( final double x, final double y ) {
1341      final HitInfo info = view.getHitInfoFactory().createHitInfo(x, y, Graph2DTraversal.NODES, true);
1342      setLastHitInfo(info);
1343      return info;
1344    }
1345
1346    /**
1347     * Determines whether or not the specified node may be clicked to jump
1348     * to another page graph.
1349     * @param node the node to check.
1350     * @return <code>true</code> if the specified node may be clicked to jump
1351     * to another page graph; <code>false</code> otherwise.
1352     */
1353    abstract boolean isLink( Node node );
1354
1355    /**
1356     * Sets the tool tip text of the graph view that is associated to this view
1357     * mode to display detail information for the specified node.
1358     * @param node the node whose details are displayed.
1359     */
1360    abstract void setToolTipText( Node node );
1361  }
1362
1363  /**
1364   * Supports jumping to page graphs from the multi-page overview component.
1365   */
1366  class OverviewViewMode extends LinkViewMode {
1367    /**
1368     * Jumps to another page graph if a linked node is clicked.
1369     * @param x the x-coordinate of the mouse event in world coordinates.
1370     * @param y the y-coordinate of the mouse event in world coordinates.
1371     */
1372    public void mouseClicked( final double x, final double y ) {
1373      final int page = getPage(x, y);
1374      if (page > 0 && page - 1 != currentPageIndex) {
1375        reset(view);
1376        setPageGraph(page - 1);
1377      }
1378    }
1379
1380    /**
1381     * Determines whether or not the specified node is a link to another page
1382     * graph.
1383     * @param node the node to check.
1384     * @return <code>true</code> if the specified node's default label is
1385     * a valid page number; <code>false</code> otherwise.
1386     */
1387    boolean isLink( final Node node ) {
1388      final int page = getPage(node);
1389      return page > 0 && page - 1 != currentPageIndex;
1390    }
1391
1392    /**
1393     * Sets the tool tip text <em>Go to page x</em> where <code>x</code> is
1394     * the index of the page graph that is linked by the specified node.
1395     * @param node the node whose details are displayed.
1396     */
1397    void setToolTipText( final Node node ) {
1398      view.setToolTipText("Go to page " + getPage(node));
1399    }
1400
1401    /**
1402     * Determines whether or not the specified location lies on a node that
1403     * links to another page graph.
1404     * @param x the x-coordinate of the hit location.
1405     * @param y the y-coordinate of the hit location.
1406     * @return the index of a page graph that is linked from the specified
1407     * location or <code>-1</code> if the specified location does not link to
1408     * another page graph.
1409     */
1410    private int getPage( final double x, final double y ) {
1411      final HitInfo hitInfo = getHitInfo(x, y);
1412      if (hitInfo.hasHitNodes()) {
1413        return getPage(hitInfo.getHitNode());
1414      } else {
1415        return -1;
1416      }
1417    }
1418
1419    /**
1420     * Determines whether or not the specified node links to another page graph.
1421     * @param node the node to check.
1422     * @return the index of a page graph that is linked to the specified
1423     * node or <code>-1</code> if the specified node does not link to
1424     * another page graph.
1425     */
1426    private int getPage( final Node node ) {
1427      final NodeRealizer nr = getGraph2D().getRealizer(node);
1428      if (nr.labelCount() > 0) {
1429        try {
1430          return Integer.parseInt(nr.getLabelText());
1431        } catch (NumberFormatException e) {
1432          return -1;
1433        }
1434      }
1435      return -1;
1436    }
1437  }
1438
1439  /**
1440   * Supports jumping to page graphs for
1441   * {@link NodeInfo#TYPE_CONNECTOR connector},
1442   * {@link NodeInfo#TYPE_PROXY proxy}, and
1443   * {@link NodeInfo#TYPE_PROXY_REFERENCE proxy reference} nodes in the demo's
1444   * main view component.
1445   */
1446  class PageViewMode extends LinkViewMode {
1447    /**
1448     * Jumps to another page graph if a
1449     * {@link NodeInfo#TYPE_CONNECTOR connector},
1450     * {@link NodeInfo#TYPE_PROXY proxy}, or
1451     * {@link NodeInfo#TYPE_PROXY_REFERENCE proxy reference} node is clicked.
1452     * @param x the x-coordinate of the mouse event in world coordinates.
1453     * @param y the y-coordinate of the mouse event in world coordinates.
1454     */
1455    public void mouseClicked( final double x, final double y ) {
1456      if (lastClickEvent.getButton() == MouseEvent.BUTTON1) {
1457        final Graph2DView view = this.view;
1458        final HitInfo info = view.getHitInfoFactory().createHitInfo(x, y,
1459            Graph2DTraversal.NODES, true);
1460        if (info.hasHitNodes()) {
1461          reset(view);
1462          final Node hitNode = info.getHitNode();
1463          jump(pageBuilder.getReferencingNodeId(hitNode),
1464               view.getGraph2D().getLabelText(hitNode));
1465        }
1466      }
1467    }
1468
1469    /**
1470     * Determines whether or not the specified node is a link to another page
1471     * graph.
1472     * @param node the node to check.
1473     * @return <code>true</code> if the specified node references another node;
1474     * <code>false</code> otherwise.
1475     */
1476    boolean isLink( final Node node ) {
1477      return pageBuilder.getReferencingNodeId(node) != null;
1478    }
1479
1480    /**
1481     * Sets the tool tip text of the graph view that is associated to this view
1482     * mode to display detail information for the specified node.
1483     * @param node the node whose details are displayed.
1484     */
1485    void setToolTipText( final Node node ) {
1486      final Graph2DView view = this.view;
1487      final int pageNo = getLocationInfo(
1488              pageBuilder.getReferencingNodeId(node)).pageNo + 1;
1489      switch (pageBuilder.getNodeType(node)) {
1490        case NodeInfo.TYPE_PROXY:
1491          view.setToolTipText(
1492                  "<html>" +
1493                  "<h3>Proxy</h3>" +
1494                  "<p>Transfers to the original node on page " + pageNo +
1495                  ".</p></html>");
1496          break;
1497        case NodeInfo.TYPE_PROXY_REFERENCE:
1498          view.setToolTipText(
1499                  "<html>" +
1500                  "<h3>Proxy Reference</h3>" +
1501                  "<p>Transfers to the proxy on page " + pageNo +
1502                  ".</p></html>");
1503          break;
1504        case NodeInfo.TYPE_CONNECTOR:
1505          view.setToolTipText(
1506                  "<html>" +
1507                  "<h3>Connector</h3>" +
1508                  "<p>Transfers to the opposite node of the connecting edge" +
1509                  " on page " + pageNo + ".</p></html>");
1510          break;
1511        default:
1512          throw new IllegalStateException();
1513      }
1514    }
1515  }
1516}
1517