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