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.layout.hierarchic;
29  
30  import demo.view.hierarchy.GroupingDemo;
31  import y.base.DataMap;
32  import y.base.DataProvider;
33  import y.base.Edge;
34  import y.base.EdgeCursor;
35  import y.base.EdgeMap;
36  import y.base.Node;
37  import y.base.NodeCursor;
38  import y.base.NodeMap;
39  import y.layout.PortConstraint;
40  import y.layout.PortConstraintKeys;
41  import y.layout.hierarchic.IncrementalHierarchicLayouter;
42  import y.layout.hierarchic.incremental.EdgeLayoutDescriptor;
43  import y.layout.hierarchic.incremental.IncrementalHintsFactory;
44  import y.module.PortConstraintModule;
45  import y.util.Maps;
46  import y.view.Graph2D;
47  import y.view.Graph2DLayoutExecutor;
48  import y.view.Graph2DViewActions;
49  import y.view.hierarchy.HierarchyManager;
50  
51  import javax.swing.AbstractAction;
52  import javax.swing.Action;
53  import javax.swing.JLabel;
54  import javax.swing.JToolBar;
55  import java.awt.EventQueue;
56  import java.awt.event.ActionEvent;
57  import java.util.Locale;
58  
59  
60  /**
61   * This demo showcases how {@link y.layout.hierarchic.IncrementalHierarchicLayouter} can be used to update the layout
62   * of hierarchically nested graphs after closing a group or opening a folder.
63   * After expanding folder nodes or collapsing group nodes, an incremental layout is applied automatically. Furthermore,
64   * there are toolbar buttons that trigger a full layout or an incremental relayout.
65   * In incremental layout mode all selected elements are added incrementally to the existing layout.
66   *
67   * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/incremental_hierarchical_layouter#ihl_hierarchically_organized_graphs" target="_blank">Section Layout of Grouped Graphs</a> in the yFiles for Java Developer's Guide
68   */
69  public class IncrementalHierarchicFoldExpandDemo extends GroupingDemo {
70  
71    private final IncrementalHierarchicLayouter layouter;
72    private PortConstraintModule pcModule;
73  
74    public IncrementalHierarchicFoldExpandDemo() {
75  
76      //configure layout algorithm
77      layouter = new IncrementalHierarchicLayouter();
78      layouter.setOrthogonallyRouted(true);
79      layouter.setRecursiveGroupLayeringEnabled(true);
80      layouter.getEdgeLayoutDescriptor().setRecursiveEdgeStyle(EdgeLayoutDescriptor.RECURSIVE_EDGE_STYLE_DIRECTED);
81      layouter.setMinimumLayerDistance(40);
82      
83      //create a module that allows to define PortConstraints for the edges
84      pcModule = new PortConstraintModule();
85  
86      view.fitContent();
87    }
88  
89    /**
90     * Register custom open folder and close group actions that adjust the layout of the graph.
91     */
92    protected void registerViewActions() {
93      super.registerViewActions();
94      view.getCanvasComponent().getActionMap().put(Graph2DViewActions.CLOSE_GROUPS, new CloseGroupsAndLayoutAction());
95      view.getCanvasComponent().getActionMap().put(Graph2DViewActions.OPEN_FOLDERS, new OpenFoldersAndLayoutAction());
96    }
97  
98    /**
99     * Expand a folder node. After expanding the folder node, an incremental layout is automatically triggered.
100    * For this, the expanded node and all of its descendants will be treated as incremental elements.
101    */
102   class OpenFoldersAndLayoutAction extends Graph2DViewActions.OpenFoldersAction {
103 
104     OpenFoldersAndLayoutAction() {
105       super(IncrementalHierarchicFoldExpandDemo.this.view);
106     }
107 
108     public void openFolder(Node folderNode, Graph2D graph) {
109       //Store alternative bounds for folder that is being expanded
110       final NodeMap alternativeBounds = Maps.createHashedNodeMap();
111       alternativeBounds.set(folderNode, graph.getRectangle(folderNode));
112       graph.addDataProvider(IncrementalHierarchicLayouter.ALTERNATIVE_GROUP_BOUNDS_DPKEY, alternativeBounds);
113       
114       //Store alternative edge path for all edges connecting to the folder node
115       final EdgeMap alternativePathMap = Maps.createHashedEdgeMap();
116       for (EdgeCursor edgeCursor = folderNode.edges(); edgeCursor.ok(); edgeCursor.next()) {
117         final Edge edge = edgeCursor.edge();
118         alternativePathMap.set(edge, graph.getPath(edge));
119       }
120       graph.addDataProvider(IncrementalHierarchicLayouter.ALTERNATIVE_EDGE_PATH_DPKEY, alternativePathMap);
121 
122       super.openFolder(folderNode, graph);
123 
124       layoutIncrementally(null);
125       
126       graph.removeDataProvider(IncrementalHierarchicLayouter.ALTERNATIVE_GROUP_BOUNDS_DPKEY);
127       graph.removeDataProvider(IncrementalHierarchicLayouter.ALTERNATIVE_EDGE_PATH_DPKEY);
128 
129       graph.setSelected(folderNode, true);
130       graph.updateViews();
131     }
132   }
133 
134   /**
135    * Collapse a group node. After collapsing the group node, an incremental layout is automatically triggered.
136    * For this, the collapsed node is treated as an incremental element.
137    */
138   class CloseGroupsAndLayoutAction extends Graph2DViewActions.CloseGroupsAction {
139 
140     CloseGroupsAndLayoutAction() {
141       super(IncrementalHierarchicFoldExpandDemo.this.view);
142     }
143 
144     public void closeGroup(Node groupNode, Graph2D graph) {
145       //Store alternative bounds for group that is being closed
146       final NodeMap alternativeBounds = Maps.createHashedNodeMap();
147       alternativeBounds.set(groupNode, graph.getRectangle(groupNode));
148       graph.addDataProvider(IncrementalHierarchicLayouter.ALTERNATIVE_GROUP_BOUNDS_DPKEY, alternativeBounds);
149 
150       //Store alternative edge path for edges connecting directly to or to the content of the group that is being closed 
151       final EdgeMap alternativePathMap = Maps.createHashedEdgeMap();
152       final HierarchyManager hm = graph.getHierarchyManager();
153       for (EdgeCursor edgeCursor = graph.edges(); edgeCursor.ok(); edgeCursor.next()) {
154         final Edge edge = edgeCursor.edge();
155         final Node source = edge.source();
156         final Node target = edge.target();
157         
158         if (hm.isAncestor(groupNode, source) || hm.isAncestor(groupNode, target) 
159             || source == groupNode || target == groupNode){
160           alternativePathMap.set(edge, graph.getPath(edge));
161         }
162       }
163       graph.addDataProvider(IncrementalHierarchicLayouter.ALTERNATIVE_EDGE_PATH_DPKEY, alternativePathMap);
164       
165       super.closeGroup(groupNode, graph);
166 
167       layoutIncrementally(null);
168       
169       graph.removeDataProvider(IncrementalHierarchicLayouter.ALTERNATIVE_GROUP_BOUNDS_DPKEY);
170       graph.removeDataProvider(IncrementalHierarchicLayouter.ALTERNATIVE_EDGE_PATH_DPKEY);
171 
172       graph.updateViews();      
173     }
174   }
175   
176   
177   /**
178    * Loads the initial graph
179    */
180   protected void loadInitialGraph() {
181     loadGraph("resource/IncrementalHierarchicGroupDemo.graphml");
182   }
183 
184   /**
185    * Creates the toolbar for the demo.
186    */
187   protected JToolBar createToolBar() {
188     final JToolBar toolBar = super.createToolBar();    
189     addLayoutActions(toolBar);    
190     toolBar.addSeparator();
191     toolBar.add(new PortConstraintConfigurationAction());
192     return toolBar;
193   }
194 
195   protected void addLayoutActions(JToolBar toolBar) {
196     final Action incrementalLayoutAction = new AbstractAction(
197             "Incremental", SHARED_LAYOUT_ICON) {
198       public void actionPerformed(ActionEvent e) {
199         layoutIncrementally();
200       }
201     };
202 
203     final Action layoutAction = new AbstractAction(
204             "Complete", SHARED_LAYOUT_ICON) {
205       public void actionPerformed(ActionEvent e) {
206         layout();
207       }
208     };
209 
210     toolBar.addSeparator();
211     toolBar.add(new JLabel("Layout: "));
212     toolBar.add(createActionControl(incrementalLayoutAction));
213     toolBar.add(createActionControl(layoutAction));
214   }
215 
216   /**
217    * A simple action that starts a dialog allowing to configure and create {@link y.layout.PortConstraint}s
218    */
219   class PortConstraintConfigurationAction extends AbstractAction {
220     PortConstraintConfigurationAction() {
221       super("Port Constraints");
222     }
223 
224     public void actionPerformed(ActionEvent ev) {
225       pcModule.getOptionHandler().showEditor();
226       pcModule.start(view.getGraph2D());
227     }
228   }
229 
230   /**
231    * Performs incremental layout. All selected elements will be treated incrementally.
232    */
233   void layoutIncrementally() {
234     final Graph2D graph = view.getGraph2D();
235 
236     // create storage for both nodes and edges
237     final DataMap incrementalElements = Maps.createHashedDataMap();
238     // configure the mode
239     final IncrementalHintsFactory ihf = layouter.createIncrementalHintsFactory();
240 
241     for (NodeCursor nc = graph.selectedNodes(); nc.ok(); nc.next()) {
242       incrementalElements.set(nc.node(), ihf.createLayerIncrementallyHint(nc.node()));
243     }
244 
245     for (EdgeCursor ec = graph.selectedEdges(); ec.ok(); ec.next()) {
246       incrementalElements.set(ec.edge(), ihf.createSequenceIncrementallyHint(ec.edge()));
247     }
248 
249     layoutIncrementally(incrementalElements);
250     
251     graph.removeDataProvider(IncrementalHierarchicLayouter.FOLDER_NODES_DPKEY);
252   }
253 
254   /**
255    * Performs incremental layout. The given data map indicates the elements to treat incrementally.
256    */
257   void layoutIncrementally(DataMap incrementalElements) {
258     final Graph2D graph = view.getGraph2D();
259 
260     //mark the folder nodes for the upcoming layout run
261     final NodeMap folderNodes = getFolderNodes(graph);
262     graph.addDataProvider(IncrementalHierarchicLayouter.FOLDER_NODES_DPKEY, folderNodes);
263 
264     layouter.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_INCREMENTAL);
265 
266     if (incrementalElements != null) {
267       graph.addDataProvider(IncrementalHierarchicLayouter.INCREMENTAL_HINTS_DPKEY, incrementalElements);
268     }
269     
270     //prepare the port constraints before running the layout
271     final DataProvider spcProvider = graph.getDataProvider(PortConstraintKeys.SOURCE_PORT_CONSTRAINT_KEY);
272     final DataProvider tpcProvider = graph.getDataProvider(PortConstraintKeys.TARGET_PORT_CONSTRAINT_KEY);
273     removePortConstraintsAtInterEdges(graph, spcProvider, tpcProvider);
274     
275     try {
276       final Graph2DLayoutExecutor layoutExecutor = new Graph2DLayoutExecutor();
277       layoutExecutor.getLayoutMorpher().setSmoothViewTransform(true);
278       layoutExecutor.doLayout(view, layouter);
279     } finally {
280       if (incrementalElements != null) {
281         graph.removeDataProvider(IncrementalHierarchicLayouter.INCREMENTAL_HINTS_DPKEY);
282       }
283       graph.removeDataProvider(IncrementalHierarchicLayouter.FOLDER_NODES_DPKEY);
284       
285       //re-add the original providers for PortConstraints (if they exist)
286       if (spcProvider != null) {
287         graph.addDataProvider(PortConstraintKeys.SOURCE_PORT_CONSTRAINT_KEY, spcProvider);
288       }
289       if (tpcProvider != null) {
290         graph.addDataProvider(PortConstraintKeys.TARGET_PORT_CONSTRAINT_KEY, tpcProvider);
291       }
292     }
293   }
294 
295 
296   /**
297    * Performs global layout. The new layout can strongly differ from the existing layout.
298    */
299   void layout() {
300     final Graph2D graph = view.getGraph2D();
301     //mark the folder nodes for the upcoming layout run
302     final NodeMap folderNodes = getFolderNodes(graph);
303     graph.addDataProvider(IncrementalHierarchicLayouter.FOLDER_NODES_DPKEY, folderNodes);
304 
305     layouter.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_FROM_SCRATCH);
306     final Graph2DLayoutExecutor layoutExecutor = new Graph2DLayoutExecutor();
307     layoutExecutor.getLayoutMorpher().setSmoothViewTransform(true);
308     layoutExecutor.doLayout(view, layouter);
309         
310     graph.removeDataProvider(IncrementalHierarchicLayouter.FOLDER_NODES_DPKEY);
311   }
312 
313   /**
314    * Removes {@link y.layout.PortConstraint}s at inter-edges to make sure that the overall recursive edge shape is
315    * not changed due to folding/expanding a group node.
316    * 
317    * For example: If a recursive edge has a NORTH constraint at its source and the group containing the source is
318    *              currently expanded, then the edge connects at NORTH to the actual source but due to the recursive
319    *              property leaves the group at the bottom. Now, when folding the group containing the actual source
320    *              node, we still would like that the edge leaves the group at the bottom (SOUTH). This increases
321    *              the stability over several layout runs and helps to preserve the mental map of a layout.
322    * 
323    * @param graph the input graph
324    * @param spcProvider {@link y.base.DataProvider} holding the source {@link y.layout.PortConstraint}s
325    * @param tpcProvider {@link y.base.DataProvider} holding the target {@link y.layout.PortConstraint}s
326    */
327   private void removePortConstraintsAtInterEdges(Graph2D graph, DataProvider spcProvider, DataProvider tpcProvider) {
328     final HierarchyManager hm = graph.getHierarchyManager();
329     
330     if (hm == null || (spcProvider == null && tpcProvider == null)) {
331       //no groups/folders or no constraint defined, nothing to do
332       return;
333     }
334 
335 
336     final EdgeMap tmpSpcProvider = Maps.createHashedEdgeMap();
337     final EdgeMap tmpTpcProvider = Maps.createHashedEdgeMap();
338     for (EdgeCursor edgeCursor = graph.edges(); edgeCursor.ok(); edgeCursor.next()) {
339       final Edge edge = edgeCursor.edge();
340       if (spcProvider != null) {
341         final PortConstraint spc = (PortConstraint) spcProvider.get(edge);
342         final Node source = edge.source();
343         if (spc != null && hm.isFolderNode(source) && hm.isInterEdge(edge) && hm.getRealSource(edge) != source) {
344           //set source constraint to null (no constraint) because the edge connects to the content of a
345           // folder node (is an inter edge)
346           tmpSpcProvider.set(edge, null);
347         } else {
348           tmpSpcProvider.set(edge, spc);
349         }
350       }
351       if (tpcProvider != null) {
352         final PortConstraint tpc = (PortConstraint) tpcProvider.get(edge);
353         final Node target = edge.target();
354         if (tpc != null && hm.isFolderNode(target) && hm.isInterEdge(edge) && hm.getRealTarget(edge) != target) {
355           //set target constraint to null (no constraint) because the edge connects to the content of a
356           // folder node (is an inter edge)
357           tmpTpcProvider.set(edge, null);
358         } else {
359           tmpTpcProvider.set(edge, tpc);
360         }
361       }
362     }
363     
364     //register the newly filled maps with the updated PortConstraints
365     if (spcProvider != null) {
366       graph.addDataProvider(PortConstraintKeys.SOURCE_PORT_CONSTRAINT_KEY, tmpSpcProvider);
367     }
368     if (tpcProvider != null) {
369       graph.addDataProvider(PortConstraintKeys.TARGET_PORT_CONSTRAINT_KEY, tmpTpcProvider);
370     }
371   }
372 
373   /**
374    * Marks all folder nodes of the given graph in the returned {@link y.base.NodeMap}.
375    */
376   private NodeMap getFolderNodes(Graph2D graph) {
377     final HierarchyManager hierarchy = graph.getHierarchyManager();
378     final NodeMap folderNodes = Maps.createHashedNodeMap();
379     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
380       final Node node = nc.node();
381       if (hierarchy.isFolderNode(node)) {
382         folderNodes.setBool(node, true);
383       }
384     }
385     return folderNodes;
386   }
387 
388   /**
389    * Launches this demo.
390    * @param args ignored.
391    */
392   public static void main(String[] args) {
393     EventQueue.invokeLater(new Runnable() {
394       public void run() {
395         Locale.setDefault(Locale.ENGLISH);
396         initLnF();
397         (new IncrementalHierarchicFoldExpandDemo()).start();
398       }
399     });
400   }
401 }
402