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.orgchart;
15  
16  import java.awt.Dimension;
17  import java.awt.event.ActionEvent;
18  import java.awt.event.InputEvent;
19  import java.awt.event.KeyEvent;
20  import java.awt.event.MouseWheelListener;
21  import java.awt.geom.Point2D;
22  import java.beans.PropertyChangeListener;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  
29  import javax.swing.AbstractAction;
30  import javax.swing.Action;
31  import javax.swing.ActionMap;
32  import javax.swing.ComponentInputMap;
33  import javax.swing.InputMap;
34  import javax.swing.JComponent;
35  import javax.swing.KeyStroke;
36  import javax.swing.tree.TreeModel;
37  
38  import y.anim.AnimationFactory;
39  import y.anim.AnimationObject;
40  import y.anim.AnimationPlayer;
41  import y.anim.CompositeAnimationObject;
42  import y.base.DataMap;
43  import y.base.DataProvider;
44  import y.base.Edge;
45  import y.base.EdgeCursor;
46  import y.base.EdgeList;
47  import y.base.EdgeMap;
48  import y.base.Node;
49  import y.base.NodeCursor;
50  import y.base.NodeList;
51  import y.base.NodeMap;
52  import y.geom.YInsets;
53  import y.geom.YPoint;
54  import y.geom.YRectangle;
55  import y.layout.GraphLayout;
56  import y.layout.NormalizingGraphElementOrderStage;
57  import y.layout.Layouter;
58  import y.layout.tree.GenericTreeLayouter;
59  import y.util.Maps;
60  import y.view.EdgeRealizer;
61  import y.view.Graph2D;
62  import y.view.Graph2DView;
63  import y.view.Graph2DViewActions;
64  import y.view.Graph2DViewMouseWheelZoomListener;
65  import y.view.HitInfo;
66  import y.view.NavigationMode;
67  import y.view.NodeRealizer;
68  import y.view.Overview;
69  import y.view.Selections;
70  import y.view.ViewAnimationFactory;
71  import y.view.ViewMode;
72  import y.view.Graph2DLayoutExecutor;
73  import y.view.hierarchy.GroupNodeRealizer;
74  import y.view.hierarchy.HierarchyManager;
75  
76  /**
77   * Component that visualizes tree data structures.
78   */
79  public class JTreeChart extends Graph2DView {
80    
81    private boolean viewLocalHierarchy = false;
82    private boolean siblingViewEnabled = false;
83    private boolean groupViewEnabled = false;   
84    private DataProvider groupIdDP;  
85    private DataProvider userObjectDP;  
86    private TreeModel model;
87    private Map graph2TreeMap;
88    private Map tree2GraphMap;
89    private NodeList allNodes = new NodeList();
90    private EdgeList allEdges = new EdgeList();
91    private HashMap idToGroupNodeMap;
92    private HashMap groupNodeToIdMap;
93    private Object lastUserObject;
94  
95    /**
96     * Creates a new <code>JTreeChart</code>.
97     * @param model   the data model which determines the tree structure to
98     * visualize.
99     * @param userObjectDP   a mapping from model data to business data.
100    * @param groupIdDP   a mapping from business data to grouping ids. Business
101    * data items that share a grouping id are considered a business unit.
102    * Business units may be visualized by a group node containing all nodes
103    * representing the appropriate business data items.
104    */
105   public JTreeChart(TreeModel model, DataProvider userObjectDP, DataProvider groupIdDP) {
106     super();
107     
108     this.groupIdDP = groupIdDP;
109     this.userObjectDP = userObjectDP;    
110     this.model = model;
111     
112     new HierarchyManager(getGraph2D());
113     
114     setRealizerDefaults();
115     updateChart();
116     addMouseInteraction();    
117     addKeyboardInteraction();
118   }
119 
120   /**
121    * Registers handlers for mouse events.
122    */
123   protected void addMouseInteraction() {
124     ViewMode vm = createTreeChartViewMode();
125     if(vm != null) {
126       addViewMode(vm);
127     }
128     
129     MouseWheelListener mwl = createMouseWheelListener();
130     if(mwl != null) {
131       getCanvasComponent().addMouseWheelListener(mwl);
132     } 
133   }
134 
135   /**
136    * Registers handlers for keyboard events.
137    */
138   protected void addKeyboardInteraction() {
139     Graph2DViewActions actions = new Graph2DViewActions(this);
140     
141     ActionMap actionMap = actions.createActionMap();
142     actionMap.put(Graph2DViewActions.FOCUS_BOTTOM_NODE, new SelectRootWrapperAction(actionMap.get(Graph2DViewActions.FOCUS_BOTTOM_NODE),this));
143     actionMap.put(Graph2DViewActions.FOCUS_TOP_NODE, new SelectRootWrapperAction(actionMap.get(Graph2DViewActions.FOCUS_TOP_NODE),this));
144     actionMap.put(Graph2DViewActions.FOCUS_LEFT_NODE, new SelectRootWrapperAction(actionMap.get(Graph2DViewActions.FOCUS_LEFT_NODE),this));
145     actionMap.put(Graph2DViewActions.FOCUS_RIGHT_NODE, new SelectRootWrapperAction(actionMap.get(Graph2DViewActions.FOCUS_RIGHT_NODE),this));
146     actionMap.put("NODE_ACTION", new NodeAction());
147 
148     final JComponent canvas = getCanvasComponent();
149     InputMap inputMap =  new ComponentInputMap(canvas); 
150     inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,InputEvent.CTRL_MASK), Graph2DViewActions.FOCUS_LEFT_NODE);
151     inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,InputEvent.CTRL_MASK), Graph2DViewActions.FOCUS_RIGHT_NODE);
152     inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,InputEvent.CTRL_MASK), Graph2DViewActions.FOCUS_TOP_NODE);
153     inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,InputEvent.CTRL_MASK), Graph2DViewActions.FOCUS_BOTTOM_NODE);
154     
155     inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "NODE_ACTION");
156     
157     canvas.setActionMap(actionMap);
158     canvas.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, inputMap);
159 
160     KeyboardNavigation kNav = new KeyboardNavigation(this);
161     canvas.addKeyListener(kNav.createZoomInKeyListener(KeyEvent.VK_ADD, KeyEvent.VK_PLUS));
162     canvas.addKeyListener(kNav.createZoomOutKeyListener(KeyEvent.VK_SUBTRACT, KeyEvent.VK_MINUS));    
163     canvas.addKeyListener(kNav.createMoveViewportUpKeyListener(KeyEvent.VK_UP));
164     canvas.addKeyListener(kNav.createMoveViewportDownKeyListener(KeyEvent.VK_DOWN));
165     canvas.addKeyListener(kNav.createMoveViewportLeftKeyListener(KeyEvent.VK_LEFT));
166     canvas.addKeyListener(kNav.createMoveViewportRightKeyListener(KeyEvent.VK_RIGHT));
167   }
168 
169   /**
170    * Creates a handler for mouse wheel events.
171    * @return a handler for mouse wheel events.
172    */
173   protected MouseWheelListener createMouseWheelListener() {
174     return new Graph2DViewMouseWheelZoomListener();
175   }
176 
177   /**
178    * Creates a <code>JTreeChartViewMode</code> suitable for use with this
179    * component.
180    * @return a <code>JTreeChartViewMode</code> suitable for use with this
181    * component.
182    */
183   protected JTreeChartViewMode createTreeChartViewMode() {
184     return new JTreeChartViewMode();
185   }
186 
187   public Action createNodeAction() {
188     return new NodeAction();
189   }
190 
191   public Action createZoomInAction() {
192     return new AnimatedZoomAction(true);
193   }
194 
195   public Action createZoomOutAction() {
196     return new AnimatedZoomAction(false);
197   }
198 
199   public Action createFitContentAction() {
200     return new FitContentAction();
201   }
202 
203   public Overview createOverview() {
204     return new Overview(this);
205   }
206 
207   /**
208    * Callback method to set up the default {@link y.view.NodeRealizer}s and
209    * {@link y.view.EdgeRealizer}s.
210    * Note, this method is called from <code>JTreeChart</code>'s constructor.
211    */
212   protected void setRealizerDefaults() {
213   }
214 
215   /**
216    * Callback method that is used to configure {@link y.view.NodeRealizer}s
217    * for nodes representing business data.
218    * @param n   a node representing business data.
219    */
220   protected void configureNodeRealizer(Node n) {
221   }
222 
223   /**
224    * Callback method that is used to configure {@link y.view.NodeRealizer}s
225    * for nodes representing business units.
226    * @param node   a node representing a business unit.
227    * @param groupId   the id of the business unit.
228    * @param collapsed   the current state of the business units.
229    * If <code>true</code> the business unit is represented as a folder, i.e.
230    * the nodes representing the business data associated to the unit are
231    * not being displayed; if <code>false</code> the business unit is represented
232    * as a group node containing the nodes representing the business data
233    * associated to the unit.
234    */
235   protected void configureGroupRealizer(Node node, Object groupId, boolean collapsed) {
236     NodeRealizer nr = getGraph2D().getRealizer(node);
237     if(nr instanceof GroupNodeRealizer) {
238       GroupNodeRealizer gnr = (GroupNodeRealizer) nr;
239       gnr.setGroupClosed(collapsed);
240       gnr.setBorderInsets(new YInsets(0,0,0,0));      
241     }
242   }
243 
244   /**
245    * Callback method that is used to configure {@link y.view.EdgeRealizer}s for
246    * all edges.
247    * @param e   an edge for which the realizer has to be configured.
248    */
249   protected void configureEdgeRealizer(Edge e) {
250   }
251 
252   /**
253    * Calls the appropriate <code>configureXXXRealizer</code> method for each
254    * element in the chart.
255    */
256   private void configureRealizers() {
257     Graph2D graph = getGraph2D();
258     HierarchyManager hm = graph.getHierarchyManager();
259     for(NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
260       Node n = nc.node();
261       if(groupNodeToIdMap == null || groupNodeToIdMap.get(n) == null) {
262         configureNodeRealizer(n);
263       }
264       else {
265         configureGroupRealizer(n, groupNodeToIdMap.get(n), hm.isFolderNode(n));
266       }
267     }
268     for(EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
269       configureEdgeRealizer(ec.edge());
270     }
271   }
272 
273   /**
274    * Calculates and applies a new layout to the chart.
275    */
276   private void layoutGraph() {
277     new Graph2DLayoutExecutor(Graph2DLayoutExecutor.BUFFERED).doLayout(getGraph2D(), createLayouter());
278     fitContent();
279     updateView();
280   }
281 
282   /**
283    * Returns the business data represented by the specified node.
284    * @param node   the node for which the business data should be retrieved.
285    * @return the business data represented by the specified node.
286    */
287   public Object getUserObject(Node node) {
288     Object treeNode = graph2TreeMap.get(node);
289     if(treeNode == null) {
290       return null;
291     } else {
292       return getUserObject(treeNode);
293     }
294   }
295 
296   /**
297    * Returns the <code>Node</code> representing the model data root.
298    * @return the <code>Node</code> representing the model data root.
299    */
300   public Node getRootNode() {
301     Object treeNode = model.getRoot();
302     Object userObject = getUserObject(treeNode);
303     return getNodeForUserObject(userObject);
304   }
305 
306   /**
307    * Returns the business data corresponding to the specified model data.
308    * @param treeNode   the model data for which the business data should be
309    * retrieved.
310    * @return the business data corresponding to the specified model data.
311    */
312   private Object getUserObject(Object treeNode) {
313     return userObjectDP == null ? null : userObjectDP.get(treeNode);
314   }
315 
316   /**
317    * Returns the grouping id (or business unit id) for the specified business
318    * data.
319    * Business data items that share a grouping id are considered a business
320    * unit. Business units may be visualized by a group node containing all nodes
321    * representing the appropriate business data items.
322    * @param userObject   the business data for which the grouping id should be
323    * retrieved.
324    * @return the grouping id (or business unit id) for the specified business
325    * data.
326    */
327   public Object getGroupId(Object userObject) {
328     return groupIdDP == null ? null : groupIdDP.get(userObject);
329   }
330 
331   /**
332    * Returns the node representing the specified business data or
333    * <code>null</code> if there is no such node.
334    * @param userObject   the business data for which the representative node
335    * should be retrieved.
336    * @return  the node representing the specified business data or
337    * <code>null</code> if there is no such node.
338    */
339   public Node getNodeForUserObject(Object userObject) {
340     for (NodeCursor nc = getGraph2D().nodes(); nc.ok(); nc.next()) {
341       Node n = nc.node();
342       if(getUserObject(n) == userObject) {
343         return n;
344       }        
345     }
346     return null;
347   }
348 
349   /**
350    * Returns the model data represented by the specified node.
351    * @param node   the node for which the model data should be retrieved.
352    * @return the model data represented by the specified node.
353    */
354   public Object getTreeNode(Node node) {
355     return graph2TreeMap.get(node);
356   }
357 
358   /**
359    * Updates the component to visualize all of the model/business data.
360    */
361   public void showGlobalHierarchy() {
362     viewLocalHierarchy = false;
363 
364     final NodeCursor nc = getGraph2D().selectedNodes();
365     final Object selected = nc.ok() ? getUserObject(nc.node()) : null;
366 
367     buildGlobalGraph();
368     configureRealizers();
369 
370     if (selected != null) {
371       getGraph2D().setSelected(getNodeForUserObject(selected), true);
372     }
373 
374     layoutGraph();
375     getGraph2D().updateViews();
376   }
377   
378   /**
379    * Updates the component to visualize the neighborhood of the specified
380    * business data. In this context, the neighborhood of a business data item
381    * is defined as follows: Let <code>m</code> be the model data corresponding
382    * to business data <code>b</code>. Then business data <code>bn</code>
383    * is said to be a <em>neighbor</em> of <code>b</code>, iff the model data
384    * <code>mn</code> corresponding to <code>bn</code> is either the parent or
385    * one of the children of <code>m</code> in the tree model of this component.
386    * The <em>neighborhood</em> of <code>b</code> consists of all neighbors of
387    * <code>b</code>.
388    * <p>
389    * If the specified business data is <code>null</code>, the neighborhood
390    * of the business data corresponding to the model root is displayed.
391    * </p>
392    * @param userObject   the business data.
393    */
394   public void showLocalHierarchy(Object userObject) {
395     viewLocalHierarchy = true;
396     Graph2D graph = getGraph2D();
397         
398     if (userObject == null) {
399       Node root = null;
400       final NodeCursor selected = graph.selectedNodes();
401       if (selected.ok()) {
402         root = selected.node();
403       } else {
404         for(NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
405           if(nc.node().inDegree() == 0) {
406             root = nc.node();
407             break;
408           }
409         }
410       }   
411       userObject = getUserObject(root);
412     }
413     
414     lastUserObject = userObject;
415     
416     boolean incrChange = getNodeForUserObject(userObject) != null;
417     
418     final NodeList addedNodes = new NodeList();
419     NodeList removedNodes = new NodeList();
420     EdgeList removedEdges = new EdgeList();
421     final EdgeList addedEdges = new EdgeList();
422     
423     buildLocalView(userObject, removedNodes, addedNodes, removedEdges, addedEdges);       
424     
425     if (!incrChange) {
426       configureRealizers();
427       // TODO - refactor
428       new Graph2DLayoutExecutor(Graph2DLayoutExecutor.BUFFERED).doLayout(graph, createLayouter());
429       fitContent();
430     } else {
431       for(NodeCursor nc = removedNodes.nodes(); nc.ok(); nc.next()) {
432         graph.reInsertNode(nc.node());
433       }
434       for(EdgeCursor ec = removedEdges.edges(); ec.ok(); ec.next()) {
435         graph.reInsertEdge(ec.edge());
436       }      
437       
438       configureRealizers();
439 
440       for(EdgeCursor ec = addedEdges.edges(); ec.ok(); ec.next()) {
441         graph.removeEdge(ec.edge());
442       }
443       for(NodeCursor nc = addedNodes.nodes(); nc.ok(); nc.next()) {
444         graph.removeNode(nc.node());      
445       }    
446       
447       ViewAnimationFactory factory = new ViewAnimationFactory(this);
448       AnimationPlayer player = factory.createConfiguredPlayer();
449       player.setBlocking(true);
450       
451       AnimationObject deleteAnim = createDeleteAnimation(graph, removedNodes, removedEdges, factory, 200);
452       
453       player.animate(deleteAnim);
454       
455       for(NodeCursor nc = addedNodes.nodes(); nc.ok(); nc.next()) {
456         graph.reInsertNode(nc.node());           
457         graph.getRealizer(nc.node()).setVisible(false);
458       }
459 
460       for(EdgeCursor ec = addedEdges.edges(); ec.ok(); ec.next()) {
461         graph.reInsertEdge(ec.edge());
462         graph.getRealizer(ec.edge()).setVisible(false);
463       }
464              
465       if(isGroupViewEnabled()) {
466         for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
467           Node n = nc.node();
468           Object obj = getUserObject(n);
469           if(obj != null && getGroupId(obj) != null) {
470             HierarchyManager hm = getGraph2D().getHierarchyManager();
471             Node groupNode = (Node) idToGroupNodeMap.get(getGroupId(obj));
472             if(hm.isNormalNode(groupNode)) {
473               hm.convertToGroupNode(groupNode);
474             }
475             hm.setParentNode(n, groupNode);
476           }
477         }
478       }
479 
480       new Graph2DLayoutExecutor(){
481         protected AnimationObject createAnimation(Graph2DView view, Graph2D graph, GraphLayout graphLayout) {
482           for(NodeCursor nc = addedNodes.nodes(); nc.ok(); nc.next()) {
483             graph.getRealizer(nc.node()).setVisible(false);
484           }
485           for(EdgeCursor ec = addedEdges.edges(); ec.ok(); ec.next()) {
486             graph.getRealizer(ec.edge()).setVisible(false);
487           }
488           return super.createAnimation(view, graph, graphLayout);
489         }
490       }.doLayout(this, createLayouter());
491 
492       for(NodeCursor nc = addedNodes.nodes(); nc.ok(); nc.next()) {
493         graph.getRealizer(nc.node()).setVisible(true);
494       }
495       for(EdgeCursor ec = addedEdges.edges(); ec.ok(); ec.next()) {
496         graph.getRealizer(ec.edge()).setVisible(true);
497       }
498       
499       AnimationObject fadeInAnim = createFadeInAnimation(graph, addedNodes, addedEdges, factory, 500);           
500       player.animate(fadeInAnim);
501     }
502     
503     
504     graph.updateViews();
505   }
506 
507   protected Layouter createLayouter() {
508     GenericTreeLayouter layouter = new GenericTreeLayouter();
509 
510     // hiding/removing and unhiding/reinserting graph elements which is done
511     // e.g. when switching from displaying the whole chart to displaying a
512     // local excerpt may change the order of elements in the chart's graph
513     // however, the order of elements in a graph usually affects the results
514     // produced by a layout algorithm which in turn means that the above
515     // mentioned hide/unhide operations could lead to different layouts for
516     // a given set of displayed data
517     // NormalizingGraphElementOrderStage prevents that from happening by
518     // enforcing an externally specified, fixed graph element order
519     // see the usage of
520     // NormalizingGraphElementOrderStage.COMPARABLE_EDGE_DPKEY
521     // and
522     // NormalizingGraphElementOrderStage.COMPARABLE_NODE_DPKEY
523     // in buildGlobalGraph
524     return new NormalizingGraphElementOrderStage(layouter);
525   }
526 
527   /**
528    * Updates the displayed chart to either show the neighborhood of the
529    * currently selected item or to show the whole business data at once.
530    * @see #showGlobalHierarchy
531    * @see #showLocalHierarchy(Object)
532    */
533   public void updateChart() {
534     if(isLocalViewEnabled()) {
535       buildGlobalGraph();
536       showLocalHierarchy(lastUserObject);
537     } else {
538       showGlobalHierarchy();
539     }
540   }
541 
542   /**
543    * Determines whether or not siblings are included when displaying
544    * the neighborhood of a business data item.
545    * @return <code>true</code> if siblings are included when displaying
546    * the neighborhood of a business data item; <code>false</code> otherwise.
547    */
548   public boolean isSiblingViewEnabled() {
549     return siblingViewEnabled;
550   }
551 
552   /**
553    * Specifies whether or not siblings should be included when displaying
554    * the neighborhood of a business data item.
555    * In this context, siblings are defined as follows: Let <code>m</code> be
556    * the model data corresponding to business data <code>b</code>. Let
557    * <code>mp</code> be the parent of <code>m</code> in the tree model of this
558    * component. Then business data <code>bs</code> is said to be a
559    * <em>sibling</em> of <code>b</code>, iff the model data <code>ms</code>
560    * corresponding to <code>bs</code> is a child of <code>mp</code> in the tree
561    * model of this component.
562    * @param siblingViewEnabled   if <code>true</code>, siblings will be
563    * displayed.
564    */
565   public void setSiblingViewEnabled(boolean siblingViewEnabled) {
566     this.siblingViewEnabled = siblingViewEnabled;
567   }
568 
569   /**
570    * Determines whether all of the business data or only a local excerpt
571    * is displayed.
572    * @return <code>false</code> if all of the business data is displayed;
573    * <code>true</code> otherwise.
574    */
575   public boolean isLocalViewEnabled() {
576     return viewLocalHierarchy;
577   }
578 
579   /**
580    * Returns whether or not business units are displayed using group nodes.
581    * @return whether or not business units are displayed using group nodes.
582    */
583   public boolean isGroupViewEnabled() {
584     return groupViewEnabled && groupIdDP != null;
585   }
586 
587   /**
588    * Specifies whether or not business units should be displayed using
589    * group nodes.
590    * @param enabled   if <code>true</code> business units will be displayed.
591    */
592   public void setGroupViewEnabled(boolean enabled) {
593     groupViewEnabled = enabled;
594   }
595 
596   /**
597    * Focuses on the specified node by moving the node into the center of
598    * this component.
599    * @param node   the node to focus on.
600    */
601   public void focusNode(Node node) {
602     YPoint p = getGraph2D().getCenter(node);
603     focusView(getZoom(), new Point2D.Double(p.x, p.y), false);
604     updateView();
605   }
606 
607   /**
608    * Focuses on the specified node.
609    * If this component currently displays the whole chart, the specified node
610    * will become its center and the component's zoom level will be adjusted to
611    * prominently display the specified node.
612    * If this component currently displays a local excerpt of the chart, the
613    * displayed excerpt will be changed to the specified node's neighborhood,
614    * see also {@link #showLocalHierarchy(Object)}.
615    * @param node   the node to focus on.
616    */
617   public void performNodeAction(Node node) {
618     if(getGraph2D().getHierarchyManager().isNormalNode(node)) {
619       if(viewLocalHierarchy) {
620         showLocalHierarchy(getUserObject(node));
621       }
622       else {
623         Point2D center = new Point2D.Double(getGraph2D().getCenterX(node), getGraph2D().getCenterY(node));
624         YRectangle nodeSize = getGraph2D().getRectangle(node);
625         Dimension viewSize = getViewSize();
626         double zoom;
627         if(viewSize.width/nodeSize.width < viewSize.height/nodeSize.height) {
628           zoom = viewSize.width/nodeSize.width;
629         } else {
630           zoom = viewSize.height/nodeSize.height;
631         }
632         zoom *= 0.5;
633         focusView(zoom, center, true);
634       }
635     }
636   }
637 
638   /**
639    * Removes the business data of a business unit from the chart.
640    * @param groupNode   a node representing a business unit that displays
641    * its business data.
642    */
643   private void collapseGroup(Node groupNode) {
644     Graph2D graph = getGraph2D();
645     HierarchyManager hm = graph.getHierarchyManager();
646     hm.closeGroup(groupNode);
647     configureGroupRealizer(groupNode, groupNodeToIdMap.get(groupNode), true);
648     layoutGraph();
649   }
650 
651   /**
652    * Reinserts the business data of a business unit from the char.
653    * @param folderNode   a node representing a business unit that does not
654    * display its business data.
655    */
656   private void expandGroup(Node folderNode) {
657     Graph2D graph = getGraph2D();
658     HierarchyManager hm = graph.getHierarchyManager();
659     hm.openFolder(folderNode);
660     configureGroupRealizer(folderNode, groupNodeToIdMap.get(folderNode), false);
661     layoutGraph();
662   }
663 
664   /**
665    * Creates the chart from scratch including all business data. Business
666    * units are included as appropriate for the return value of
667    * {@link #isGroupViewEnabled()}.
668    */
669   private void buildGlobalGraph() {
670     Graph2D graph = getGraph2D();
671     graph.clear();
672     tree2GraphMap = new HashMap();
673     graph2TreeMap = new HashMap();
674     Object treeNode = model.getRoot();
675     Node graphNode = graph.createNode();
676     tree2GraphMap.put(treeNode, graphNode);
677     graph2TreeMap.put(graphNode, treeNode);
678     buildGraph(treeNode, graphNode, tree2GraphMap, graph2TreeMap);
679 
680     if(isGroupViewEnabled()) {
681       addGroupNodes();
682     }
683 
684     allNodes = new NodeList(graph.nodes());
685     allEdges = new EdgeList(graph.edges());
686 
687     DataMap comparableMap = Maps.createHashedDataMap();
688     NormalizingGraphElementOrderStage.fillComparableMapFromGraph(graph,  comparableMap, comparableMap);
689     graph.addDataProvider(NormalizingGraphElementOrderStage.COMPARABLE_EDGE_DPKEY, comparableMap);
690     graph.addDataProvider(NormalizingGraphElementOrderStage.COMPARABLE_NODE_DPKEY, comparableMap);
691   }
692 
693   /**
694    * Recursively builds the chart from the tree model.
695    * @param treeNode   the model root of the subtree to build.
696    * @param graphNode   the node representing the model root.
697    * @param tree2GraphMap   an output parameter to store a mapping from model
698    * items to graph nodes.
699    * @param graph2TreeMap   an output parameter to store a mapping from graph
700    * nodes to model items.
701    */
702   private void buildGraph(Object treeNode, Node graphNode, Map tree2GraphMap, Map graph2TreeMap) {
703     Graph2D graph = getGraph2D();
704     int count = model.getChildCount(treeNode);
705     for(int i = 0; i < count; i++) {
706       Object treeChild = model.getChild(treeNode, i);
707       Node graphChild = graph.createNode();
708       tree2GraphMap.put(treeChild, graphChild);
709       graph2TreeMap.put(graphChild, treeChild);
710       //configureNode(graphChild);
711       graph.createEdge(graphNode, graphChild);
712       buildGraph(treeChild, graphChild, tree2GraphMap, graph2TreeMap);
713     }
714   }
715 
716   /**
717    * Adds group nodes representing business units to the chart.
718    */
719   private void addGroupNodes() {
720     Graph2D graph = getGraph2D();
721     idToGroupNodeMap = new HashMap();
722     groupNodeToIdMap = new HashMap();
723     HierarchyManager hm = graph.getHierarchyManager();
724     for(NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
725       Node n = nc.node();
726       Object obj = getUserObject(n);
727       if(obj != null && getGroupId(obj) != null) {
728         Node groupNode = (Node) idToGroupNodeMap.get(getGroupId(obj));
729         if(groupNode == null) {
730           groupNode = hm.createGroupNode(graph);
731           idToGroupNodeMap.put(getGroupId(obj), groupNode);
732           groupNodeToIdMap.put(groupNode, getGroupId(obj));
733         }
734         hm.setParentNode(n, groupNode);
735       }
736     }
737   }
738 
739   /**
740    * Builds a local excerpt for the neighborhood of the specified business data.
741    * @param userObject   the business data.
742    * @param removedNodes   output parameter containing the nodes that should not
743    * be part of the chart anymore.
744    * @param addedNodes   output parameter containing the nodes that need to
745    * be added to the chart.
746    * @param removedEdges   output parameter containing the edges that should not
747    * be part of the chart anymore.
748    * @param addedEdges   output parameter containing the edges that need to
749    * be added to the chart.
750    */
751   private void buildLocalView(Object userObject, NodeList removedNodes, NodeList addedNodes, EdgeList removedEdges, EdgeList addedEdges) {
752     expandAll();
753     Graph2D graph = getGraph2D();
754     NodeMap prevNodeMap = Maps.createHashedNodeMap();
755     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
756       Node n = nc.node();
757       prevNodeMap.setBool(n, true);
758     }
759     EdgeMap prevEdgeMap = Maps.createHashedEdgeMap();
760     for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
761       Edge e = ec.edge();
762       prevEdgeMap.setBool(e, true);
763     }
764 
765     rebuildGlobalGraph();
766     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
767       Node n = nc.node();
768       if(getUserObject(n).equals(userObject)) {
769         NodeList nodes = new NodeList(n);
770         if(n.inDegree() == 1) {
771           Node parent = n.firstInEdge().source();
772           nodes.add(parent);
773           if(isSiblingViewEnabled()) {
774             nodes.pop();
775             nodes.addAll(parent.successors());
776           }
777         }
778         nodes.addAll(n.successors());
779 
780         NodeList nodesToRemove = new NodeList(graph.nodes());
781 
782         if(isGroupViewEnabled()) {
783           HashSet requiredGroups = new HashSet();
784           //iterate over local view elements marking required groups
785           for(NodeCursor ncc = nodes.nodes(); ncc.ok(); ncc.next()) {
786             Node node = ncc.node();
787             requiredGroups.add(graph.getHierarchyManager().getParentNode(node));
788           }
789           nodesToRemove.removeAll(requiredGroups);
790         }
791         nodesToRemove.removeAll(nodes);
792 
793         while(!nodesToRemove.isEmpty()) {
794           graph.removeNode(nodesToRemove.popNode());
795         }
796         break;
797       }
798     }
799 
800     for(NodeCursor nc = allNodes.nodes(); nc.ok(); nc.next()) {
801       Node n = nc.node();
802       if(n.getGraph() != null) {
803         //node currently present
804         if(prevNodeMap.getBool(n)) {
805           //was present before - no delta
806         } else {
807           //was not present before - added node
808           addedNodes.add(n);
809         }
810       } else {
811         //node currently not present
812         if(prevNodeMap.getBool(n)) {
813           //was present before - removed node
814           removedNodes.add(n);
815         } else {
816           //was not present before - no delta
817         }
818       }
819     }
820     for(EdgeCursor ec = allEdges.edges(); ec.ok(); ec.next()) {
821       Edge e = ec.edge();
822       if(e.getGraph() != null) {
823         //edge currently present
824         if(prevEdgeMap.getBool(e)) {
825           //was present before - no delta
826         } else {
827           //was not present before - added edge
828           addedEdges.add(e);
829         }
830       } else {
831         //edge currently not present
832         if(prevEdgeMap.getBool(e)) {
833           //was present before - removed edge
834           removedEdges.add(e);
835         } else {
836           //was not present before - no delta
837         }
838       }
839     }
840 
841     Node employeeNode = getNodeForUserObject(userObject);
842     if(employeeNode != null) {
843       graph.setSelected(employeeNode, true);
844     }
845   }
846 
847   /**
848    * Expands all folder nodes to group nodes. That is for all business units
849    * that currently do not display their business data reinsert said data.
850    */
851   private void expandAll() {
852     HierarchyManager hm = getGraph2D().getHierarchyManager();
853     for (NodeCursor nc = allNodes.nodes(); nc.ok(); nc.next()) {
854       Node n = nc.node();
855       if(hm.isFolderNode(n)) {
856         hm.openFolder(n);
857       }
858     }
859   }
860 
861   private void rebuildGlobalGraph() {
862     Graph2D graph = getGraph2D();
863     graph.clear();
864     for (NodeCursor nc = allNodes.nodes(); nc.ok(); nc.next()) {
865       Node n = nc.node();
866       graph.reInsertNode(n);
867     }
868     for (EdgeCursor ec = allEdges.edges(); ec.ok(); ec.next()) {
869       Edge e = ec.edge();
870       graph.reInsertEdge(e);
871     }
872 
873     //reestablish grouping structure
874     if(isGroupViewEnabled()) {
875       for (NodeCursor nc = allNodes.nodes(); nc.ok(); nc.next()) {
876         Node n = nc.node();
877         Object obj = getUserObject(n);
878         if(obj != null && getGroupId(obj) != null) {
879           HierarchyManager hm = getGraph2D().getHierarchyManager();
880           Node groupNode = (Node) idToGroupNodeMap.get(getGroupId(obj));
881           if(hm.isNormalNode(groupNode)) {
882             hm.convertToGroupNode(groupNode);
883           }
884           hm.setParentNode(n, groupNode);
885         }
886       }
887     }
888   }
889 
890   /**
891    * Creates an animation for retracting edges and fading out nodes.
892    * As a side effect, this animation will result in said edges and nodes being
893    * removed from the graph.
894    * @param nodesToBeDeleted   the nodes to fade out
895    * @param edgesToBeDeleted   the edges to retract
896    * @return an animation for retracting edges and fading out nodes.
897    */
898   private AnimationObject createDeleteAnimation(
899           Graph2D graph,
900           final List nodesToBeDeleted,
901           final List edgesToBeDeleted,
902           ViewAnimationFactory factory,
903           long preferredDuration
904   ) {
905     final CompositeAnimationObject deleteEdges = AnimationFactory.createConcurrency();
906     for (Iterator it = edgesToBeDeleted.iterator(); it.hasNext();) {
907       final EdgeRealizer er = graph.getRealizer((Edge) it.next());
908       deleteEdges.addAnimation(factory.fadeOut(er, ViewAnimationFactory.APPLY_EFFECT, preferredDuration));
909     }
910 
911     final CompositeAnimationObject deleteNodes = AnimationFactory.createConcurrency();
912     for (Iterator it = nodesToBeDeleted.iterator(); it.hasNext();) {
913       final NodeRealizer nr = graph.getRealizer((Node) it.next());
914       deleteNodes.addAnimation(factory.fadeOut(nr, ViewAnimationFactory.APPLY_EFFECT, preferredDuration));
915     }
916     return AnimationFactory.createSequence(deleteEdges, deleteNodes);
917   }
918 
919   /**
920    * Creates an animation for fading in edges and nodes.
921    * removed from the graph.
922    * @param nodesToBeAdded   the nodes to fade in.
923    * @param edgesToBeAdded   the edges to fade in.
924    * @return an animation for fading in edges and nodes.
925    */
926   private AnimationObject createFadeInAnimation(
927           Graph2D graph,
928           final List nodesToBeAdded,
929           final List edgesToBeAdded,
930           ViewAnimationFactory factory,
931           long preferredDuration
932   ) {
933     final CompositeAnimationObject addElems = AnimationFactory.createConcurrency();
934     for (Iterator it = edgesToBeAdded.iterator(); it.hasNext();) {
935       final EdgeRealizer er = graph.getRealizer((Edge) it.next());
936       addElems.addAnimation(factory.fadeIn(
937               er, preferredDuration));
938     }
939 
940     for (Iterator it = nodesToBeAdded.iterator(); it.hasNext();) {
941       final NodeRealizer nr = graph.getRealizer((Node) it.next());
942       addElems.addAnimation(factory.fadeIn(nr, preferredDuration));
943     }
944     return addElems;
945   }
946 
947 
948   /**
949    * <code>NavigationMode</code> that provides custom single and double mouse
950    * click handling.
951    * Single clicking a node representing business data (and not a business unit)
952    * will select said node; single clicking anything else will unselect any
953    * selected node.
954    * Double clicking a node representing business data will invoke
955    * {@link JTreeChart#performNodeAction(y.base.Node)} for that node, double
956    * clicking a node representing a business unit will toggle the node's
957    * collapsed/expanded state, and finally double clicking anything but a node
958    * will trigger an animated fit content operation
959    * (see {@link demo.view.orgchart.JTreeChart#fitContent(boolean)}).
960    */
961   public static class JTreeChartViewMode extends NavigationMode {
962     public JTreeChart getJTreeChart() {
963       return (JTreeChart) view;
964     }
965 
966     public void mouseClicked(double x, double y) {
967       if(lastClickEvent.getClickCount() > 1) {
968         mouseDoubleClicked(x, y);
969       }
970       else {
971         mouseSingleClicked(x,y);
972       }
973     }
974 
975     /**
976      * Handles single mouse clicks for the specified world coordinates.
977      * Single clicking a normal node (as opposed to a group or folder node)
978      * will select said node (exclusively). Single clicking anything else will
979      * unselect all previously selected items.
980      * @param x   the x-coordinate in the associated view's world coordinate
981      * system.
982      * @param y   the y-coordinate in the associated view's world coordinate
983      * system.
984      * @see y.view.hierarchy.HierarchyManager#isFolderNode(y.base.Node)
985      * @see y.view.hierarchy.HierarchyManager#isGroupNode(y.base.Node)
986      * @see y.view.hierarchy.HierarchyManager#isNormalNode(y.base.Node)
987      */
988     protected void mouseSingleClicked(double x, double y) {
989       view.getCanvasComponent().requestFocus();
990       HitInfo info = getHitInfo(x,y);
991       Node node = info.getHitNode();
992       Graph2D graph = getGraph2D();
993       if (node != null && getGraph2D().getHierarchyManager().isNormalNode(node)) {
994         if (!graph.isSelected(node)) {
995           graph.unselectAll();
996           graph.setSelected(node, true);
997         }
998       } else {
999         getGraph2D().unselectAll();
1000      }
1001      getGraph2D().updateViews();
1002    }
1003
1004    /**
1005     * Handles double mouse clicks for the specified world coordinates.
1006     * Double clicking a normal node will invoke
1007     * {@link demo.view.orgchart.JTreeChart#performNodeAction(y.base.Node)} for
1008     * that node; double clicking a group node will collapse or close the group
1009     * (i.e. hide its content); double clicking a folder node will expand or
1010     * open the folder (i.e. unhide its content).
1011     * @param x   the x-coordinate in the associated view's world coordinate
1012     * system.
1013     * @param y   the y-coordinate in the associated view's world coordinate
1014     * system.
1015     * @see y.view.hierarchy.HierarchyManager#isFolderNode(y.base.Node)
1016     * @see y.view.hierarchy.HierarchyManager#isGroupNode(y.base.Node)
1017     * @see y.view.hierarchy.HierarchyManager#isNormalNode(y.base.Node)
1018     */
1019    protected void mouseDoubleClicked(double x, double y) {
1020      if (lastClickEvent.getClickCount() == 2) {
1021        HitInfo info = getHitInfo(x,y);
1022        Node node = info.getHitNode();
1023        if (node != null) {
1024          if (getGraph2D().getHierarchyManager().isGroupNode(node)) {
1025            getJTreeChart().collapseGroup(node);
1026          } else if (getGraph2D().getHierarchyManager().isFolderNode(node)) {
1027            getJTreeChart().expandGroup(node);
1028          } else {
1029            getJTreeChart().performNodeAction(node);
1030          }
1031        } else {
1032          getJTreeChart().fitContent(true);
1033        }
1034      }
1035    }
1036  }
1037
1038  /**
1039   * <code>Action</code> for decorating {@link Graph2DViewActions}' focus node
1040   * actions such that triggering this action while no node is selected will
1041   * select either a node with indegree <code>0</code> or the first node in the
1042   * graph if there is no node with indegree <code>0</code>. In other words,
1043   * this <code>Action</code> will try to select the node representing
1044   * the model root of a {@link demo.view.orgchart.JTreeChart} component.
1045   */
1046  private static class SelectRootWrapperAction implements Action {
1047    Action delegateAction;
1048    Graph2DView view;
1049
1050    SelectRootWrapperAction(Action delegateAction, Graph2DView view) {
1051      this.delegateAction = delegateAction;
1052      this.view = view;
1053    }
1054
1055    public void addPropertyChangeListener(PropertyChangeListener listener) {
1056      delegateAction.addPropertyChangeListener(listener);
1057    }
1058
1059    public Object getValue(String key) {
1060      return delegateAction.getValue(key);
1061    }
1062
1063    public boolean isEnabled() {
1064      return delegateAction.isEnabled();
1065    }
1066
1067    public void putValue(String key, Object value) {
1068      delegateAction.putValue(key, value);
1069    }
1070
1071    public void removePropertyChangeListener(PropertyChangeListener listener) {
1072      delegateAction.removePropertyChangeListener(listener);
1073    }
1074
1075    public void setEnabled(boolean b) {
1076      delegateAction.setEnabled(b);
1077    }
1078
1079    /**
1080     * Selects a node in the associated view's graph. The node which is selected
1081     * is determined as follows: If there is currently no selected node then
1082     * either select a node with indegree <code>0</code> or (if there is no node
1083     * with indegree <code>0</code>) select the first node in the graph. If
1084     * there is a currently selected node, then call the decorated action's
1085     * <code>actionPerformed</code> method and let it handle node selection.
1086     */
1087    public void actionPerformed(ActionEvent e) {
1088      Graph2D graph = view.getGraph2D();
1089      boolean selectionEmpty = Selections.isNodeSelectionEmpty(graph);
1090      if(selectionEmpty) {
1091        //select root node
1092        for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
1093          Node n = nc.node();
1094          if(n.inDegree() == 0) {
1095            graph.setSelected(n, true);
1096            selectionEmpty = false;
1097            break;
1098          }
1099        }
1100        if(graph.nodeCount() > 0 && selectionEmpty) {
1101          graph.setSelected(graph.firstNode(), true);
1102        }
1103      }
1104      else {
1105        delegateAction.actionPerformed(e);
1106      }
1107    }
1108  }
1109
1110  /**
1111   * <code>Action</code> that changes this component's zoom level in an
1112   * animated fashion.
1113   */
1114  private class AnimatedZoomAction extends AbstractAction {
1115    private final boolean zoomIn;
1116
1117    private ViewAnimationFactory factory;
1118    private AnimationPlayer player;
1119
1120    AnimatedZoomAction( final boolean zoomIn ) {
1121      this.zoomIn = zoomIn;
1122    }
1123
1124    /**
1125     * Changes the zoom level in an animated fashion.
1126     * @param e   the event that triggered the zom level change.
1127     */
1128    public void actionPerformed(ActionEvent e) {
1129      if (factory == null) {
1130        factory = new ViewAnimationFactory(JTreeChart.this);
1131        player = factory.createConfiguredPlayer();
1132      }
1133
1134      if (!player.isPlaying()) {
1135        player.animate(AnimationFactory.createEasedAnimation(
1136                factory.zoom(calculateZoom(), ViewAnimationFactory.APPLY_EFFECT, 500)));
1137      }
1138    }
1139
1140    /**
1141     * Calculates a new zoom level for the component.
1142     * @return  a new zoom level for the component.
1143     */
1144    double calculateZoom() {
1145      if (zoomIn) {
1146        return Math.min(4, getZoom()*2);
1147      } else {
1148        Point2D oldP = getViewPoint2D();
1149        double oldZoom = getZoom();
1150        fitContent();
1151        double fitContentZoom = getZoom();
1152        setZoom(oldZoom);
1153        setViewPoint2D(oldP.getX(), oldP.getY());
1154
1155        return Math.max(fitContentZoom, getZoom()*0.5);
1156      }
1157    }
1158  }
1159
1160  /**
1161   * <code>Action</code> that updates this COMPONENT to focus on the
1162   * currently selected chart item.
1163   */
1164  private class NodeAction extends AbstractAction {
1165    public void actionPerformed(ActionEvent e) {
1166      if(!Selections.isNodeSelectionEmpty(getGraph2D())) {
1167        performNodeAction(getGraph2D().selectedNodes().node());
1168      }
1169    }
1170  }
1171
1172  /**
1173   * <code>Action</code> that updates this component to adjust its zoom level
1174   * and view point such that all of the current chart is visible at once.
1175   */
1176  private class FitContentAction extends AbstractAction {
1177    public void actionPerformed(ActionEvent e) {
1178      fitContent(true);
1179    }
1180  }
1181}
1182