1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.6. 
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-2008 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.view.layout.tree;
15  
16  import demo.view.DemoBase;
17  import y.algo.GraphConnectivity;
18  import y.algo.Trees;
19  import y.anim.AnimationFactory;
20  import y.anim.AnimationObject;
21  import y.anim.AnimationPlayer;
22  import y.base.DataMap;
23  import y.base.Edge;
24  import y.base.EdgeCursor;
25  import y.base.EdgeList;
26  import y.base.Node;
27  import y.base.NodeCursor;
28  import y.base.NodeList;
29  import y.base.NodeMap;
30  import y.geom.YPoint;
31  import y.layout.BufferedLayouter;
32  import y.layout.GraphLayout;
33  import y.layout.LayoutOrientation;
34  import y.layout.NodeLayout;
35  import y.layout.hierarchic.IncrementalHierarchicLayouter;
36  import y.layout.hierarchic.incremental.IncrementalHintsFactory;
37  import y.layout.hierarchic.incremental.SimplexNodePlacer;
38  import y.layout.organic.OrganicLayouter;
39  import y.layout.tree.BalloonLayouter;
40  import y.layout.tree.TreeLayouter;
41  import y.layout.tree.XCoordComparator;
42  import y.util.DefaultMutableValue2D;
43  import y.util.Maps;
44  import y.view.EditMode;
45  import y.view.Graph2D;
46  import y.view.LayoutMorpher;
47  import y.view.NodeLabel;
48  import y.view.NodeRealizer;
49  import y.view.ViewAnimationFactory;
50  import y.view.ViewMode;
51  import y.view.hierarchy.GroupNodeRealizer;
52  
53  import javax.swing.AbstractAction;
54  import javax.swing.ButtonGroup;
55  import javax.swing.JToggleButton;
56  import javax.swing.JToolBar;
57  import javax.swing.SwingUtilities;
58  import java.awt.Insets;
59  import java.awt.event.ActionEvent;
60  import java.awt.event.MouseEvent;
61  import java.awt.geom.Point2D;
62  import java.util.WeakHashMap;
63  
64  /**
65   * This demo shows how to collapse and expand subtrees by simply clicking on a root node. Three different layout
66   * algorithms can be chosen: {@link y.layout.tree.TreeLayouter}, {@link y.layout.tree.BalloonLayouter}, {@link
67   * y.layout.organic.OrganicLayouter} and {@link y.layout.hierarchic.IncrementalHierarchicLayouter}.
68   */
69  public class CollapsibleTreeDemo extends DemoBase {
70    public static final byte STYLE_TREE = 1;
71    public static final byte STYLE_BALLOON = 2;
72    private static final byte STYLE_ORGANIC = 3;
73    private static final byte STYLE_HIERARCHIC = 4;
74  
75    private byte style = STYLE_TREE;
76    private TreeLayouter treeLayouter;
77    private BalloonLayouter balloonLayouter;
78    private OrganicLayouter organicLayouter;
79    private IncrementalHierarchicLayouter hierarchicLayouter;
80    private CollapsibleTreeDemo.CollapseExpandViewMode viewMode;
81    private DataMap ihlHintMap;
82    private IncrementalHintsFactory hintsFactory;
83  
84    public CollapsibleTreeDemo() {
85      Graph2D graph = view.getGraph2D();
86  
87      //create realizer that displays an additional expand/collapse icon.
88      //alternatively implement your own realizer.
89      NodeRealizer nr = graph.getDefaultNodeRealizer();
90      NodeLabel nl = nr.createNodeLabel();
91      nr.addLabel(nl);
92      nl.setIcon(GroupNodeRealizer.defaultOpenGroupIcon);
93      nl.setIconTextGap((byte) 0);
94      nl.setPosition(NodeLabel.TOP_RIGHT);
95      nl.setInsets(new Insets(2, 2, 2, 2));
96      nl.setDistance(0);
97  
98      //create a sample tree structure
99      createTree(graph);
100 
101     //collapse/expand some nodes
102     viewMode.collapseSubtree(graph, Trees.getRoot(graph));
103     Node root = Trees.getRoot(graph);
104     viewMode.expandSubtree(graph, root, 2);
105 
106     //configure layouters
107     treeLayouter = new TreeLayouter();
108     treeLayouter.setComparator(new XCoordComparator()); //important to keep node order of collpsed/expanded items.
109     treeLayouter.setLayoutOrientation(LayoutOrientation.LEFT_TO_RIGHT);
110     treeLayouter.setLayoutStyle(TreeLayouter.ORTHOGONAL_STYLE);
111 
112     balloonLayouter = new BalloonLayouter();
113     balloonLayouter.setFromSketchModeEnabled(true);
114     balloonLayouter.setCompactnessFactor(0.1);
115     balloonLayouter.setAllowOverlaps(true);
116 
117     organicLayouter = new OrganicLayouter();
118     organicLayouter.setInitialPlacement(OrganicLayouter.AS_IS);
119 
120     hierarchicLayouter = new IncrementalHierarchicLayouter();
121     hierarchicLayouter.getEdgeLayoutDescriptor().setOrthogonallyRouted(true);
122     hierarchicLayouter.setLayoutOrientation(LayoutOrientation.TOP_TO_BOTTOM);
123     // read the "old" nodes from the sketch
124     hierarchicLayouter.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_INCREMENTAL);
125     ((SimplexNodePlacer) hierarchicLayouter.getNodePlacer()).setBaryCenterModeEnabled(true);
126 
127     // create a map to store the hints for the incremental layout mechanism
128     ihlHintMap = Maps.createHashedDataMap();
129     graph.addDataProvider(IncrementalHierarchicLayouter.INCREMENTAL_HINTS_DPKEY, ihlHintMap);
130     // get a reference to a hints factory
131     hintsFactory = hierarchicLayouter.createIncrementalHintsFactory();
132 
133     //layout the graph
134     SwingUtilities.invokeLater(new Runnable() {
135       public void run() {
136         layout(view.getGraph2D(), null, true);
137       }
138     });
139   }
140 
141   /** EditMode not supported by this demo. */
142   protected EditMode createEditMode() {
143     return null;
144   }
145 
146   /** Register CollapseExpandViewMode. */
147   protected void registerViewModes() {
148     viewMode = new CollapseExpandViewMode();
149     view.addViewMode(viewMode);
150   }
151 
152   /**
153    * A ViewMode that allows to expand and collapse the subtrees rooted at a node by simply clicking on the node.
154    * Clicking on a node, while ctrl is pushed, will expand/collapse all nodes in the subtree. Note that this view mode
155    * is also responsible to keeping track of the expansion state of each node.
156    */
157   class CollapseExpandViewMode extends ViewMode {
158     NodeMap collapsedEdges = Maps.createNodeMap(new WeakHashMap());
159     NodeMap collapsedState = Maps.createNodeMap(new WeakHashMap());
160 
161     public void mouseClicked(MouseEvent ev) {
162       //if (ev.getClickCount() != 2) return;
163       Node node = getHitInfo(ev).getHitNode();
164 
165       if (node != null) {
166         prepareForLayout(view.getGraph2D(), node);
167         if (collapsedState.getBool(node)) {
168           if (ev.isControlDown()) {//ctrl is pressed expand whole subtree (max depth of 10000) of current node
169             expandSubtree(getGraph2D(), node, 10000);
170           } else {//ctrl is not pressed expand only current node
171             expandNode(getGraph2D(), node);
172           }
173         } else {
174           if (ev.isControlDown()) {//ctrl is pressed collapse whole subtree of current node
175             collapseSubtree(getGraph2D(), node);
176           } else {//ctrl is not pressed collapse only current node
177             collapseNode(getGraph2D(), node);
178           }
179         }
180         layout(getGraph2D(), node, false);
181       }
182     }
183 
184     /**
185      * Collapses the given node and it's whole subtree.
186      *
187      * @param graph the graph, the root node belongs to.
188      * @param root  the node whose subtree is to be collapsed.
189      */
190     public void collapseSubtree(Graph2D graph, Node root) {
191       NodeList list = GraphConnectivity.getSuccessors(graph, new NodeList(root), graph.N());
192       NodeCursor nodeCursor = list.nodes();
193       for (nodeCursor.toLast(); nodeCursor.ok(); nodeCursor.prev()) {
194         Node node = nodeCursor.node();
195         if (!collapsedState.getBool(node) && node != root) {
196           collapseNode(graph, node);
197         }
198       }
199       collapseNode(graph, root);
200     }
201 
202     /**
203      * collapses the given node.
204      *
205      * @param graph the graph, the root node belongs to.
206      * @param root  the node which is to be collapsed.
207      */
208     public void collapseNode(Graph2D graph, final Node root) {
209       EdgeList edgeList = collapsedEdges.get(root) != null ? (EdgeList) collapsedEdges.get(root) : new EdgeList();
210       edgeList.addAll(root.outEdges());
211       NodeList collapsedNodes = GraphConnectivity.getSuccessors(graph, new NodeList(root), graph.N());
212 
213       for (NodeCursor nc = collapsedNodes.nodes(); nc.ok(); nc.next()) {
214         Node n = nc.node();
215         edgeList.addAll(n.outEdges());
216         double x = graph.getCenterX(n) - graph.getCenterX(root);
217         double y = graph.getCenterY(n) - graph.getCenterY(root);
218 
219         // store relative location to root
220         graph.getRealizer(n).setLocation(0.01 * x, 0.01 * y);
221 
222         //remove node from graph
223         graph.hide(n);
224       }
225       collapsedState.setBool(root, true);
226       collapsedEdges.set(root, edgeList);
227 
228       if (!edgeList.isEmpty()) {
229         getGraph2D().getRealizer(root).getLabel(1).setIcon(GroupNodeRealizer.defaultClosedGroupIcon);
230       }
231     }
232 
233     /**
234      * Expands a node and it's subtree to a given depth.
235      *
236      * @param graph the graph, the root node belongs to.
237      * @param root  the node whose subtree is to be expanded.
238      * @param depth determines the depth (how many layers) till which the subtree should be expanded.
239      */
240     public void expandSubtree(Graph2D graph, Node root, int depth) {
241       if (depth <= 0) {
242         return;
243       }
244       //expand the root
245       expandNode(graph, root);
246       NodeList list = GraphConnectivity.getSuccessors(graph, new NodeList(root), depth);
247       for (NodeCursor nodeCursor = list.nodes(); nodeCursor.ok(); nodeCursor.next()) {
248         Node node = nodeCursor.node();
249         if (collapsedState.getBool(node)) {
250           //expand the subtree
251           expandSubtree(graph, node, depth - 1);
252         }
253       }
254     }
255 
256     /**
257      * Expands a single node.
258      *
259      * @param graph the graph, the root node belongs to.
260      * @param root  the node which is to be expanded.
261      */
262     public void expandNode(Graph2D graph, Node root) {
263       final EdgeList edgeList = (EdgeList) collapsedEdges.get(root);
264       if (edgeList != null) {
265         for (EdgeCursor ec = edgeList.edges(); ec.ok(); ec.next()) {
266           Edge e = ec.edge();
267           if (!graph.contains(e.source())) {
268             graph.unhide(e.source());
269             graph.setLocation(e.source(), graph.getX(root) + graph.getX(e.source()),
270                 graph.getY(root) + graph.getY(e.source()));
271           }
272           if (!graph.contains(e.target())) {
273             graph.unhide(e.target());
274             graph.setLocation(e.target(), graph.getX(root) + graph.getX(e.target()),
275                 graph.getY(root) + graph.getY(e.target()));
276           }
277           //inserts the node into the graph
278           graph.unhide(e);
279           //cosmetics
280           graph.getRealizer(e).clearBends();
281         }
282         collapsedEdges.set(root, null);
283       }
284       collapsedState.setBool(root, false);
285 
286       if (root.outDegree() > 0) {
287         getGraph2D().getRealizer(root).getLabel(1).setIcon(GroupNodeRealizer.defaultOpenGroupIcon);
288       }
289     }
290   }
291 
292   /**
293    * Layout the tree according to the set layout style.
294    *
295    * @param graph2D    the graph, which will be laid out.
296    * @param focusNode  the current focus.
297    * @param fitContent determines whether to fit the content to the current view. Should be prevented, if layout. is
298    *                   started due to a mouse click on a node.
299    */
300   void layout(Graph2D graph2D, Node focusNode, boolean fitContent) {
301     GraphLayout gl;
302     //calculate layout according to chosen style
303     switch (style) {
304       case CollapsibleTreeDemo.STYLE_TREE:
305         gl = new BufferedLayouter(treeLayouter).calcLayout(graph2D);
306         break;
307       case CollapsibleTreeDemo.STYLE_BALLOON:
308         gl = new BufferedLayouter(balloonLayouter).calcLayout(graph2D);
309         break;
310       case CollapsibleTreeDemo.STYLE_ORGANIC:
311         gl = new BufferedLayouter(organicLayouter).calcLayout(graph2D);
312         break;
313       case CollapsibleTreeDemo.STYLE_HIERARCHIC:
314         prepareForLayout(graph2D, focusNode);
315         gl = new BufferedLayouter(hierarchicLayouter).calcLayout(graph2D);
316         break;
317       default:
318         gl = new BufferedLayouter(treeLayouter).calcLayout(graph2D);
319     }
320 
321     LayoutMorpher lm = new LayoutMorpher(view, gl);
322     if (!fitContent) {
323       lm.setKeepZoomFactor(true); //optional
324     }
325 
326     //keep view at focus node position
327     if (focusNode != null) {
328       Point2D cp = view.getCenter();
329       YPoint oldFocus = graph2D.getCenter(focusNode);
330       NodeLayout nl = gl.getNodeLayout(focusNode);
331       YPoint newFocus = new YPoint(nl.getX() + 0.5 * nl.getWidth(), nl.getY() + 0.5 * nl.getHeight());
332       double dx = newFocus.x - oldFocus.x;
333       double dy = newFocus.y - oldFocus.y;
334 
335       ViewAnimationFactory af = new ViewAnimationFactory(view);
336       AnimationObject mc = af.moveCamera(DefaultMutableValue2D.create(cp.getX() + dx, cp.getY() + dy), 300);
337       AnimationObject animObject = AnimationFactory.createConcurrency(lm, mc);
338       AnimationPlayer player = af.createConfiguredPlayer();
339       player.setBlocking(true);
340       player.animate(animObject);
341     } else {
342       lm.execute();
343     }
344   }
345 
346   private void prepareForLayout(Graph2D graph2D, Node node) {
347     NodeList incrementalNodes = GraphConnectivity.getSuccessors(graph2D, new NodeList(node), graph2D.N());
348     // mark nodes as "new"
349     for (NodeCursor nodeCursor = incrementalNodes.nodes(); nodeCursor.ok(); nodeCursor.next()) {
350       ihlHintMap.set(nodeCursor.node(), hintsFactory.createLayerIncrementallyHint(nodeCursor.node()));
351     }
352   }
353 
354 
355   /** Adds some buttons to the toolbar, to choose the layout style from. */
356   protected JToolBar createToolBar() {
357     JToolBar toolbar = super.createToolBar();
358     ButtonGroup group = new ButtonGroup();
359     JToggleButton b1 = new JToggleButton(new AbstractAction("Tree") {
360       public void actionPerformed(ActionEvent e) {
361         style = CollapsibleTreeDemo.STYLE_TREE;
362         layout(view.getGraph2D(), null, true);
363       }
364     });
365     b1.setSelected(true);
366     group.add(b1);
367     toolbar.add(b1);
368 
369 
370     JToggleButton b2 = new JToggleButton(new AbstractAction("Balloon") {
371       public void actionPerformed(ActionEvent e) {
372         style = CollapsibleTreeDemo.STYLE_BALLOON;
373         layout(view.getGraph2D(), null, true);
374       }
375     });
376     group.add(b2);
377     toolbar.add(b2);
378 
379     JToggleButton b3 = new JToggleButton(new AbstractAction("Organic") {
380       public void actionPerformed(ActionEvent e) {
381         style = CollapsibleTreeDemo.STYLE_ORGANIC;
382         layout(view.getGraph2D(), null, true);
383       }
384     });
385     group.add(b3);
386     toolbar.add(b3);
387 
388     JToggleButton b4 = new JToggleButton(new AbstractAction("Hierarchic") {
389       public void actionPerformed(ActionEvent e) {
390         style = CollapsibleTreeDemo.STYLE_HIERARCHIC;
391         Graph2D graph = view.getGraph2D();
392         layout(graph, Trees.getRoot(graph), true);
393       }
394     });
395     group.add(b4);
396     toolbar.add(b4);
397 
398     return toolbar;
399   }
400 
401   void createTree(Graph2D graph) {
402     NodeList queue = new NodeList();
403     queue.add(graph.createNode(0, 0, 80, 30, "Root"));
404     for (int i = 0; i < 50; i++) {
405       Node root = queue.popNode();
406       Node c1 = graph.createNode(0, 0, 80, 30, String.valueOf(graph.N()));
407       Edge e1 = graph.createEdge(root, c1);
408       Node c2 = graph.createNode(0, 0, 80, 30, String.valueOf(graph.N()));
409       Edge e2 = graph.createEdge(root, c2);
410       queue.add(c2);
411       queue.add(c1);
412       if (i == 25 || i == 40) {
413         for (int j = 0; j < 20; j++) {
414           Node c3 = graph.createNode(0, 0, 80, 30, String.valueOf(graph.N()));
415           Edge e3 = graph.createEdge(root, c3);
416           queue.add(c3);
417         }
418       }
419     }
420     for (NodeCursor nodeCursor = graph.nodes(); nodeCursor.ok(); nodeCursor.next()) {
421       Node node = nodeCursor.node();
422       if (node.outDegree() == 0) {
423         graph.getRealizer(node).getLabel(1).setIcon(null);
424       }
425     }
426   }
427 
428 
429   public static void main(String[] args) {
430     initLnF();
431     CollapsibleTreeDemo demo = new CollapsibleTreeDemo();
432     demo.start(demo.getClass().getName());
433   }
434 }