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.application;
15  
16  import demo.view.DemoBase;
17  
18  import y.base.Edge;
19  import y.base.EdgeCursor;
20  import y.base.EdgeList;
21  import y.base.Node;
22  import y.base.NodeCursor;
23  import y.base.NodeList;
24  import y.base.YCursor;
25  import y.geom.YPoint;
26  import y.layout.Layouter;
27  import y.layout.LayoutOrientation;
28  import y.layout.hierarchic.incremental.EdgeLayoutDescriptor;
29  import y.layout.hierarchic.IncrementalHierarchicLayouter;
30  import y.util.GraphCopier;
31  import y.view.EditMode;
32  import y.view.Graph2D;
33  import y.view.Graph2DSelectionEvent;
34  import y.view.Graph2DSelectionListener;
35  import y.view.Graph2DView;
36  import y.view.HitInfo;
37  import y.view.LocalViewCreator.*;
38  import y.view.ViewMode;
39  import y.view.LocalViewCreator;
40  import y.view.ModelViewManager;
41  import y.view.NodeRealizer;
42  import y.view.hierarchy.GroupNodeRealizer;
43  import y.view.hierarchy.HierarchyManager;
44  
45  import java.awt.BorderLayout;
46  import java.awt.CardLayout;
47  import java.awt.Dimension;
48  import java.awt.EventQueue;
49  import java.awt.FlowLayout;
50  import java.awt.GridBagConstraints;
51  import java.awt.GridBagLayout;
52  import java.awt.Insets;
53  import java.awt.geom.Point2D;
54  import java.awt.event.ActionEvent;
55  import java.awt.event.ActionListener;
56  import java.net.URL;
57  import java.util.HashSet;
58  import java.util.Iterator;
59  import java.util.Locale;
60  
61  import javax.swing.AbstractAction;
62  import javax.swing.BorderFactory;
63  import javax.swing.Box;
64  import javax.swing.ButtonGroup;
65  import javax.swing.JPanel;
66  import javax.swing.JCheckBox;
67  import javax.swing.JComponent;
68  import javax.swing.JLabel;
69  import javax.swing.JRadioButton;
70  import javax.swing.JSpinner;
71  import javax.swing.JSplitPane;
72  import javax.swing.SpinnerNumberModel;
73  import javax.swing.Timer;
74  import javax.swing.Action;
75  import javax.swing.border.CompoundBorder;
76  import javax.swing.event.ChangeEvent;
77  import javax.swing.event.ChangeListener;
78  
79  /**
80   * Demonstrates local views, a feature that uses a given (model) graph to
81   * create a (hopefully smaller) graph which emphasizes a certain aspect of
82   * the original graph. There are several predefined policies to create local
83   * views:
84   * <ul>
85   * <li>Neighborhood: For a given set of nodes, display all nodes that are
86   * reachable by a edge path up to a certain length.</li>
87   * <li>Common Parent Group: For a given set of nodes, display all nodes that
88   * share the same parent group node as one of the given nodes.</li>
89   * <li>AncestorGroups: For given set of nodes, display all ancestor group nodes.
90   * </li>
91   * <li>Folder Contents: For a given set of folder nodes, display the folders'
92   * inner graphs.</li>
93   * <li>Source and Target: For a given set of edges, display the source and
94   * target nodes.</li>
95   * <li>Edge Group: For a given set of edges, display all edges that share
96   * source and/or target with one of the original edges.</li>
97   * </ul>
98   *
99   * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/viewer_localviews.html">Section Local Views</a> in the yFiles for Java Developer's Guide
100  */
101 public class LocalViewDemo extends DemoBase {
102   /**
103    * The delay in milliseconds before a local view update
104    * is performed after a trigger event.
105    */
106   private static final int TIMER_DELAY = 100;
107 
108   /**
109    * Trigger type constant that represents the selection trigger.
110    * @see demo.view.application.LocalViewDemo.SelectionTrigger
111    */
112   private static final byte SELECTION_TRIGGER = 1;
113   /**
114    * Trigger type constant that represents the hover trigger.
115    * @see demo.view.application.LocalViewDemo.HoverTrigger
116    */
117   private static final byte HOVER_TRIGGER = 2;
118 
119 
120   private final Graph2DView localView;
121 
122   // node related local view creators
123   private final Neighborhood neighborhood;
124   private final CommonParentGroup commonParentGroup;
125   private final AncestorGroups ancestorGroups;
126   private final FolderContents folderContents;
127 
128   // edge related local view creators
129   private final SourceAndTarget sourceAndTarget;
130   private final EdgeGroup edgeGroup;
131 
132   // custom local view creator
133   private final SelectedSubgraph selectedSubgraph;
134 
135   // the local view creator responsible for the current local view
136   private LocalViewCreator currentLocalViewCreator;
137   // the selected local view creator for node related local view
138   private AbstractLocalViewCreator nodeLocalViewCreator;
139   // the selected local view creator for edge related local view
140   private AbstractLocalViewCreator edgeLocalViewCreator;
141 
142 
143   // reusable instance of selection trigger, i.e. a Graph2DSelectionListener
144   // that triggers local view updates on selection events
145   private final SelectionTrigger selectionTrigger;
146   // reusable instance of hover trigger, i.e. a ViewMode
147   // that triggers local view updates when the mouse hovers over graph elements
148   private final HoverTrigger hoverTrigger;
149 
150   // used to keep track of the current trigger for local view updates
151   private byte triggerType;
152 
153 
154   public LocalViewDemo() {
155     this(null);
156   }
157 
158   public LocalViewDemo( final String helpFilePath ) {
159     final Graph2D graph = view.getGraph2D();
160     (new HierarchyManager(graph)).addHierarchyListener(
161             new GroupNodeRealizer.StateChangeListener());
162 
163 
164     // instantiate several local view creators that share the current view's
165     // graph as model as well as the graph used as local view
166     // the factory of the current view's graph is used to actually populate
167     // the local view with elements
168     final GraphCopier.CopyFactory factory = graph.getGraphCopyFactory();
169     final Graph2D localViewGraph = (Graph2D) factory.createGraph();
170 
171     localView = new Graph2DView(localViewGraph);
172     localView.setPreferredSize(new Dimension(320, 240));
173 
174     final Layouter layouter = createLayouter();
175     neighborhood = new Neighborhood(graph, factory, localViewGraph);
176     neighborhood.setLayouter(layouter);
177     commonParentGroup = new CommonParentGroup(graph, factory, localViewGraph);
178     ancestorGroups = new AncestorGroups(graph, factory, localViewGraph);
179     folderContents = new FolderContents(graph, factory, localViewGraph);
180 
181     sourceAndTarget = new SourceAndTarget(graph, factory, localViewGraph);
182     sourceAndTarget.setLayouter(layouter);
183     edgeGroup = new EdgeGroup(graph, factory, localViewGraph);
184     edgeGroup.setLayouter(layouter);
185 
186     selectedSubgraph = new SelectedSubgraph(graph, factory, localViewGraph);
187 
188 
189     setEdgeLocalViewCreator(sourceAndTarget);
190     setNodeLocalViewCreator(neighborhood);
191 
192 
193     // load a sample graph
194     loadGraph("resource/LocalViewDemo.graphml");
195 
196 
197     // add a ViewMode to the localView that upon double-clicking on items
198     // will select and focus the corresponding item in the original view
199     localView.addViewMode(new ViewMode() {
200       public void mouseClicked(double x, double y) {
201         // double click?
202         if (lastClickEvent.getClickCount() == 2) {
203           final HitInfo hitInfo = getHitInfo(x, y);
204           final Graph2DView view = LocalViewDemo.this.view;
205           final Graph2D viewGraph = view.getGraph2D();
206           // did we click on a node?
207           if (hitInfo.getHitNode() != null) {
208             // find the original node in the "model"
209             final LocalViewCreator lvc = getCurrentLocalViewCreator();
210             final Node modelNode = lvc.getModelNode(hitInfo.getHitNode());
211             if (modelNode != null) {
212               if (modelNode.getGraph() == viewGraph) {
213                 // we found one, so select and focus it
214                 viewGraph.unselectAll();
215                 final NodeRealizer nr = viewGraph.getRealizer(modelNode);
216                 Point2D.Double center = new Point2D.Double(nr.getCenterX(), nr.getCenterY());
217                 double minZoom = Math.min(view.getWidth() / (nr.getWidth() + 40),
218                     view.getHeight() / (nr.getHeight() + 40));
219                 view.focusView(Math.min(minZoom, view.getZoom()), center, true);
220                 viewGraph.setSelected(modelNode, true);
221                 viewGraph.updateViews();
222               }
223             }
224           } else if (hitInfo.getHitEdge() != null) { // we clicked on an edge
225             // find the original one
226             final LocalViewCreator lvc = getCurrentLocalViewCreator();
227             final Edge modelEdge = lvc.getModelEdge(hitInfo.getHitEdge());
228             if (modelEdge != null) {
229               if(modelEdge.getGraph() == viewGraph) {
230                 // found one - so select and focus source and target nodes
231                 final NodeRealizer snr = viewGraph.getRealizer(modelEdge.source());
232                 final NodeRealizer tnr = viewGraph.getRealizer(modelEdge.target());
233                 Point2D.Double sCenter = new Point2D.Double(snr.getCenterX(), snr.getCenterY());
234                 Point2D.Double tCenter = new Point2D.Double(tnr.getCenterX(), tnr.getCenterY());
235                 double minZoom =
236                     Math.min(
237                         view.getWidth()/(snr.getWidth() + tnr.getWidth() + Math.abs(sCenter.x - tCenter.x)),
238                         view.getHeight() / (snr.getHeight() + tnr.getHeight() + Math.abs(sCenter.y - tCenter.y)));
239                 view.focusView(Math.min(view.getZoom(), minZoom), new Point2D.Double((sCenter.x + tCenter.x) * 0.5d, (sCenter.y + tCenter.y) * 0.5d), true);
240                 viewGraph.unselectAll();
241                 viewGraph.setSelected(modelEdge, true);
242                 viewGraph.updateViews();
243               }
244             }
245           }
246         }
247       }
248     });
249 
250     localView.setFitContentOnResize(true);
251     
252     selectionTrigger = new SelectionTrigger();
253     hoverTrigger = new HoverTrigger();
254 
255     final JSplitPane localViewAndSettings = new JSplitPane(
256             JSplitPane.VERTICAL_SPLIT, localView, createSettingsComponent());
257     localViewAndSettings.setBorder(BorderFactory.createEmptyBorder());
258     localViewAndSettings.setResizeWeight(1);
259     final JSplitPane center = new JSplitPane(
260             JSplitPane.HORIZONTAL_SPLIT, localViewAndSettings, view);
261     center.setBorder(BorderFactory.createEmptyBorder());
262     center.setResizeWeight(0);
263     
264     contentPane.add(center, BorderLayout.CENTER);
265     addHelpPane(helpFilePath);
266 
267     // set the initial trigger for local view updates
268     setTrigger(SELECTION_TRIGGER);
269 
270 
271     // set an initial selection
272     if (!graph.isEmpty()) {
273       graph.unselectAll();
274       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
275         if (nc.node().inDegree() > 0 && nc.node().outDegree() > 0) {
276           graph.setSelected(nc.node(), true);
277           break;
278         }
279       }
280       if (graph.isSelectionEmpty()) {
281         graph.setSelected(graph.firstNode(), true);
282       }
283       graph.updateViews();
284     }
285   }
286 
287   /**
288    * Overwritten to disable node and edge creation.
289    */
290   protected EditMode createEditMode() {
291      EditMode editMode = super.createEditMode();
292      editMode.allowEdgeCreation(false);
293      editMode.allowNodeCreation(false);
294      return editMode;
295   }
296 
297   /**
298    * Overwritten to disable saving graphs.
299    */
300   protected Action createSaveAction() {
301     return null;
302   }
303 
304   /**
305    * Overwritten to clear local view upon loading.
306    */
307   protected void loadGraph( final URL resource ) {
308     if (resource != null && localView != null) {
309       localView.getGraph2D().clear();
310     }
311 
312     super.loadGraph(resource);
313 
314     if (resource != null && localView != null) {
315       localView.updateView();
316     }
317   }
318 
319   /**
320    * Sets the trigger for local view updates represented by the specified type
321    * constant.
322    * @param triggerType either {@link #SELECTION_TRIGGER} representing
323    * {@link #selectionTrigger} or {@link #HOVER_TRIGGER} representing
324    * {@link #hoverTrigger}.
325    * @see demo.view.application.LocalViewDemo.HoverTrigger
326    * @see demo.view.application.LocalViewDemo.SelectionTrigger
327    */
328   private void setTrigger( final byte triggerType ) {
329     // remove the old trigger
330     switch (this.triggerType) {
331       case SELECTION_TRIGGER:
332         view.getGraph2D().removeGraph2DSelectionListener(selectionTrigger);
333         break;
334       case HOVER_TRIGGER:
335         view.removeViewMode(hoverTrigger);
336         break;
337     }
338 
339     this.triggerType = triggerType;
340 
341     // add the new trigger
342     switch (this.triggerType) {
343       case SELECTION_TRIGGER:
344         view.getGraph2D().addGraph2DSelectionListener(selectionTrigger);
345         break;
346       case HOVER_TRIGGER:
347         view.addViewMode(hoverTrigger);
348         break;
349     }
350   }
351 
352 
353   /**
354    * Specifies the local view creator for edge related local views.
355    * @param elvc   the new edge related local view creator.
356    */
357   private void setEdgeLocalViewCreator( final AbstractLocalViewCreator elvc ) {
358     final boolean update = currentLocalViewCreator == edgeLocalViewCreator;
359 
360     edgeLocalViewCreator = elvc;
361 
362     // update the demo's local view if the last view creator was edge related
363     updateEdgeViewImpl(update);
364   }
365 
366   /**
367    * Refreshes the demo's local view if the current local view creator is
368    * node related.
369    */
370   private void updateEdgeView() {
371     updateEdgeViewImpl(currentLocalViewCreator == edgeLocalViewCreator);
372   }
373 
374   private void updateEdgeViewImpl( final boolean update ) {
375     if (update) {
376       if (SELECTION_TRIGGER == triggerType) {
377         createEdgeView(view.getGraph2D().selectedEdges());
378       } else {
379         createEdgeView(new EdgeList().edges());
380       }
381     }
382   }
383 
384   /**
385    * Specifies the local view creator for node related local views.
386    * @param nlvc   the new node related local view creator.
387    */
388   private void setNodeLocalViewCreator( final AbstractLocalViewCreator nlvc ) {
389     final boolean update = currentLocalViewCreator == nodeLocalViewCreator;
390 
391     nodeLocalViewCreator = nlvc;
392 
393     // update the demo's local view if the last view creator was node related
394     updateNodeViewImpl(update);
395   }
396 
397   /**
398    * Refreshes the demo's local view if the current local view creator is
399    * edge related.
400    */
401   private void updateNodeView() {
402     updateNodeViewImpl(currentLocalViewCreator == nodeLocalViewCreator);
403   }
404 
405   private void updateNodeViewImpl( final boolean update ) {
406     if (update) {
407       if (SELECTION_TRIGGER == triggerType) {
408         createNodeView(view.getGraph2D().selectedNodes());
409       } else {
410         createNodeView((new NodeList().nodes()));
411       }
412     }
413   }
414 
415   /**
416    * Updates the demo's local view using the specified edges and the currently
417    * selected edge related local view creator.
418    * @param edges   a cursor over a collection of edges.
419    */
420   private void createEdgeView( final YCursor edges ) {
421     // mark the selected local view creator for edges as the currently active
422     // local view creator
423     currentLocalViewCreator = edgeLocalViewCreator;
424 
425     // replaced the local view creator's focus elements with the passed in
426     // edges
427     edgeLocalViewCreator.clearFocusEdges();
428     for (; edges.ok(); edges.next()) {
429       edgeLocalViewCreator.addFocusEdge((Edge) edges.current());
430     }
431 
432     // update the demo's local view
433     // the create call will ...
434     //    ... clear the creator's associated view graph
435     //    ... creates new elements in the creator's associated view graph
436     //    ... lays out the creator's associated view graph
437     //    ... and finally calls updateView for all Views associated to
438     //        the creator's associated view graph (and fitContent for all of
439     //        these that are of type Graph2DView)
440     edgeLocalViewCreator.updateViewGraph();
441   }
442 
443   /**
444    * Updates the demo's local view using the specified nodes and the currently
445    * selected node related local view creator.
446    * @param nodes   a cursor over a collection of nodes.
447    */
448   private void createNodeView( final YCursor nodes ) {
449     // mark the selected local view creator for nodes as the currently active
450     // local view creator
451     currentLocalViewCreator = nodeLocalViewCreator;
452 
453     // replaced the local view creator's focus elements with the passed in
454     // nodes
455     nodeLocalViewCreator.clearFocusNodes();
456     for (; nodes.ok(); nodes.next()) {
457       nodeLocalViewCreator.addFocusNode((Node) nodes.current());
458     }
459 
460     // update the demo's local view
461     // the create call will ...
462     //    ... clear the creator's associated view graph
463     //    ... creates new elements in the creator's associated view graph
464     //    ... lays out the creator's associated view graph
465     //    ... and finally calls updateView for all Views associated to
466     //        the creator's associated view graph (and fitContent for all of
467     //        these that are of type Graph2DView)
468     nodeLocalViewCreator.updateViewGraph();
469   }
470 
471   /**
472    * Returns the local view creator responsible for the current local view.
473    * @return the local view creator responsible for the current local view.
474    */
475   private LocalViewCreator getCurrentLocalViewCreator() {
476     return currentLocalViewCreator;
477   }
478 
479   /**
480    * Creates user interface controls for the various settings of the available
481    * local view creators.
482    * @return user interface controls for the various settings of the available
483    * local view creators.
484    */
485   private JComponent createSettingsComponent() {
486     final JPanel settingsPane = new JPanel(new CardLayout());
487 
488     final Box nodeStrategyButtons = Box.createVerticalBox();
489     nodeStrategyButtons.setBorder(BorderFactory.createTitledBorder("Node Strategies"));
490 
491     final ButtonGroup nodeStrategiesGroup = new ButtonGroup();
492 
493     // controls for selecting the node related view creators
494     addStrategy(settingsPane, nodeStrategyButtons, nodeStrategiesGroup, "Neighborhood", createNeighborhoodSettings(), neighborhood, true);
495     addStrategy(settingsPane, nodeStrategyButtons, nodeStrategiesGroup, "Common Parent Group", createCommonParentGroupSettings(), commonParentGroup, true);
496     addStrategy(settingsPane, nodeStrategyButtons, nodeStrategiesGroup, "Ancestor Groups", createAncestorGroupsSettings(), ancestorGroups, true);
497     addStrategy(settingsPane, nodeStrategyButtons, nodeStrategiesGroup, "Folder Contents", createFolderContentsSettings(), folderContents, true);
498     addStrategy(settingsPane, nodeStrategyButtons, nodeStrategiesGroup, "Selected Subgraph", null, selectedSubgraph, true);
499 
500 
501     final ButtonGroup edgeStrategiesGroup = new ButtonGroup();
502     final Box edgeStrategyButtons = Box.createVerticalBox();
503     edgeStrategyButtons.setBorder(BorderFactory.createTitledBorder("Edge Strategies"));
504 
505     // controls for selecting the edge related view creators
506     addStrategy(settingsPane, edgeStrategyButtons, edgeStrategiesGroup, "Source and Target", createSourceAndTargetSettings(), sourceAndTarget, false);
507     addStrategy(settingsPane, edgeStrategyButtons, edgeStrategiesGroup, "Edge Group", createEdgeGroupSettings(), edgeGroup, false);
508 
509 
510     Box container = Box.createVerticalBox();
511     container.add(createTriggerPane());
512 
513     // put all the controls together and ensure nice resizing behavior
514     final GridBagConstraints gbc = new GridBagConstraints();
515     final JPanel strategies = new JPanel(new GridBagLayout());
516     gbc.anchor = GridBagConstraints.NORTHWEST;
517     gbc.fill = GridBagConstraints.HORIZONTAL;
518     gbc.gridx = 0;
519     gbc.weightx = 1;
520     gbc.weighty = 0;
521     strategies.add(nodeStrategyButtons, gbc);
522     strategies.add(edgeStrategyButtons, gbc);
523     gbc.fill = GridBagConstraints.BOTH;
524     gbc.weighty = 1;
525     strategies.add(Box.createGlue(), gbc);
526 
527     Box strategy = Box.createHorizontalBox();
528     strategy.add(strategies);
529     strategy.add(settingsPane);
530 
531     container.add(strategy);
532     return container;
533   }
534 
535   /**
536      * Creates a user control for the {@link y.view.LocalViewCreator.SourceAndTarget}
537      * local view creator.
538      * @return a user control for the {@link y.view.LocalViewCreator.SourceAndTarget}
539      * local view creator.
540      */
541     private JComponent createSourceAndTargetSettings() {
542     final JCheckBox hierarchyAware = new JCheckBox("Hierarchy aware");
543     hierarchyAware.addActionListener(new ActionListener() {
544       public void actionPerformed(ActionEvent e) {
545         sourceAndTarget.setHierarchyAware(hierarchyAware.isSelected());
546         updateEdgeView();
547       }
548     });
549 
550     final Box settings = Box.createVerticalBox();
551     settings.add(hierarchyAware);
552     settings.add(Box.createGlue());
553     return settings;
554   }
555 
556   /**
557    * Creates user controls for the {@link y.view.LocalViewCreator.EdgeGroup}
558    * local view creator.
559    * @return user controls for the {@link y.view.LocalViewCreator.EdgeGroup}
560    * local view creator.
561    */
562   private JComponent createEdgeGroupSettings() {
563     final JCheckBox src = new JCheckBox("Source");
564     src.setSelected(true);
565     final JCheckBox tgt = new JCheckBox("Target");
566     tgt.setSelected(true);
567     final AbstractAction groupTypes = new AbstractAction() {
568       public void actionPerformed( final ActionEvent e ) {
569         src.setEnabled(tgt.isSelected());
570         tgt.setEnabled(src.isSelected());
571         byte types = 0;
572         if (src.isSelected()) {
573           types |= EdgeGroup.GROUP_BY_SOURCE;
574         }
575         if (tgt.isSelected()) {
576           types |= EdgeGroup.GROUP_BY_TARGET;
577         }
578         edgeGroup.setGroupByPolicy(types);
579         updateEdgeView();
580       }
581     };
582     src.addActionListener(groupTypes);
583     tgt.addActionListener(groupTypes);
584 
585     final Box settings = Box.createVerticalBox();
586     settings.add(new JLabel("Group by"));
587     settings.add(src);
588     settings.add(tgt);
589     settings.add(Box.createGlue());
590     return settings;
591   }
592 
593   /**
594    * Creates user controls for the {@link y.view.LocalViewCreator.FolderContents}
595    * local view creator.
596    * @return user controls for the {@link y.view.LocalViewCreator.FolderContents}
597    * local view creator.
598    */
599   private JComponent createFolderContentsSettings() {
600     final ButtonGroup includeFoldersGroup = new ButtonGroup();
601     final JRadioButton always = new JRadioButton("Always Include Folders");
602     always.addActionListener(new ActionListener() {
603       public void actionPerformed( final ActionEvent e ) {
604         folderContents.setFolderPolicy(FolderContents.FOLDER_POLICY_ALWAYS);
605         updateNodeView();
606       }
607     });
608     includeFoldersGroup.add(always);
609     final JRadioButton asNeeeded = new JRadioButton("Include Folders As Needed");
610     asNeeeded.setSelected(true);
611     asNeeeded.addActionListener(new ActionListener() {
612       public void actionPerformed( final ActionEvent e ) {
613         folderContents.setFolderPolicy(FolderContents.FOLDER_POLICY_AS_NEEDED);
614         updateNodeView();
615       }
616     });
617     includeFoldersGroup.add(asNeeeded);
618     final JRadioButton never = new JRadioButton("Never Include Folders");
619     never.addActionListener(new ActionListener() {
620       public void actionPerformed( final ActionEvent e ) {
621         folderContents.setFolderPolicy(FolderContents.FOLDER_POLICY_NEVER);
622         updateNodeView();
623       }
624     });
625     includeFoldersGroup.add(never);
626 
627     final Box settings = Box.createVerticalBox();
628     settings.add(always);
629     settings.add(asNeeeded);
630     settings.add(never);
631     settings.add(Box.createGlue());
632     return settings;
633   }
634 
635   /**
636    * Creates user controls for the {@link y.view.LocalViewCreator.AncestorGroups}
637    * local view creator.
638    * @return user controls for the {@link y.view.LocalViewCreator.AncestorGroups}
639    * local view creator.
640    */
641   private JComponent createAncestorGroupsSettings() {
642     final JCheckBox includeFocusNodes = new JCheckBox("Include Focus Nodes");
643     includeFocusNodes.setSelected(ancestorGroups.isIncludeFocusNodes());
644     includeFocusNodes.addActionListener(new AbstractAction() {
645       public void actionPerformed( final ActionEvent e ) {
646         ancestorGroups.setIncludeFocusNodes(includeFocusNodes.isSelected());
647         updateNodeView();
648       }
649     });
650 
651     final Box settings = Box.createVerticalBox();
652     settings.add(includeFocusNodes);
653     settings.add(Box.createGlue());
654     return settings;
655   }
656 
657   /**
658    * Creates user controls for the {@link y.view.LocalViewCreator.CommonParentGroup}
659    * local view creator.
660    * @return user controls for the {@link y.view.LocalViewCreator.CommonParentGroup}
661    * local view creator.
662    */
663   private JComponent createCommonParentGroupSettings() {
664     final JCheckBox includeDescendants = new JCheckBox("Include Descendants");
665     includeDescendants.setSelected(commonParentGroup.isIncludeDescendants());
666     includeDescendants.addActionListener(new AbstractAction() {
667       public void actionPerformed( final ActionEvent e ) {
668         commonParentGroup.setIncludeDescendants(includeDescendants.isSelected());
669         updateNodeView();
670       }
671     });
672 
673     final Box settings = Box.createVerticalBox();
674     settings.add(includeDescendants);
675     settings.add(Box.createGlue());
676     return settings;
677   }
678 
679   /**
680    * Creates user controls for the {@link y.view.LocalViewCreator.Neighborhood}
681    * local view creator.
682    * @return user controls for the {@link y.view.LocalViewCreator.Neighborhood}
683    * local view creator.
684    */
685   private JComponent createNeighborhoodSettings() {
686     final ButtonGroup nodesGroup = new ButtonGroup();
687     final JRadioButton preds = new JRadioButton("Predecessors");
688     preds.addActionListener(new ActionListener() {
689       public void actionPerformed( final ActionEvent e ) {
690         neighborhood.setNeighborhoodType(Neighborhood.NEIGHBORHOOD_TYPE_PREDECESSORS);
691         updateNodeView();
692       }
693     });
694     nodesGroup.add(preds);
695     final JRadioButton succs = new JRadioButton("Successors");
696     succs.addActionListener(new ActionListener() {
697       public void actionPerformed( final ActionEvent e ) {
698         neighborhood.setNeighborhoodType(Neighborhood.NEIGHBORHOOD_TYPE_SUCCESSORS);
699         updateNodeView();
700       }
701     });
702     nodesGroup.add(succs);
703     final JRadioButton predsSuccs = new JRadioButton("Predecessors and Successors");
704     predsSuccs.setSelected(true);
705     predsSuccs.addActionListener(new ActionListener() {
706       public void actionPerformed( final ActionEvent e ) {
707         neighborhood.setNeighborhoodType(Neighborhood.NEIGHBORHOOD_TYPE_PREDECESSORS_AND_SUCCESSORS);
708         updateNodeView();
709       }
710     });
711     nodesGroup.add(predsSuccs);
712     final JRadioButton neighbors = new JRadioButton("All Neighbors");
713     neighbors.addActionListener(new ActionListener() {
714       public void actionPerformed( final ActionEvent e ) {
715         neighborhood.setNeighborhoodType(Neighborhood.NEIGHBORHOOD_TYPE_NEIGHBORS);
716         updateNodeView();
717       }
718     });
719     nodesGroup.add(neighbors);
720 
721     final JSpinner neighborhoodMGD = new JSpinner();
722     neighborhoodMGD.getModel().setValue(new Integer(neighborhood.getMaximumGraphDistance()));
723     ((SpinnerNumberModel) neighborhoodMGD.getModel()).setMinimum(new Integer(0));
724     neighborhoodMGD.addChangeListener(new ChangeListener() {
725       public void stateChanged( final ChangeEvent e ) {
726         final Object value = neighborhoodMGD.getModel().getValue();
727         if (value instanceof Integer) {
728           final int mgd = ((Integer) value).intValue();
729           if (mgd > -1) {
730             neighborhood.setMaximumGraphDistance(mgd);
731             updateNodeView();
732           }
733         }
734       }
735     });
736 
737     final JCheckBox hierarchyAware = new JCheckBox("Hierarchy aware");
738     hierarchyAware.addActionListener(new ActionListener() {
739       public void actionPerformed(ActionEvent e) {
740         neighborhood.setHierarchyAware(hierarchyAware.isSelected());
741         updateNodeView();
742       }
743     });
744 
745     final ButtonGroup edgesGroup = new ButtonGroup();
746     final JRadioButton none = new JRadioButton("None");
747     none.addActionListener(new ActionListener() {
748       public void actionPerformed( final ActionEvent e ) {
749         neighborhood.setEdgePolicy(Neighborhood.EDGE_POLICY_NONE);
750         updateNodeView();
751       }
752     });
753     edgesGroup.add(none);
754     final JRadioButton subgraph = new JRadioButton("Subgraph Edges");
755     subgraph.addActionListener(new ActionListener() {
756       public void actionPerformed( final ActionEvent e ) {
757         neighborhood.setEdgePolicy(Neighborhood.EDGE_POLICY_INDUCED_SUBGRAPH);
758         updateNodeView();
759       }
760     });
761     edgesGroup.add(subgraph);
762     final JRadioButton shortestPaths = new JRadioButton("Shortest Paths");
763     shortestPaths.setSelected(true);
764     shortestPaths.addActionListener(new ActionListener() {
765       public void actionPerformed( final ActionEvent e ) {
766         neighborhood.setEdgePolicy(Neighborhood.EDGE_POLICY_SHORTEST_PATHS);
767         updateNodeView();
768       }
769     });
770     edgesGroup.add(shortestPaths);
771 
772 
773     final Box nodeTypes = Box.createVerticalBox();
774     nodeTypes.add(preds);
775     nodeTypes.add(succs);
776     nodeTypes.add(predsSuccs);
777     nodeTypes.add(neighbors);
778 
779     final GridBagConstraints gbc = new GridBagConstraints();
780     final JPanel includedNodes = new JPanel(new GridBagLayout());
781     includedNodes.setBorder(BorderFactory.createTitledBorder("Included Nodes"));
782     gbc.anchor = GridBagConstraints.NORTHWEST;
783     gbc.fill = GridBagConstraints.HORIZONTAL;
784     gbc.gridx = 0;
785     gbc.gridy = 0;
786     gbc.gridwidth = 2;
787     gbc.gridheight = 1;
788     gbc.weightx = 1;
789     gbc.weighty = 0;
790     includedNodes.add(nodeTypes, gbc);
791     gbc.fill = GridBagConstraints.NONE;
792     gbc.gridy = 1;
793     gbc.gridwidth = 1;
794     gbc.insets = new Insets(5, 0, 0, 0);
795     gbc.weightx = 0;
796     includedNodes.add(new JLabel("Maximum Graph Distance"), gbc);
797     gbc.fill = GridBagConstraints.HORIZONTAL;
798     gbc.gridx = 1;
799     gbc.insets = new Insets(5, 5, 0, 0);
800     gbc.weightx = 1;
801     includedNodes.add(neighborhoodMGD, gbc);
802     gbc.gridwidth = 2;
803     gbc.gridx = 0;
804     gbc.gridy = 2;
805     gbc.insets = new Insets(0, 0, 5, 0);
806     includedNodes.add(hierarchyAware, gbc);
807 
808     final Box includedEdges = Box.createVerticalBox();
809     includedEdges.setBorder(BorderFactory.createTitledBorder("Included Edges"));
810     includedEdges.add(none);
811     includedEdges.add(subgraph);
812     includedEdges.add(shortestPaths);
813 
814     final JPanel controls = new JPanel(new GridBagLayout());
815     gbc.gridx = 0;
816     gbc.gridy = 0;
817     gbc.gridwidth = 1;
818     gbc.gridheight = 1;
819     gbc.weightx = 1;
820     gbc.weighty = 0;
821     controls.add(includedNodes, gbc);
822     gbc.gridy = 1;
823     controls.add(includedEdges, gbc);
824 
825     final JPanel settings = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
826     settings.add(controls);
827     return settings;
828   }
829 
830   /**
831    * Creates user controls for selecting the update trigger of the demo's local
832    * view.
833    * @return user controls for selecting the update trigger of the demo's local
834    * view.
835    */
836   private JComponent createTriggerPane() {
837     final ButtonGroup updateGroup = new ButtonGroup();
838     final JRadioButton updateHover = new JRadioButton(new AbstractAction("Mouse Hover") {
839       public void actionPerformed( final ActionEvent e ) {
840         setTrigger(HOVER_TRIGGER);
841       }
842     });
843     updateGroup.add(updateHover);
844     final JRadioButton updateSelection = new JRadioButton(new AbstractAction("Selection Change") {
845       public void actionPerformed( final ActionEvent e ) {
846         setTrigger(SELECTION_TRIGGER);
847       }
848     });
849     updateSelection.setSelected(true);
850     updateGroup.add(updateSelection);
851 
852     final Box updatePane = Box.createHorizontalBox();
853     updatePane.add(Box.createHorizontalStrut(5));
854     updatePane.add(new JLabel("Update Trigger: "));
855     updatePane.add(updateHover);
856     updatePane.add(updateSelection);
857     updatePane.add(Box.createGlue());
858     return updatePane;
859   }
860 
861   /**
862    * Adds the user controls for the specified local view creator.
863    * @param title   the display name of the specified local view creator.
864    * @param settings   the user controls for the specified local view creator's
865    * specific settings.
866    * @param lvc   the local view creator for which user controls are added.
867    * @param nodeBased   <code>true</code> if the specified local view creator
868    * is node related, <code>false</code> otherwise.
869    */
870   private void addStrategy(
871           final JPanel settingsPane,
872           final Box strategyButtons,
873           final ButtonGroup strategiesGroup,
874           final String title,
875           JComponent settings,
876           final AbstractLocalViewCreator lvc,
877           final boolean nodeBased
878   ) {
879     if (settings == null) {
880       settings = new JPanel();
881       settings.add(new JLabel("No Settings"));
882     }
883     final JRadioButton strategyButton = new JRadioButton(new AbstractAction(title) {
884       public void actionPerformed(ActionEvent e) {
885         ((CardLayout) settingsPane.getLayout()).show(settingsPane, title);
886         if (nodeBased) {
887           setNodeLocalViewCreator(lvc);
888         } else {
889           setEdgeLocalViewCreator(lvc);
890         }
891       }
892     });
893 
894     final CompoundBorder border = BorderFactory.createCompoundBorder(
895         BorderFactory.createTitledBorder(title + " Settings"),
896         BorderFactory.createEmptyBorder(5, 5, 5, 5));
897     settings.setBorder(border);
898     settingsPane.add(settings, title);
899 
900     strategiesGroup.add(strategyButton);
901     if (strategiesGroup.getButtonCount() == 1) {
902       strategiesGroup.setSelected(strategyButton.getModel(), true);
903     }
904     strategyButtons.add(strategyButton);
905   }
906 
907   /**
908    * Creates a layouter that can be used to lay out the contents of a local view
909    * after an local view update.
910    * @return a layouter that can be used to lay out the contents of a local view
911    * after an local view update.
912    */
913   private Layouter createLayouter() {
914     // the sample graph should be an UML inheritance diagram which means
915     // a hierachical layout style is well suited to lay out all or parts
916     // of such diagrams
917     final IncrementalHierarchicLayouter layouter = new IncrementalHierarchicLayouter();
918     layouter.setLayoutOrientation(LayoutOrientation.BOTTOM_TO_TOP);
919 
920     // specify the routing style for edges and minimum edge lengths
921     // to produce "nice" local views
922     final EdgeLayoutDescriptor eld = layouter.getEdgeLayoutDescriptor();
923     eld.setOrthogonallyRouted(true);
924     eld.setMinimumFirstSegmentLength(25);
925     eld.setMinimumLength(35);
926     return layouter;
927   }
928 
929   public static void main( String[] args ) {
930     EventQueue.invokeLater(new Runnable() {
931       public void run() {
932         Locale.setDefault(Locale.ENGLISH);
933         initLnF();
934         (new LocalViewDemo("resource/localviewhelp.html")).start();
935       }
936     });
937   }
938 
939   /**
940    * A <code>Graph2DSelectionListener</code> that triggers an update of the
941    * demo's local view upon selection changes.
942    */
943   private final class SelectionTrigger implements Graph2DSelectionListener {
944     private final Timer timer;
945     private Graph2DSelectionEvent lastEvent;
946 
947     SelectionTrigger() {
948       timer = new Timer(TIMER_DELAY, new ActionListener() {
949         public void actionPerformed( final ActionEvent e ) {
950           if (lastEvent != null) {
951             handleEvent(lastEvent);
952           }
953         }
954 
955         /**
956          * Triggers the actual update for the demo's local view.
957          */
958         private void handleEvent( final Graph2DSelectionEvent e ) {
959           if (e.isNodeSelection()) {
960             createNodeView(e.getGraph2D().selectedNodes());
961           } else if (e.isEdgeSelection()) {
962             createEdgeView(e.getGraph2D().selectedEdges());
963           }
964         }
965       });
966       timer.setRepeats(false);
967     }
968 
969     public void onGraph2DSelectionEvent( final Graph2DSelectionEvent e ) {
970       if (e.getGraph2D() == getCurrentLocalViewCreator().getModel()) {
971         if (e.isNodeSelection() || e.isEdgeSelection()) {
972           lastEvent = e;
973           timer.restart();
974         }
975       }
976     }
977   }
978 
979   /**
980    * A <code>ViewMode</code> that triggers an update of the demo's local view
981    * whenever the mouse hovers over graph elements for a certain amount of time.
982    */
983   private final class HoverTrigger extends ViewMode {
984     private final Timer timer;
985     private YPoint lastPosition;
986 
987     private HoverTrigger() {
988       timer = new Timer(TIMER_DELAY, new ActionListener() {
989         public void actionPerformed( ActionEvent e) {
990           if (lastPosition != null) {
991             handleHit(getHitInfo(lastPosition.x, lastPosition.y));
992           }
993         }
994 
995         /**
996          * Triggers the actual update for the demo's local view.
997          */
998         private void handleHit( final HitInfo hitInfo ) {
999           if (hitInfo.hasHitNodes()) {
1000            createNodeView(hitInfo.hitNodes());
1001          } else if (hitInfo.hasHitEdges()) {
1002            createEdgeView(hitInfo.hitEdges());
1003          }
1004        }
1005      });
1006      timer.setRepeats(false);
1007    }
1008
1009    public void mouseMoved(double x, double y) {
1010      super.mouseMoved(x, y);
1011      this.lastPosition = new YPoint(x, y);
1012      timer.restart();
1013    }
1014  }
1015
1016  /**
1017   * Custom local view creator that displays the subgraph that is induced by
1018   * the currently selected nodes of the creator's focus nodes set.
1019   * This class serves as a sample implementation of a simple strategy
1020   * for creating local views.
1021   */
1022  private static final class SelectedSubgraph extends AbstractLocalViewCreator {
1023    private final Graph2D model;
1024    private final GraphCopier.CopyFactory factory;
1025    private final Graph2D view;
1026
1027    /**
1028     * Creates a new <code>SelectedSubgraph</code> instance
1029     * @param model   the creator's model graph.
1030     * @param factory   the <code>CopyFactory</code> used to create graph
1031     * elements in the creator's view graph. (The specified factory has to be
1032     * able to create copies of graph elements from the specified model graph in
1033     * the specified view graph.)
1034     * @param view   the creator's view graph. This graph is updated/modified
1035     * whenever the creator's <code>create</code> method is called.
1036     * @see #updateViewGraph()
1037     */
1038    SelectedSubgraph(
1039            final Graph2D model,
1040            final GraphCopier.CopyFactory factory,
1041            final Graph2D view
1042    ) {
1043      super(AbstractLocalViewCreator.ELEMENT_TYPE_NODE);
1044      this.model = model;
1045      this.factory = factory;
1046      this.view = view;
1047
1048      // ModelViewManager is a convenient way to get model-to-view mappings
1049      // for free when creating copies/views of graphs
1050      final ModelViewManager mvm = ModelViewManager.getInstance(model);
1051      if (!mvm.isViewGraph(view)) {
1052        mvm.addViewGraph(view, null, false, false);
1053        mvm.setCopyFactory(view, factory);
1054      }
1055    }
1056
1057    /**
1058     * Returns the creator's model graph.
1059     * @return the creator's model graph.
1060     */
1061    public Graph2D getModel() {
1062      return model;
1063    }
1064
1065    /**
1066     * Returns the creator's view graph.
1067     * @return the creator's view graph.
1068     */
1069    public Graph2D getViewGraph() {
1070      return view;
1071    }
1072
1073    /**
1074     * Returns a node in the creator's model graph that corresponds to the
1075     * specified node in the creator's view graph.
1076     * @param view   a node in the creator's view graph.
1077     * @return a node in the creator's model graph that corresponds to the
1078     * specified node in the creator's view graph.
1079     */
1080    public Node getModelNode( final Node view ) {
1081      return getManager().getModelNode(view);
1082    }
1083
1084    /**
1085     * Returns a node in the creator's view graph that corresponds to the
1086     * specified node in the creator's model graph or <code>null</code> if
1087     * there is no corresponding node.
1088     * @param model   a node in the creator's model graph.
1089     * @return a node in the creator's view graph that corresponds to the
1090     * specified node in the creator's model graph or <code>null</code> if
1091     * there is no corresponding node.
1092     */
1093    public Node getViewNode( final Node model ) {
1094      return getManager().getViewNode(model, getViewGraph());
1095    }
1096
1097    /**
1098     * Returns a edge in the creator's model graph that corresponds to the
1099     * specified edge in the creator's view graph.
1100     * @param view   a edge in the creator's view graph.
1101     * @return a edge in the creator's model graph that corresponds to the
1102     * specified edge in the creator's view graph.
1103     */
1104    public Edge getModelEdge( final Edge view ) {
1105      return getManager().getModelEdge(view);
1106    }
1107
1108    /**
1109     * Returns a edge in the creator's view graph that corresponds to the
1110     * specified edge in the creator's model graph or <code>null</code> if
1111     * there is no corresponding edge.
1112     * @param model   a edge in the creator's model graph.
1113     * @return a edge in the creator's view graph that corresponds to the
1114     * specified edge in the creator's model graph or <code>null</code> if
1115     * there is no corresponding edge.
1116     */
1117    public Edge getViewEdge( final Edge model ) {
1118      return getManager().getViewEdge(model, getViewGraph());
1119    }
1120
1121    /**
1122     * Returns <code>null</code> to indicate that the creator's view graph
1123     * should not be laid out on updates.
1124     * @return <code>null</code> to indicate that the creator's view graph
1125     * should not be laid out on updates.
1126     */
1127    protected Layouter createDefaultLayouter() {
1128      return null;
1129    }
1130
1131    /**
1132     * Updates the creator's view graph.
1133     */
1134    protected void buildViewGraph() {
1135      final Graph2D model = getModel();
1136      if (!model.isSelectionEmpty()) {
1137        final HashSet nodes = new HashSet();
1138        final HashSet edges = new HashSet();
1139
1140        // take only the currently selected nodes into account
1141        for (Iterator it = focusNodes(); it.hasNext();) {
1142          final Node node = (Node) it.next();
1143          if (model.isSelected(node)) {
1144            nodes.add(node);
1145          }
1146        }
1147
1148        // collect the edges that make up the induced subgraph
1149        for (EdgeCursor ec = model.edges(); ec.ok(); ec.next()) {
1150          final Edge edge = ec.edge();
1151          if (nodes.contains(edge.source()) && nodes.contains(edge.target())) {
1152            edges.add(edge);
1153          }
1154        }
1155
1156        // now create corresponding graph elements in the creator's view graph
1157        final Graph2D view = getViewGraph();
1158        final ModelViewManager mvm = getManager();
1159
1160        // in theory, each local view creator could use a different factory
1161        // but share the same view graph. ModelViewManager, however, can only
1162        // store one factory per view graph. therefore the "correct" factory
1163        // has to be temporarily set for the creator's view graph.
1164        final GraphCopier.CopyFactory oldFactory = mvm.getCopyFactory(view);
1165        if (oldFactory != factory) {
1166          mvm.setCopyFactory(view, factory);
1167        }
1168        try {
1169          mvm.synchronizeModelToViewGraph(
1170                  (new NodeList(nodes.iterator())).nodes(),
1171                  (new EdgeList(edges.iterator())).edges(),
1172                  view);
1173        } finally {
1174          // reset the original factory stored for the view graph
1175          if (oldFactory != factory) {
1176            mvm.setCopyFactory(view, oldFactory);
1177          }
1178        }
1179      }
1180    }
1181
1182    private ModelViewManager getManager() {
1183      return ModelViewManager.getInstance(getModel());
1184    }
1185  }
1186}
1187