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