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