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.view.graphexplorer;
29  
30  import java.awt.BorderLayout;
31  import java.awt.Color;
32  import java.awt.Component;
33  import java.awt.Dimension;
34  import java.awt.EventQueue;
35  import java.awt.FlowLayout;
36  import java.awt.Graphics2D;
37  import java.awt.Window;
38  import java.awt.dnd.DnDConstants;
39  import java.awt.dnd.DragGestureEvent;
40  import java.awt.dnd.DragGestureListener;
41  import java.awt.dnd.DragSource;
42  import java.awt.event.ActionEvent;
43  import java.awt.event.ActionListener;
44  import java.awt.event.MouseAdapter;
45  import java.awt.event.MouseEvent;
46  import java.awt.geom.Rectangle2D;
47  import java.io.File;
48  import java.io.IOException;
49  import java.net.MalformedURLException;
50  import java.net.URI;
51  import java.net.URISyntaxException;
52  import java.net.URL;
53  import java.util.ArrayList;
54  import java.util.Collections;
55  import java.util.Comparator;
56  import java.util.HashMap;
57  import java.util.HashSet;
58  import java.util.Iterator;
59  import java.util.List;
60  import java.util.Locale;
61  import java.util.Map;
62  import java.util.Set;
63  
64  import javax.swing.AbstractAction;
65  import javax.swing.Action;
66  import javax.swing.ActionMap;
67  import javax.swing.BorderFactory;
68  import javax.swing.InputMap;
69  import javax.swing.JComponent;
70  import javax.swing.JDialog;
71  import javax.swing.JEditorPane;
72  import javax.swing.JFileChooser;
73  import javax.swing.JFrame;
74  import javax.swing.JLabel;
75  import javax.swing.JMenu;
76  import javax.swing.JMenuBar;
77  import javax.swing.JPanel;
78  import javax.swing.JProgressBar;
79  import javax.swing.JRootPane;
80  import javax.swing.JScrollPane;
81  import javax.swing.JSplitPane;
82  import javax.swing.JToolBar;
83  import javax.swing.JTree;
84  import javax.swing.SwingUtilities;
85  import javax.swing.Timer;
86  import javax.swing.filechooser.FileFilter;
87  import javax.swing.tree.DefaultTreeCellRenderer;
88  import javax.swing.tree.TreePath;
89  
90  import demo.view.DemoBase;
91  
92  import y.algo.Bfs;
93  import y.anim.AnimationEvent;
94  import y.anim.AnimationFactory;
95  import y.anim.AnimationListener;
96  import y.anim.AnimationPlayer;
97  import y.anim.CompositeAnimationObject;
98  import y.base.DataProvider;
99  import y.base.Edge;
100 import y.base.EdgeCursor;
101 import y.base.Graph;
102 import y.base.Node;
103 import y.base.NodeCursor;
104 import y.base.NodeList;
105 import y.base.NodeMap;
106 import y.geom.YPoint;
107 import y.geom.YRectangle;
108 import y.io.IOHandler;
109 import y.io.ZipGraphMLIOHandler;
110 import y.layout.LayoutTool;
111 import y.util.Comparators;
112 import y.util.D;
113 import y.util.DataProviderAdapter;
114 import y.util.DefaultMutableValue2D;
115 import y.util.GraphCopier;
116 import y.util.Maps;
117 import y.view.DefaultGraph2DRenderer;
118 import y.view.DropSupport;
119 import y.view.EditMode;
120 import y.view.Graph2D;
121 import y.view.Graph2DCopyFactory;
122 import y.view.Graph2DSelectionEvent;
123 import y.view.Graph2DSelectionListener;
124 import y.view.Graph2DTraversal;
125 import y.view.Graph2DUndoManager;
126 import y.view.Graph2DView;
127 import y.view.Graph2DViewActions;
128 import y.view.Graph2DViewMouseWheelZoomListener;
129 import y.view.HitInfo;
130 import y.view.ModelViewManager;
131 import y.view.NodeRealizer;
132 import y.view.ViewAnimationFactory;
133 import y.view.hierarchy.AutoBoundsFeature;
134 import y.view.hierarchy.DefaultHierarchyGraphFactory;
135 import y.view.hierarchy.GroupNodeRealizer;
136 import y.view.hierarchy.HierarchyManager;
137 
138 /**
139  * Demonstrates how to successively explore large graphs.
140  * <p>
141  * Things to try:
142  * </p>
143  * <ul>
144  * <li>Double-click a node in the view to show connected nodes/edges.</li>
145  * <li>Double-click an item in the tree to add the corresponding node to the
146  * view.</li>
147  * <li>Move a node around, then double-click the node while holding
148  * <code>CTRL</code> to reposition its neighbors.</li>
149  * </ul>
150  *
151  */
152 public class GraphExplorerDemo extends DemoBase {
153   private static final int DURATION_ADD = 500;
154 
155   private final Graph2D baseModel;
156   private final ModelViewManager manager;
157   private final InclusionFilter inclusionFilter;
158   private final GraphExplorerOptionHandler oh;
159   private final Graph2DUndoManager undoManager;
160 
161 
162   public GraphExplorerDemo() {
163     this(null);
164   }
165 
166   public GraphExplorerDemo( final String helpFilePath ) {
167     baseModel = view.getGraph2D();
168     new HierarchyManager(baseModel);
169 
170     manager = ModelViewManager.getInstance(baseModel);
171     manager.setInnerGraphBindingAllowed(true);
172     inclusionFilter = new InclusionFilter();
173 
174 
175     final SearchableTreeViewPanel baseModelView =
176             new SearchableTreeViewPanel(baseModel);
177     final JTree jt = baseModelView.getTree();
178     //add a navigational action to the tree
179     jt.addMouseListener(new MyDoubleClickListener());
180     jt.setCellRenderer(new MyTreeCellRenderer());
181 
182 
183     view.setPreferredSize(new Dimension(480, 640));
184     view.setGraph2D((Graph2D) manager.createViewGraph(new MyGraphCopyFactory(), inclusionFilter, false, false));
185     view.setFitContentOnResize(true);
186     view.setGraph2DRenderer(new MyGraph2DRenderer());
187     new Graph2DViewMouseWheelZoomListener().addToCanvas(view);
188     view.getGraph2D().addGraph2DSelectionListener(new SelectionTrigger(baseModel));
189     view.getGraph2D().addDataProvider(
190             EditMode.ORTHOGONAL_ROUTING_DPKEY,
191             new DataProviderAdapter() {
192               public boolean getBool( Object edge ) {
193                 final byte id = oh.getLayoutId();
194                 return id == GraphExplorerOptionHandler.ID_LAYOUT_HIERARCHIC ||
195                        id == GraphExplorerOptionHandler.ID_LAYOUT_ORTHOGONAL;
196               }
197             });
198 
199     //add actions to view
200     final Graph2DViewActions actions = new Graph2DViewActions(view);
201     final ActionMap actionMap = view.getCanvasComponent().getActionMap();
202     actionMap.put(Graph2DViewActions.DELETE_SELECTION, new MyDeleteSelectionAction());
203     final InputMap inputMap = actions.createDefaultInputMap(actionMap);
204     view.getCanvasComponent().setActionMap(actionMap);
205     view.getCanvasComponent().setInputMap(JComponent.WHEN_FOCUSED, inputMap);
206 
207     //create and set customized edit mode
208     final EditMode filteredViewMode = new EditMode() {
209       public void mouseClicked(final double x, final double y) {
210         if (SwingUtilities.isLeftMouseButton(lastClickEvent) &&
211             lastClickEvent.getClickCount() == 2) {
212           final HitInfo info = view.getHitInfoFactory().createHitInfo(x, y,
213               Graph2DTraversal.NODES | Graph2DTraversal.EDGES, true);
214           if (info.hasHitNodes()) {
215             final Node modelNode = manager.getModelNode(info.getHitNode());
216             if (baseModel.getHierarchyManager() != null && !baseModel.getHierarchyManager().isGroupNode(modelNode)) {
217               final NodeList selectedNodes = new NodeList();
218               final Graph2D graph = view.getGraph2D();
219               for (NodeCursor nc = graph.selectedNodes(); nc.ok(); nc.next()) {
220                 selectedNodes.add(manager.getModelNode(nc.node()));
221               }
222 
223               graph.firePreEvent();
224               try {
225                 handleClick(selectedNodes, modelNode, !lastClickEvent.isControlDown());
226               } finally {
227                 graph.firePostEvent();
228               }
229 
230               baseModelView.repaint();
231             }
232           }
233         }
234       }
235     };    
236     filteredViewMode.setOrthogonalEdgeRouting(true);
237     filteredViewMode.getMouseInputMode().setNodeSearchingEnabled(true);
238     filteredViewMode.allowEdgeCreation(false);
239     filteredViewMode.allowNodeCreation(false);
240     view.addViewMode(filteredViewMode);
241 
242     final JToolBar jtb = getToolBar();
243     if (jtb == null) {
244       undoManager = null;
245     } else {
246       undoManager = new Graph2DUndoManager(view.getGraph2D());
247       undoManager.setViewContainer(view);
248       jtb.addSeparator();
249       jtb.add(prepare(undoManager.getUndoAction(), "Undo", "resource/undo.png"));
250       jtb.add(prepare(undoManager.getRedoAction(), "Redo", "resource/redo.png"));
251     }
252 
253 
254     //create help pane
255     JComponent helpPane = null;
256     if (helpFilePath != null) {
257       final URL url = getResource(helpFilePath);
258       if (url == null) {
259         System.err.println("Could not locate help file: " + helpFilePath);
260       } else {
261         helpPane = createHelpPane(url);
262       }
263     }
264 
265     //create panels
266     final JPanel rightPanel = new JPanel(new BorderLayout());
267 
268     oh = new GraphExplorerOptionHandler();
269     oh.getItem("General", "Layout").setAttribute(
270             GraphExplorerOptionHandler.ATTRIBUTE_LAYOUT_CALLBACK,
271             new ActionListener() {
272               public void actionPerformed( final ActionEvent e ) {
273                 final NodeCursor nc = view.getGraph2D().selectedNodes();
274                 final Node node = nc.ok() ? nc.node() : null;
275                 doLayout(new LayoutContext(view, true, false, node, baseModel.getHierarchyManager().containsGroups()),
276                     oh.getLayoutId());
277               }
278             });
279     rightPanel.add(oh.createEditorComponent(), BorderLayout.NORTH);
280 
281     if (helpPane != null) {
282       rightPanel.add(helpPane, BorderLayout.CENTER);
283     }
284 
285     final JSplitPane splitPane = newSplitPane(view, rightPanel);
286     splitPane.setBorder(BorderFactory.createEmptyBorder());
287     splitPane.setResizeWeight(1);
288     splitPane.setContinuousLayout(false);
289     contentPane.add(newSplitPane(baseModelView, splitPane), BorderLayout.CENTER);
290 
291     //begin setup drag and drop support
292     //nodes can be added to the filtered graph by dragging JTree nodes to the filtered view
293     final DropSupport dropSupport = new DropSupport(view) {
294       private Node createNodeImpl(
295               final Graph2DView view,
296               final NodeRealizer r,
297               final double x,
298               final double y
299       ) {
300         final Node modelNode = r.getNode();
301         final HierarchyManager hierarchy = baseModel.getHierarchyManager();
302         for (Node n = modelNode; n != null; n = hierarchy.getParentNode(n)) {
303           inclusionFilter.includeNode(n);
304         }
305 
306         final Graph2D filteredGraph = view.getGraph2D();
307 
308         AutoBoundsFeature abf = null;
309         final Node modelParent = hierarchy.getParentNode(modelNode);
310         if (modelParent != null) {
311           final Node viewParent = manager.getViewNode(modelParent, filteredGraph);
312           if (viewParent != null) {
313             final NodeRealizer nr = filteredGraph.getRealizer(viewParent);
314             abf = nr.getAutoBoundsFeature();
315           }
316         }
317         final boolean oldABF = abf != null && abf.isAutoBoundsEnabled();
318         if (oldABF) {
319           abf.setAutoBoundsEnabled(false);
320         }
321 
322         updateFilteredGraph(filteredGraph, true);
323 
324         final Node viewNode = manager.getViewNode(modelNode, filteredGraph);
325         filteredGraph.setCenter(viewNode, x, y);
326 
327         if (oldABF) {
328           abf.setAutoBoundsEnabled(true);
329         }
330 
331         return viewNode;
332       }
333 
334       protected Node createNode(
335               final Graph2DView view,
336               final NodeRealizer r,
337               final double x,
338               final double y
339       ) {
340         final Graph2D graph = view.getGraph2D();
341         graph.firePreEvent();
342         try {
343           return createNodeImpl(view, r, x ,y);
344         } finally {
345           graph.firePostEvent();
346         }
347       }
348 
349       protected boolean dropNodeRealizer(
350               final Graph2DView view,
351               final NodeRealizer r,
352               final double x,
353               final double y
354       ) {
355         final boolean success = super.dropNodeRealizer(view, r, x, y);
356         if (success) {
357           view.requestFocus();
358         }
359         return success;
360       }
361     };
362     dropSupport.setPreviewEnabled(true);
363 
364     final DragSource dragSource = new DragSource();
365     dragSource.createDefaultDragGestureRecognizer(jt, DnDConstants.ACTION_MOVE,
366         new DragGestureListener() {
367           public void dragGestureRecognized(final DragGestureEvent e) {
368             final TreePath[] paths = jt.getSelectionPaths();
369             if (paths != null && paths.length > 0) {
370               final Object value = paths[0].getLastPathComponent();
371               if (value instanceof Node) {
372                 final Node modelNode = (Node) value;
373                 final HierarchyManager hierarchy = baseModel.getHierarchyManager();
374                 if (!hierarchy.isGroupNode(modelNode)) {
375                   dropSupport.startDrag(
376                           dragSource,
377                           baseModel.getRealizer(modelNode),
378                           e,
379                           DragSource.DefaultMoveDrop);
380                 }
381               }
382             }
383           }
384         });
385   }
386 
387   /**
388    * Calculates a new graph layout using the specified layout style.
389    * @param context the graph to lay out.
390    * @param layout one of
391    * {@link GraphExplorerOptionHandler#ID_LAYOUT_ORTHOGONAL},
392    * {@link GraphExplorerOptionHandler#ID_LAYOUT_ORGANIC},
393    * {@link GraphExplorerOptionHandler#ID_LAYOUT_HIERARCHIC},
394    * {@link GraphExplorerOptionHandler#ID_LAYOUT_BALLOON},
395    * {@link GraphExplorerOptionHandler#ID_LAYOUT_CIRCULAR},
396    */
397   private static void doLayout( final LayoutContext context, final byte layout ) {
398     switch (layout) {
399       case GraphExplorerOptionHandler.ID_LAYOUT_ORGANIC:
400         LayoutSupport.doOrganicLayout(context);
401         break;
402       case GraphExplorerOptionHandler.ID_LAYOUT_HIERARCHIC:
403         LayoutSupport.doHierarchicLayout(context);
404         break;
405       case GraphExplorerOptionHandler.ID_LAYOUT_BALLOON:
406         LayoutSupport.doBalloonLayout(context);
407         break;
408       case GraphExplorerOptionHandler.ID_LAYOUT_CIRCULAR:
409         LayoutSupport.doCircularLayout(context);
410         break;
411       default:
412         LayoutSupport.doOrthogonalLayout(context);
413         break;
414     }
415   }
416 
417   /**
418    * Updates the specified graph according to the inclusion settings
419    * of the demo's inclusion filter.
420    * @param graph the graph to be changed. The given graph instance has to be
421    * registered as a view graph in the demo's model-view manager.
422    * @param allowRemovals if <code>true</code> elements may be removed from
423    * the specified graph.
424    * 
425    */
426   private void updateFilteredGraph(
427           final Graph2D graph, final boolean allowRemovals
428   ) {
429     final InclusionFilter filter = inclusionFilter;
430     try {
431       filter.setAllowRemovals(allowRemovals);
432       manager.synchronizeModelToViewGraph(graph);
433     } finally {
434       filter.setAllowRemovals(true);
435     }
436   }
437 
438   /**
439    * Returns the application tool bar component.
440    * @return the application tool bar component or <code>null</code> if there
441    * is no tool bar.
442    */
443   private JToolBar getToolBar() {
444     final JPanel container = contentPane;
445     for (int i = 0, n = container.getComponentCount(); i < n; ++i) {
446       final Component c = container.getComponent(i);
447       if (c instanceof JToolBar) {
448         return (JToolBar) c;
449       }
450     }
451 
452     return null;
453   }
454 
455   /**
456    * Sets tool tip text and display icon for the specified action.
457    * @param a the action to update.
458    * @param tip the tool tip text for the specified action.
459    * @param icon the resource path to the display icon for the specified action.
460    * @return the specified action instance.
461    */
462   private Action prepare( final Action a, final String tip, final String icon ) {
463     a.putValue(Action.SHORT_DESCRIPTION, tip);
464     a.putValue(Action.SMALL_ICON, getIconResource(icon));
465     return a;
466   }
467 
468   /**
469    * Creates the application help pane.
470    *
471    * @param helpURL the URL of the HTML help page to display.
472    */
473   protected JComponent createHelpPane(final URL helpURL) {
474     try {
475       JEditorPane editorPane = new JEditorPane(helpURL);
476       editorPane.setEditable(false);
477       editorPane.setPreferredSize(new Dimension(250, 250));
478       return new JScrollPane(editorPane);
479     } catch (IOException e) {
480       e.printStackTrace();
481     }
482     return null;
483   }
484 
485   /**
486    * Overwritten to load an initial sample graph <em>after</em> the
487    * graphical user interface has been completely created.
488    * @param rootPane the container to hold the demo's graphical user interface.
489    */
490   public void addContentTo( final JRootPane rootPane ) {
491     super.addContentTo(rootPane);
492     loadInitialGraph();
493   }
494 
495   /**
496    * Overwritten to create an action that loads a model graph in GraphML or
497    * compressed GraphML format in a background thread.
498    * @return an action that that loads a model graph in GraphML or compressed
499    * GraphML format in a background thread. 
500    */
501   protected Action createLoadAction() {
502     return new LoadAction();
503   }
504 
505   /**
506    * Overwritten to create an action that saves the model graph in GraphML or
507    * compressed GraphML format.
508    * @return an action that that saves the model graph in GraphML or compressed
509    * GraphML format. 
510    */
511   protected Action createSaveAction() {
512     return new SaveAction("Save Model Graph", baseModel);
513   }
514 
515   /**
516    * Creates an action the saves the displayed graph in GraphML or compressed
517    * GraphML format.
518    * @return an action the saves the displayed graph in GraphML or compressed
519    * GraphML format.
520    */
521   protected Action createSaveFilteredGraphAction() {
522     return new SaveAction("Save Filtered Graph", view.getGraph2D());
523   }
524 
525   /**
526    * Overwritten to add controls to save the model and the displayed graph
527    * as well as controls to open sample graphs to the application menu bar.
528    * @return the application menu bar.
529    */
530   protected JMenuBar createMenuBar() {
531     JMenuBar mb = new JMenuBar();
532 
533     JMenu file = new JMenu("File");
534     Action action;
535     action = createLoadAction();
536     if (action != null) {
537       file.add(action);
538     }
539     action = createSaveAction();
540     if (action != null) {
541       file.add(action);
542     }
543     file.add(createSaveFilteredGraphAction());
544     file.addSeparator();
545     file.add(new PrintAction());
546     file.addSeparator();
547     file.add(new ExitAction());
548     mb.add(file);
549 
550     //add valid sample graphs to the "sample graphs" menu
551     JMenu sampleGraphs = new JMenu("Sample Graphs");
552     for (Iterator it = getLoadSampleActions(); it.hasNext();) {
553       sampleGraphs.add((Action) it.next());
554     }
555     mb.add(sampleGraphs);
556 
557     return mb;
558   }
559 
560   private Iterator getLoadSampleActions() {
561     final String key = "MultiPageLayoutDemo.samples";
562     final Object samples = contentPane.getClientProperty(key);
563     if (samples instanceof List) {
564       return ((List) samples).iterator();
565     } else {
566       final ArrayList list = new ArrayList(3);
567       list.add(createLoadSampleActions(
568               "Pop Artists Relationships",
569               "resource/pop-artists.graphmlz"));
570       list.add(createLoadSampleActions(
571               "yFiles Class Relationships",
572               "resource/yfiles-classes.graphmlz"));
573       list.add(createLoadSampleActions(
574               "yFiles Class Relationships with Nested Packages",
575               "resource/yfiles-classes-and-packages-nested.graphmlz"));
576       contentPane.putClientProperty(key, list);
577       return list.iterator();
578     }
579   }
580 
581   private Action createLoadSampleActions(final String name, final String resource) {
582     if (isResourceValid(resource)) {
583       return new LoadSampleGraphAction(name, resource);
584     } else {
585       throw new RuntimeException("Missing resource: " + resource);
586     }
587   }
588 
589   /**
590    * Determines whether or not the specified resource can be resolved.
591    * @param resource the name of the resource to check.
592    * @return <code>true</code> if the resource can be resolved;
593    * <code>false</code> otherwise.
594    */
595   private boolean isResourceValid(final String resource) {
596     return getResource(resource) != null;
597   }
598 
599   /**
600    * Loads an initial sample graph.
601    */
602   protected void loadInitialGraph() {
603     EventQueue.invokeLater(new Runnable() {
604       public void run() {
605         final Iterator it = getLoadSampleActions();
606         if (it.hasNext()) {
607           final Action action = (Action) it.next();
608           action.putValue("onLoaded", new Runnable() {
609             public void run() {
610               final Node mn = baseModel.firstNode();
611               baseModel.unselectAll();
612               baseModel.setSelected(mn, true);
613 
614               inclusionFilter.reset();
615               inclusionFilter.includeNode(mn);
616 
617               final Graph2D v = view.getGraph2D();
618               v.firePreEvent();
619               try {
620                 updateFilteredGraph(v, true);
621       
622                 final Node vn = manager.getViewNode(mn, v);
623                 final NodeRealizer vnr = v.getRealizer(vn);
624       
625                 view.setZoom(1);
626                 view.setCenter(vnr.getCenterX(), vnr.getCenterY());
627       
628                 handleClick(new NodeList(mn), mn, true);
629               } finally {
630                 v.firePostEvent();
631               }
632 
633               if (undoManager != null) {
634                 undoManager.resetQueue();
635             }
636             }
637           });
638           action.actionPerformed(null);
639         }
640       }
641     });
642   }
643 
644   /**
645    * Overwritten to load a model graph in GraphML or compressed GraphML format.
646    * @param resourceName the name of the graph resource to load. 
647    */
648   protected void loadGraph(String resourceName) {
649     final Graph2D filteredGraph = view.getGraph2D();
650     filteredGraph.clear();
651     filteredGraph.updateViews();
652 
653     loadGraph(baseModel, resourceName);
654   }
655 
656   /**
657    * Loads the specified graph structure resource in GraphML of compressed
658    * GraphML formant into the given graph instance.
659    * @param graph the graph instance to store the loaded data.
660    * @param resourceName the name of the graph resource to load.
661    */
662   private void loadGraph(final Graph2D graph, final String resourceName) {
663     URL resource = null;
664     final File file = new File(resourceName);
665     if (file.exists()) {
666       try {
667         resource = file.toURI().toURL();
668       } catch (MalformedURLException e) {
669         D.showError(e.getMessage());
670         return;
671       }
672     } else {
673       resource = getResource(resourceName);
674       if (resource == null) {
675         return;
676       }
677     }
678 
679     try {
680       final IOHandler ioh = resource.getFile().endsWith(".graphmlz") ? new ZipGraphMLIOHandler() : createGraphMLIOHandler();
681       
682       graph.clear();
683       ioh.read(graph, resource);
684 
685       normalizeModel(graph);
686     } catch (IOException ioe) {
687       D.showError("Unexpected error while loading resource \"" + resource + "\" due to " + ioe.getMessage());
688     }
689     graph.setURL(resource);
690   }
691 
692   /**
693    * Disables default undo-/redo-support. This demo uses its own {@link Graph2DUndoManager}.
694    */
695   protected boolean isUndoRedoEnabled() {
696     return false;
697   }
698 
699   /**
700    * Disables the clipboart for this demo.
701    */
702   protected boolean isClipboardEnabled() {
703     return false;
704   }
705 
706   //"open" folder nodes and reset node positions as well as edge paths
707   private static void normalizeModel( final Graph2D graph ) {
708     if (!graph.isEmpty()) {
709       final GroupNodeRealizer.StateChangeListener listener =
710               new GroupNodeRealizer.StateChangeListener();
711       final HierarchyManager hierarchy = graph.getHierarchyManager();
712       hierarchy.addHierarchyListener(listener);
713 
714       //open folder nodes
715       final ArrayList stack = new ArrayList();
716       stack.add(null);
717       while (!stack.isEmpty()) {
718         final Node root = (Node) stack.remove(stack.size() - 1);
719 
720         for (NodeCursor nc = hierarchy.getChildren(root); nc.ok(); nc.next()) {
721           final Node node = nc.node();
722           if (hierarchy.isFolderNode(node)) {
723             hierarchy.openFolder(node);
724             stack.add(node);
725           }
726         }
727       }
728       hierarchy.removeHierarchyListener(listener);
729 
730       //reset node positions and edge paths
731       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
732         Node node = nc.node();
733         graph.setCenter(node, YPoint.ORIGIN);
734       }
735       LayoutTool.resetPaths(graph, true);
736 
737       final Rectangle2D.Double r = new Rectangle2D.Double(0, 0, -1, -1);
738       for (NodeCursor nc = hierarchy.getChildren(null); nc.ok(); nc.next()) {
739         graph.getRealizer(nc.node()).calcUnionRect(r);
740       }
741     }
742   }
743 
744   /**
745    * Overwritten to prevent keyboard shortcuts from being registered multiple
746    * times. The demo constructor registers the demo's shortcuts.
747    */
748   protected void registerViewActions() {
749   }
750 
751   /**
752    * Overwritten to prevent the default view listeners from being registered.
753    * The demo constructor registers the demo's view listeners.
754    */
755   protected void registerViewListeners() {
756   }
757 
758   /**
759    * Overwritten to prevent the default view modes from being registered.
760    * The demo constructor registers the demo's view modes.
761    */
762   protected void registerViewModes() {
763   }
764 
765   /**
766    * Called after clicking on a node in the graph view.
767    *
768    * @param selectedModelNodes all model nodes that are selected in the view.
769    * @param clickedModelNode the clicked model node.
770    * @param explore whether or not to explore the neighbors of selected nodes.
771    */
772   protected void handleClick(
773           final NodeList selectedModelNodes,
774           final Node clickedModelNode,
775           final boolean explore
776   ) {
777     final InclusionFilter filter = inclusionFilter;
778     final Graph2D graph = view.getGraph2D();
779 
780     final LayoutContext context = new LayoutContext(
781             view, true, true, manager.getViewNode(clickedModelNode, graph), baseModel.getHierarchyManager().containsGroups());
782 
783 
784     final byte edgeType = oh.getExplorationEdgeType();
785 
786     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
787       context.addOldNode(nc.node());
788     }
789     for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
790       context.addOldEdge(ec.edge());
791     }
792 
793     filter.reset();
794     //include all selected model nodes
795     for (NodeCursor nc = selectedModelNodes.nodes(); nc.ok(); nc.next()) {
796       filter.includeNode(nc.node());
797     }
798 
799     //include all edges of the induced subgraph
800     for (NodeCursor nc = selectedModelNodes.nodes(); nc.ok(); nc.next()) {
801       final Node modelNode = nc.node();
802       for (EdgeCursor ec = getEdges(modelNode, edgeType); ec.ok(); ec.next()) {
803         final Edge modelEdge = ec.edge();
804         if (filter.acceptInsertion(modelEdge.opposite(modelNode))) {
805           filter.includeEdge(modelEdge);
806         }
807       }
808     }
809 
810     final byte direction;
811     final byte oppositeEdgeType;
812     switch (edgeType) {
813       case GraphExplorerOptionHandler.EDGE_TYPE_ALL:
814         direction = Bfs.DIRECTION_BOTH;
815         oppositeEdgeType = GraphExplorerOptionHandler.EDGE_TYPE_ALL;
816         break;
817       case GraphExplorerOptionHandler.EDGE_TYPE_OUT:
818         direction = Bfs.DIRECTION_SUCCESSOR;
819         oppositeEdgeType = GraphExplorerOptionHandler.EDGE_TYPE_IN;
820         break;
821       case GraphExplorerOptionHandler.EDGE_TYPE_IN:
822         direction = Bfs.DIRECTION_PREDECESSOR;
823         oppositeEdgeType = GraphExplorerOptionHandler.EDGE_TYPE_OUT;
824         break;
825       default:
826         throw new IllegalStateException("Unsupported edge type: " + edgeType);
827     }
828 
829     final NodeList[] modelLayers = Bfs.getLayers(
830             baseModel,
831             selectedModelNodes,
832             explore ? direction : Bfs.DIRECTION_BOTH,
833             Maps.createHashedNodeMap(),
834             2);
835 
836     if (modelLayers.length > 1) {
837       if (explore) {
838         int nodesToAdd = oh.getMaxNewNodes();
839         final HashSet tmpModelLayer = new HashSet();
840         tmpModelLayer.addAll(modelLayers[0]);
841         for (NodeCursor nc = modelLayers[1].nodes(); nc.ok(); nc.next()) {
842           final Node modelNode = nc.node();
843           final Node viewNode = manager.getViewNode(modelNode, graph);
844           if (viewNode == null) {
845             if (nodesToAdd > 0) {
846               filter.includeNode(modelNode);
847               --nodesToAdd;
848             }
849           }
850           // accept all edges from selected nodes to their direct neighbors
851           for (EdgeCursor ec = getEdges(modelNode, oppositeEdgeType); ec.ok(); ec.next()) {
852             final Edge modelEdge = ec.edge();
853             if (tmpModelLayer.contains(modelEdge.opposite(modelNode))) {
854               filter.includeEdge(modelEdge);
855             }
856           }
857         }
858       } else {
859         for (NodeCursor nc = modelLayers[1].nodes(); nc.ok(); nc.next()) {
860           final Node modelNode = nc.node();
861           final Node viewNode = manager.getViewNode(modelNode, graph);
862           if (viewNode != null) {
863             context.addNewNode(viewNode);
864           }
865         }
866       }
867     }
868 
869     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
870       filter.includeNode(manager.getModelNode(nc.node()));
871     }
872     for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
873       filter.includeEdge(manager.getModelEdge(ec.edge()));
874     }
875 
876     final HierarchyManager mh = baseModel.getHierarchyManager();
877     final HashSet parents = new HashSet();
878     for (Iterator it = inclusionFilter.includedNodes.iterator(); it.hasNext();) {
879       final Node next = (Node) it.next();
880       for (Node p = mh.getParentNode(next); p != null; p = mh.getParentNode(p)) {
881         if (!parents.add(p)) {
882           break;
883         }
884       }
885     }
886     for (Iterator it = parents.iterator(); it.hasNext();) {
887       inclusionFilter.includeNode((Node) it.next());
888     }
889 
890     final NodeMap node2Predecessor = Maps.createHashedNodeMap();
891     for (NodeCursor nc = selectedModelNodes.nodes(); nc.ok(); nc.next()) {
892       final Node modelNode = nc.node();
893       for (EdgeCursor ec = getEdges(modelNode, edgeType); ec.ok(); ec.next()) {
894         final Edge modelEdge = ec.edge();
895         final Node modelNeighbor = modelEdge.opposite(modelNode);
896         if (node2Predecessor.get(modelNeighbor) == null) {
897           node2Predecessor.set(modelNeighbor, modelNode);
898         }
899       }
900     }
901 
902     updateFilteredGraph(graph, false);
903 
904     final NodeList viewNodes = new NodeList();
905     for (NodeCursor nc = selectedModelNodes.nodes(); nc.ok(); nc.next()) {
906       final Node viewNode = manager.getViewNode(nc.node(), graph);
907       if (viewNode != null) {
908         viewNodes.add(viewNode);
909       }
910     }
911     final NodeMap viewDist = Maps.createHashedNodeMap();
912     final int maxDist = oh.getMaxDist();
913     Bfs.getLayers(
914             graph,
915             viewNodes,
916             Bfs.DIRECTION_BOTH,
917             viewDist,
918             maxDist + 1);
919     final HierarchyManager vh = graph.getHierarchyManager();
920     if (maxDist > -1) {
921       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
922         final Node viewNode = nc.node();
923         final int dist = viewDist.getInt(viewNode);
924         if ((dist < 0 || maxDist < dist) && !vh.isGroupNode(viewNode)) {
925           filter.excludeNode(manager.getModelNode(viewNode));
926           context.addRemovedNode(viewNode);
927           for (EdgeCursor ec = viewNode.edges(); ec.ok(); ec.next()) {
928             context.addRemovedEdge(ec.edge());
929           }
930         }
931       }
932 
933       for (NodeCursor nc = vh.getChildren(null); nc.ok(); nc.next()) {
934         checkEmptyGroups(vh, nc.node(), viewDist, maxDist, context);
935       }
936     } else {
937       for (NodeCursor nc = vh.getChildren(null); nc.ok(); nc.next()) {
938         checkEmptyGroups(vh, nc.node(), viewDist, Integer.MAX_VALUE, context);
939       }
940     }
941 
942     for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
943       if (!context.isOldEdge(ec.edge())) {
944         context.addNewEdge(ec.edge());
945       }
946     }
947 
948     if (explore) {
949       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
950         final Node viewNode = nc.node();
951         if (!context.isOldNode(viewNode)) {
952           context.addNewNode(viewNode);
953 
954           final Node modelNode = manager.getModelNode(viewNode);  
955           final Node predecessorNode = manager.getViewNode(
956                   (Node) node2Predecessor.get(modelNode), graph);
957           if (predecessorNode == null) {
958             graph.setCenter(viewNode, 0, 0);
959           } else if (!context.isOldNode(viewNode)) {
960             graph.setCenter(viewNode, graph.getCenter(predecessorNode));
961           }
962         }
963       }
964 
965       final Rectangle2D.Double r = new Rectangle2D.Double();
966       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
967         graph.getRealizer(nc.node()).calcUnionRect(r);
968       }
969     }
970 
971     doLayout(context, oh.getLayoutId());
972   }
973 
974   /*
975    * Marks group nodes for removal if they:
976    * - do not contain (non-marked) elements,
977    * - and the dist value of the group node is larger than max dist
978    */
979   private boolean checkEmptyGroups(
980           final HierarchyManager vh,
981           final Node viewNode,
982           final DataProvider viewDist,
983           final int maxDist,
984           final LayoutContext context
985   ) {
986     if (vh.isGroupNode(viewNode)) {
987       boolean keepGroup = false;
988       for (NodeCursor nc = vh.getChildren(viewNode); nc.ok(); nc.next()) {
989         if (checkEmptyGroups(vh, nc.node(), viewDist, maxDist, context)) {
990           keepGroup = true;
991         }
992       }
993       if (keepGroup) {
994         return true;
995       }
996 
997       final int dist = viewDist.getInt(viewNode);
998       if (0 <= dist && dist <= maxDist) {
999         return true;
1000      }
1001      context.addRemovedNode(viewNode);
1002      for (EdgeCursor ec = viewNode.edges(); ec.ok(); ec.next()) {
1003        context.addRemovedEdge(ec.edge());
1004      }
1005      return false;
1006    } else {
1007      return !context.isRemovedNode(viewNode);
1008    }
1009  }
1010
1011  /**
1012   * Returns a cursor over all edges of the specified type that are connected to
1013   * the specified node.
1014   * @param node the node whose edges are retrieved.
1015   * @param edgeType one of {@link GraphExplorerOptionHandler#EDGE_TYPE_ALL},
1016   * {@link GraphExplorerOptionHandler#EDGE_TYPE_OUT}, and
1017   * {@link GraphExplorerOptionHandler#EDGE_TYPE_IN}.
1018   * @return a cursor over edges connected to the specified node.
1019   * @throws IllegalArgumentException if edge type does not equal one of the
1020   * mentioned symbolic constants. 
1021   */
1022  private static EdgeCursor getEdges( final Node node, final byte edgeType ) {
1023    switch (edgeType) {
1024      case GraphExplorerOptionHandler.EDGE_TYPE_ALL:
1025        return node.edges();
1026      case GraphExplorerOptionHandler.EDGE_TYPE_OUT:
1027        return node.outEdges();
1028      case GraphExplorerOptionHandler.EDGE_TYPE_IN:
1029        return node.inEdges();
1030      default:
1031        throw new IllegalArgumentException("Unsupported edge type: " + edgeType);
1032    }
1033  }
1034
1035
1036  /**
1037   * Creates a horizontally split <code>JSplitPane</code> instance.
1038   * @param left the component for the split pane's left compartment.
1039   * @param right  the component for the split pane's right compartment.
1040   * @return a horizontally split <code>JSplitPane</code> instance.
1041   */
1042  private static JSplitPane newSplitPane(
1043          final JComponent left, final JComponent right
1044  ) {
1045    return new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, left, right);
1046  }
1047
1048  /** Launches this demo. */
1049  public static void main(String[] args) {
1050    EventQueue.invokeLater(new Runnable() {
1051      public void run() {
1052        Locale.setDefault(Locale.ENGLISH);
1053        initLnF();
1054        (new GraphExplorerDemo("resource/graphexplorerhelp.html")).start();
1055      }
1056    });
1057  }
1058
1059  /**
1060   * Action that excludes deleted elements from the inclusion filter (and, thus, from the graph view).
1061   */
1062  final class MyDeleteSelectionAction extends Graph2DViewActions.DeleteSelectionAction {
1063    protected boolean acceptEdge(Graph2D graph, Edge edge) {
1064      final boolean accept = super.acceptEdge(graph, edge);
1065      if (accept) {
1066        final Edge modelEdge = manager.getModelEdge(edge);
1067        inclusionFilter.excludeEdge(modelEdge);
1068      }
1069      return accept;
1070    }
1071    
1072    protected boolean acceptNode(Graph2D graph, Node node) {
1073      final boolean accept = super.acceptNode(graph, node);
1074      if (accept) {
1075        final Node modelNode = manager.getModelNode(node);
1076        inclusionFilter.excludeNode(modelNode);
1077      }
1078      return accept;
1079    }
1080  }
1081
1082  final class MyDoubleClickListener extends MouseAdapter {
1083    public void mouseClicked(MouseEvent e) {
1084      final JTree tree = (JTree) e.getSource();
1085      if (e.getClickCount() == 2) {
1086        final TreePath path = tree.getPathForLocation(e.getX(), e.getY());
1087        if (path != null) {
1088          final Object last = path.getLastPathComponent();
1089          if (last instanceof Node) {
1090            final Node modelNode = (Node) last;
1091            final HierarchyManager hierarchy = baseModel.getHierarchyManager();
1092            if (!hierarchy.isGroupNode(modelNode)) {
1093              final Graph2D graph = view.getGraph2D();
1094              graph.firePreEvent();
1095              try {
1096                createViewNode(modelNode);
1097              } finally {
1098                graph.firePostEvent();
1099              }
1100            }
1101          }
1102        }
1103      }
1104    }
1105
1106    private void createViewNode( final Node clickedModelNode ) {
1107      final NodeMap modelNode2IsNew = Maps.createHashedNodeMap();
1108      final NodeList selectedNodes = new NodeList(baseModel.selectedNodes());
1109      if (!selectedNodes.contains(clickedModelNode)) {
1110        selectedNodes.add(clickedModelNode);
1111      }
1112      collectParentNodes(selectedNodes);
1113      for (NodeCursor nc = selectedNodes.nodes(); nc.ok(); nc.next()) {
1114        Node node = nc.node();
1115        if (!inclusionFilter.isIncluded(node)) {
1116          modelNode2IsNew.setBool(node, true);
1117          inclusionFilter.includeNode(node);
1118        }
1119      }
1120
1121      final Graph2D graph = view.getGraph2D();
1122      final boolean empty = graph.nodeCount() == 0;
1123      updateFilteredGraph(graph, true);
1124
1125      final Node clickedViewNode = manager.getViewNode(clickedModelNode, graph);
1126      final LayoutContext context = new LayoutContext(view, false, true, null, baseModel.getHierarchyManager().containsGroups());
1127      for (NodeCursor nc = selectedNodes.nodes(); nc.ok(); nc.next()) {
1128        final Node modelNode = nc.node();
1129        if (modelNode2IsNew.getBool(modelNode)) {
1130          final Node viewNode = manager.getViewNode(modelNode, graph);
1131          if (viewNode.getGraph() == graph) {
1132            context.addNewNode(viewNode);
1133          }
1134        }
1135      }
1136
1137      doLayout(context, oh.getLayoutId());
1138      
1139      final ViewAnimationFactory factory = new ViewAnimationFactory(view);
1140      factory.setQuality(ViewAnimationFactory.HIGH_PERFORMANCE);
1141      final CompositeAnimationObject composite = AnimationFactory.createConcurrency();
1142      for (Iterator it = orderByGroupDepth(graph, context.newNodes()); it.hasNext();) {
1143        final NodeRealizer nr = graph.getRealizer((Node) it.next());
1144        composite.addAnimation(factory.fadeIn(nr, DURATION_ADD));
1145      }
1146
1147      if (empty) {
1148        view.setZoom(1);
1149        view.setCenter(graph.getX(clickedViewNode), graph.getY(clickedViewNode));
1150      } else {
1151        composite.addAnimation(factory.focusView(
1152                Math.max(view.getZoom(), 1),
1153                DefaultMutableValue2D.createView(graph.getLocation(clickedViewNode)),
1154                DURATION_ADD));
1155      }
1156
1157      final AnimationPlayer player = factory.createConfiguredPlayer();
1158      player.addAnimationListener(new AnimationListener() {
1159        public void animationPerformed( final AnimationEvent e ) {
1160          if (AnimationEvent.END == e.getHint()) {
1161            view.requestFocus();
1162          }
1163        }
1164      });
1165      player.animate(composite);
1166    }
1167
1168    private Iterator orderByGroupDepth( final Graph2D graph, final Iterator nodes ) {
1169      final HierarchyManager manager = graph.getHierarchyManager();
1170      if (manager == null) {
1171        return nodes;
1172      } else {
1173        final NodeMap depth = Maps.createHashedNodeMap();
1174        final ArrayList ordered = new ArrayList();
1175        while (nodes.hasNext()) {
1176          final Node node = (Node) nodes.next();
1177          depth.setInt(node, manager.getLocalGroupDepth(node));
1178          ordered.add(node);
1179        }
1180        Collections.sort(ordered, new Comparator() {
1181          public int compare( final Object o1, final Object o2 ) {
1182            return Comparators.compare(depth.getInt(o1), depth.getInt(o2));
1183          }
1184        });
1185        return ordered.iterator();
1186      }
1187    }
1188
1189    private void collectParentNodes( final NodeList nodes ) {
1190      final HierarchyManager hierarchy = baseModel.getHierarchyManager();
1191      final NodeList newNodes = new NodeList();
1192      final HashSet marked = new HashSet();
1193      for (NodeCursor nc = nodes.nodes(); nc.ok(); nc.next()) {
1194        final Node node = nc.node();
1195        for (Node p = hierarchy.getParentNode(node); p != null; p = hierarchy.getParentNode(p)) {
1196          if (marked.add(p)) {
1197            newNodes.add(p);
1198          } else {
1199            break;
1200          }
1201        }
1202      }
1203      marked.clear();
1204      marked.addAll(nodes);
1205      for (NodeCursor nc = newNodes.nodes(); nc.ok(); nc.next()) {
1206        final Node p = nc.node();
1207        if (marked.add(p)) {
1208          nodes.add(p);
1209        }
1210      }
1211    }
1212  }
1213
1214  /**
1215   * Customized graph renderer that shows, for each node with hidden links, a yellow oval in the upper-right node corner
1216   * that contains the number of such links (i.e., the number of edges of the node which are currently not shown).
1217   */
1218  final class MyGraph2DRenderer extends DefaultGraph2DRenderer {
1219    private static final int OVAL_WIDTH = 25;
1220    private static final int OVAL_HEIGHT = 15;
1221
1222    protected void paintSloppy(Graphics2D gfx, NodeRealizer nr) {
1223      super.paintSloppy(gfx, nr);
1224    }
1225
1226    protected void paint(Graphics2D gfx, NodeRealizer nr) {
1227      super.paint(gfx, nr);
1228      final Color bkpColor = gfx.getColor();
1229      final Graph2D filteredGraph = view.getGraph2D();
1230      final Node viewNode = nr.getNode();
1231      final Node modelNode = manager.getModelNode(viewNode);
1232      final byte edgeType = oh.getExplorationEdgeType();
1233      final int hiddenLinks = getDegree(modelNode, edgeType) - getDegree(viewNode, edgeType); //calculate the number of hidden (non-displayed) links
1234      if (hiddenLinks > 0) {
1235        //draw the yellow oval containing the number of hidden links
1236        final YRectangle viewNodeRect = filteredGraph.getRectangle(viewNode);
1237        final YPoint ovalLocation = new YPoint(viewNodeRect.x + viewNodeRect.width - OVAL_WIDTH * 0.5,
1238            viewNodeRect.y - OVAL_HEIGHT * 0.5);
1239        gfx.setColor(Color.YELLOW);
1240        gfx.fillOval((int) ovalLocation.x, (int) ovalLocation.y, OVAL_WIDTH, OVAL_HEIGHT);
1241        gfx.setColor(Color.YELLOW.darker());
1242        gfx.drawOval((int) ovalLocation.x, (int) ovalLocation.y, OVAL_WIDTH, OVAL_HEIGHT);
1243        gfx.setColor(Color.BLACK);
1244        final String hiddenLinksString = hiddenLinks + "";
1245        final int stringWidth = gfx.getFontMetrics().stringWidth(hiddenLinksString);
1246        final int stringHeight = gfx.getFontMetrics().getAscent() - 2;
1247        gfx.drawString("" + hiddenLinks, (float) (ovalLocation.x + OVAL_WIDTH * 0.5 - stringWidth * 0.5),
1248            (float) (ovalLocation.y + OVAL_HEIGHT * 0.5 + stringHeight * 0.5));
1249        gfx.setColor(bkpColor);
1250      }
1251    }
1252
1253    private int getDegree( final Node node, final byte edgeType ) {
1254      switch (edgeType) {
1255        case GraphExplorerOptionHandler.EDGE_TYPE_ALL:
1256          return node.degree();
1257        case GraphExplorerOptionHandler.EDGE_TYPE_OUT:
1258          return node.outDegree();
1259        case GraphExplorerOptionHandler.EDGE_TYPE_IN:
1260          return node.inDegree();
1261        default:
1262          throw new IllegalArgumentException("Unsupported edge type: " + edgeType);
1263      }
1264    }
1265  }
1266
1267  final class MyGraphCopyFactory extends Graph2DCopyFactory.HierarchicGraph2DCopyFactory {
1268    private final DefaultHierarchyGraphFactory factory;
1269
1270    MyGraphCopyFactory() {
1271      final GraphCopier.CopyFactory f = baseModel.getGraphCopyFactory();
1272      if (f instanceof DefaultHierarchyGraphFactory) {
1273        factory = (DefaultHierarchyGraphFactory) f;
1274      } else {
1275        factory = new DefaultHierarchyGraphFactory();
1276      }
1277    }
1278
1279    public Graph createGraph() {
1280      final Graph2D g = new Graph2D();
1281      final HierarchyManager hierarchy = new HierarchyManager(g);
1282      hierarchy.setGraphFactory(factory);
1283      g.setGraphCopyFactory(this);
1284      return g;
1285    }
1286
1287    public void postCopyGraphData(
1288            final Graph sourceGraph,
1289            final Graph targetGraph,
1290            final Map nodeMap,
1291            final Map edgeMap
1292    ) {
1293      final HashMap tmp = new HashMap();
1294      tmp.putAll(nodeMap);
1295
1296      //update mapping between nodes of source and target graph
1297      for (NodeCursor nc = targetGraph.nodes(); nc.ok(); nc.next()) {
1298        final Node view = nc.node();
1299        final Node model = manager.getModelNode(view);
1300        if (!tmp.containsKey(model)) {
1301          tmp.put(model, view); 
1302        }
1303      }
1304
1305      super.postCopyGraphData(sourceGraph, targetGraph, tmp, edgeMap);
1306    }
1307  }
1308
1309  /**
1310   * Customized tree cell renderer that:
1311   * - greys out elements not shown in the graph view,
1312   * - uses a customized root element that shows the number of nodes in the model graph.
1313   */
1314  final class MyTreeCellRenderer extends DefaultTreeCellRenderer {
1315    public Component getTreeCellRendererComponent(
1316            JTree tree,
1317            Object value,
1318            boolean sel,
1319            boolean expanded,
1320            boolean leaf,
1321            int row,
1322            boolean hasFocus
1323    ) {
1324      Component comp = super.getTreeCellRendererComponent(
1325              tree, value, sel, expanded, leaf, row, hasFocus);
1326      if (value instanceof Node && !inclusionFilter.isIncluded((Node) value)) {
1327        comp.setForeground(Color.GRAY);
1328      } else if(value instanceof  Graph) {
1329        comp = new JLabel("Nodes: " + ((Graph) value).nodeCount());
1330      }
1331      return comp;
1332    }
1333  }
1334
1335  /** A <code>Graph2DSelectionListener</code> that triggers an update of the demo's local view upon selection changes. */
1336  final class SelectionTrigger implements Graph2DSelectionListener {
1337    private final Timer timer;
1338    private Graph2DSelectionEvent lastEvent;
1339
1340    SelectionTrigger(final Graph2D modelGraph) {
1341      timer = new Timer(100, new ActionListener() {
1342        public void actionPerformed(final ActionEvent e) {
1343          if (lastEvent != null) {
1344            handleEvent(lastEvent);
1345          }
1346        }
1347
1348        /**
1349         * Triggers the actual update for the demo's local view.
1350         */
1351        private void handleEvent(final Graph2DSelectionEvent e) {
1352          boolean modelIsSource = (e.getGraph2D() == modelGraph); //TODO always false?
1353          if (e.isNodeSelection()) {
1354            if (modelIsSource) {
1355              for (Iterator iter = manager.viewGraphs(); iter.hasNext();) {
1356                ((Graph2D) iter.next()).unselectAll();
1357              }
1358            } else {
1359              baseModel.unselectAll();
1360            }
1361
1362            for (NodeCursor nc = e.getGraph2D().selectedNodes(); nc.ok(); nc.next()) {
1363              final Node n = nc.node();
1364              if (modelIsSource) {
1365                for (Iterator iter = manager.viewGraphs(); iter.hasNext();) {
1366                  final Graph2D viewGraph = ((Graph2D) iter.next());
1367                  Node viewNode = manager.getViewNode(n, viewGraph);
1368                  if (viewNode != null) {
1369                    viewGraph.setSelected(viewNode, true);
1370                  }
1371                }
1372              } else {
1373                Node orig = manager.getModelNode(n);
1374                if (orig != null) {
1375                  baseModel.setSelected(orig, true);
1376                }
1377              }
1378            }
1379          } else if (e.isEdgeSelection()) {
1380            if (modelIsSource) {
1381              for (Iterator iter = manager.viewGraphs(); iter.hasNext();) {
1382                ((Graph2D) iter.next()).unselectAll();
1383              }
1384            } else {
1385              baseModel.unselectAll();
1386            }
1387
1388            for (EdgeCursor ec = e.getGraph2D().selectedEdges(); ec.ok(); ec.next()) {
1389              final Edge edge = ec.edge();
1390              if (modelIsSource) {
1391                for (Iterator iter = manager.viewGraphs(); iter.hasNext();) {
1392                  final Graph2D viewGraph = ((Graph2D) iter.next());
1393                  Edge viewEdge = manager.getViewEdge(edge, viewGraph);
1394                  if (viewEdge != null) {
1395                    viewGraph.setSelected(viewEdge, true);
1396                  }
1397                }
1398              } else {
1399                Edge orig = manager.getModelEdge(edge);
1400                if (orig != null) {
1401                  baseModel.setSelected(orig, true);
1402                }
1403              }
1404            }
1405          }
1406        }
1407      });
1408      timer.setRepeats(false);
1409    }
1410
1411    public void onGraph2DSelectionEvent(final Graph2DSelectionEvent e) {
1412      if (e.isNodeSelection() || e.isEdgeSelection()) {
1413        lastEvent = e;
1414        timer.restart();
1415      }
1416    }
1417  }
1418
1419
1420  /**
1421   * Abstract base class for loading graphs in a background thread.
1422   */
1423  abstract class AbstractLoadAction extends AbstractAction {
1424    AbstractLoadAction( final String name ) {
1425      super(name);
1426    }
1427
1428    /**
1429     * Loads a model graph in a background thread.
1430     * @param resource the graph resource to load as model graph.
1431     * @param name the display name for the graph resource.
1432     */
1433    void load( final String resource, final String name ) {
1434      final Object onLoaded = getValue("onLoaded");
1435      putValue("onLoaded", null);
1436
1437      EventQueue.invokeLater(new Runnable() {
1438        public void run() {
1439          final JDialog pd = createProgressDialog(name);
1440
1441          final Graph2D filteredGraph = view.getGraph2D();
1442          filteredGraph.clear();
1443          filteredGraph.updateViews();
1444
1445          (new Thread(new Runnable() {
1446            public void run() {
1447              loadGraph(baseModel, resource);
1448              EventQueue.invokeLater(new Runnable() {
1449                public void run() {
1450                  if (undoManager != null) {
1451                    undoManager.resetQueue();
1452                  }
1453
1454                  pd.setVisible(false);
1455                  pd.dispose();
1456
1457                  if (onLoaded instanceof Runnable) {
1458                    ((Runnable) onLoaded).run();
1459                  }
1460                }
1461              });
1462            }
1463          })).start();
1464
1465          pd.setVisible(true);
1466        }
1467      });
1468    }
1469
1470    /**
1471     * Creates an progress dialog for loading graphs in
1472     * background threads.
1473     * @param name the display name of the graph resource that is loaded.
1474     * @return a dialog displaying a progress bar.
1475     */
1476    private JDialog createProgressDialog( final String name ) {
1477      final String title = "Loading " + name;
1478      final JDialog jd = new JDialog(getFrame(), title, true);
1479
1480      final JProgressBar jpb = new JProgressBar(0, 1);
1481      jpb.setString(title);
1482      jpb.setIndeterminate(true);
1483
1484      final JLabel lbl = new JLabel(title);
1485      final JPanel progressPane = new JPanel(new BorderLayout());
1486      progressPane.add(lbl, BorderLayout.NORTH);
1487      progressPane.add(jpb, BorderLayout.CENTER);
1488      final JPanel contentPane = new JPanel(new FlowLayout());
1489      contentPane.add(progressPane);
1490
1491      jd.setContentPane(contentPane);
1492      jd.pack();
1493      jd.setLocationRelativeTo(null);
1494      jd.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
1495
1496      return jd;
1497    }
1498
1499    private JFrame getFrame() {
1500      final Window ancestor = SwingUtilities.getWindowAncestor(contentPane);
1501      if (ancestor instanceof JFrame) {
1502        return (JFrame) ancestor;
1503      } else {
1504        return null;
1505      }
1506    }
1507  }
1508
1509  /**
1510   * Loads a sample graph using the given resource.
1511   */
1512  final class LoadSampleGraphAction extends AbstractLoadAction {
1513    private final String name;
1514    private final String resource;
1515
1516    LoadSampleGraphAction(final String name, final String resource) {
1517      super(name);
1518      this.name = name;
1519      this.resource = resource;
1520    }
1521
1522    public void actionPerformed(ActionEvent e) {
1523      load(resource, name);
1524    }
1525  }
1526
1527  /** Action that loads the current graph from a file in GraphML format. */
1528  protected class LoadAction extends AbstractLoadAction {
1529    JFileChooser chooser;
1530
1531    public LoadAction() {
1532      super("Load Model Graph");
1533      chooser = null;
1534    }
1535
1536    public void actionPerformed(ActionEvent e) {
1537      if (chooser == null) {
1538        chooser = new JFileChooser();
1539        chooser.setAcceptAllFileFilterUsed(false);
1540        chooser.addChoosableFileFilter(new FileFilter() {
1541          public boolean accept(File f) {
1542            return f.isDirectory() || f.getName().endsWith(".graphml");
1543          }
1544
1545          public String getDescription() {
1546            return "GraphML Format (.graphml)";
1547          }
1548        });
1549        chooser.addChoosableFileFilter(new FileFilter() {
1550          public boolean accept(File f) {
1551            return f.isDirectory() || f.getName().endsWith(".graphmlz");
1552          }
1553
1554          public String getDescription() {
1555            return "Zipped GraphML Format (.graphmlz)";
1556          }
1557        });
1558      }
1559      if (chooser.showOpenDialog(contentPane) == JFileChooser.APPROVE_OPTION) {
1560        try {
1561          final URL resource = chooser.getSelectedFile().toURI().toURL();
1562          final String ln = resource.getFile();
1563          load(ln, (new File(ln)).getName());
1564        } catch (MalformedURLException mue) {
1565          mue.printStackTrace();
1566        }
1567      }
1568    }
1569  }
1570
1571  /**
1572   * Action that saves the current graph to a file in GraphML or compressed
1573   * GraphML format.
1574   */
1575  protected class SaveAction extends AbstractAction {
1576    private JFileChooser chooser;
1577    private Graph2D graph;
1578
1579    /*
1580      Graph should be either the model graph or a view graph of the model.
1581     */
1582    public SaveAction(final String name, final Graph2D graph) {
1583      super(name);
1584      chooser = null;
1585      this.graph = graph;
1586    }
1587
1588    private void setFileChooser(final JFileChooser chooser, final File file) {
1589      FileFilter[] filters = chooser.getChoosableFileFilters();
1590      for (int i = 0; i < filters.length; i++) {
1591        if (filters[i].accept(file)) {
1592          chooser.setFileFilter(filters[i]);
1593          return;
1594        }
1595      }
1596    }
1597
1598    public void actionPerformed(ActionEvent e) {
1599      if (chooser == null) {
1600        chooser = new JFileChooser();
1601        chooser.setAcceptAllFileFilterUsed(false);
1602        chooser.addChoosableFileFilter(new FileFilter() {
1603          public boolean accept(File f) {
1604            return f.isDirectory() || f.getName().endsWith(".graphml");
1605          }
1606
1607          public String getDescription() {
1608            return "GraphML Format (.graphml)";
1609          }
1610        });
1611        chooser.addChoosableFileFilter(new FileFilter() {
1612          public boolean accept(File f) {
1613            return f.isDirectory() || f.getName().endsWith(".graphmlz");
1614          }
1615
1616          public String getDescription() {
1617            return "Zipped GraphML Format (.graphmlz)";
1618          }
1619        });
1620      }
1621
1622      final URL url = view.getGraph2D().getURL();
1623      if (url != null && "file".equals(url.getProtocol())) {
1624        try {
1625          final File file = new File(new URI(url.toString()));
1626          chooser.setSelectedFile(file);
1627          setFileChooser(chooser, file);
1628        } catch (URISyntaxException e1) {
1629          // ignore
1630        }
1631      }
1632
1633      if (chooser.showSaveDialog(contentPane) == JFileChooser.APPROVE_OPTION) {
1634        IOHandler ioh;
1635        String name = chooser.getSelectedFile().toString();
1636        final FileFilter filter = chooser.getFileFilter();
1637        if (filter.accept(new File("file.graphml"))) {
1638          if (!name.endsWith(".graphml")) {
1639            name += ".graphml";
1640          }
1641          ioh = createGraphMLIOHandler();
1642        } else {
1643          if (!name.endsWith(".graphmlz")) {
1644            name += ".graphmlz";
1645          }
1646          ioh = new ZipGraphMLIOHandler();
1647        }
1648
1649        try {
1650          ioh.write(graph, name);
1651        } catch (IOException ioe) {
1652          D.show(ioe);
1653        }
1654      }
1655    }
1656  }
1657
1658
1659  /**
1660   * Custom <code>ModelViewManager.Filter</code> filter implementation that rejects edge and node representatives from
1661   * being automatically created by a <code>ModelViewManager</code>, if the corresponding model element is not stored in
1662   * one of this filter's inclusion sets.
1663   */
1664  static final class InclusionFilter implements ModelViewManager.Filter {
1665    private final Set includedNodes;
1666    private final Set includedEdges;
1667
1668    private boolean allowRemovals;
1669
1670    InclusionFilter() {
1671      includedNodes = new HashSet();
1672      includedEdges = new HashSet();
1673      allowRemovals = true;
1674    }
1675
1676    void reset() {
1677      includedNodes.clear();
1678      includedEdges.clear();
1679    }
1680
1681    boolean getAllowRemovals() {
1682      return allowRemovals;
1683    }
1684
1685    void setAllowRemovals(final boolean allowRemovals) {
1686      this.allowRemovals = allowRemovals;
1687    }
1688
1689    boolean isIncluded(final Node modelNode) {
1690      return includedNodes.contains(modelNode);
1691    }
1692
1693    boolean includeNode(final Node modelNode) {
1694      return includedNodes.add(modelNode);
1695    }
1696
1697    boolean includeEdge(final Edge modelEdge) {
1698      return includedEdges.add(modelEdge);
1699    }
1700
1701    boolean excludeNode(final Node modelNode) {
1702      for(EdgeCursor ec = modelNode.edges(); ec.ok(); ec.next()) {
1703        includedEdges.remove(ec.edge());
1704      }
1705      return includedNodes.remove(modelNode);
1706    }
1707
1708    boolean excludeEdge(final Edge modelEdge) {
1709      return includedEdges.remove(modelEdge);
1710    }
1711
1712    public boolean acceptInsertion(final Node node) {
1713      return includedNodes.contains(node);
1714    }
1715
1716    public boolean acceptInsertion(final Edge edge) {
1717      return includedEdges.contains(edge);
1718    }
1719
1720    public boolean acceptRemoval(final Node node) {
1721      return allowRemovals;
1722    }
1723
1724    public boolean acceptRemoval(final Edge edge) {
1725      return allowRemovals;
1726    }
1727
1728    public boolean acceptRetention(final Node node) {
1729      return !allowRemovals || includedNodes.contains(node);
1730    }
1731
1732    public boolean acceptRetention(final Edge edge) {
1733      return !allowRemovals || includedEdges.contains(edge);
1734    }
1735  }
1736}
1737