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