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.DemoBase;
31  import y.base.DataMap;
32  import y.base.Edge;
33  import y.base.EdgeCursor;
34  import y.base.EdgeList;
35  import y.base.EdgeMap;
36  import y.base.GraphEvent;
37  import y.base.GraphListener;
38  import y.base.Node;
39  import y.base.NodeCursor;
40  import y.base.NodeList;
41  import y.base.NodeMap;
42  import y.geom.YPoint;
43  import y.layout.NodeLayout;
44  import y.layout.PortConstraint;
45  import y.layout.PortConstraintKeys;
46  import y.layout.hierarchic.GivenLayersLayerer;
47  import y.layout.hierarchic.IncrementalHierarchicLayouter;
48  import y.layout.hierarchic.AsIsLayerer;
49  import y.layout.hierarchic.incremental.IncrementalHintsFactory;
50  import y.layout.hierarchic.incremental.IntValueHolderAdapter;
51  import y.layout.hierarchic.incremental.RoutingStyle;
52  import y.util.Maps;
53  import y.view.Arrow;
54  import y.view.Bend;
55  import y.view.BendCursor;
56  import y.view.BendList;
57  import y.view.BridgeCalculator;
58  import y.view.CreateEdgeMode;
59  import y.view.DefaultGraph2DRenderer;
60  import y.view.Drawable;
61  import y.view.EdgeRealizer;
62  import y.view.EditMode;
63  import y.view.Graph2D;
64  import y.view.Graph2DUndoManager;
65  import y.view.HitInfo;
66  import y.view.HotSpotMode;
67  import y.view.LineType;
68  import y.view.NodeRealizer;
69  import y.view.PopupMode;
70  import y.view.PortAssignmentMoveSelectionMode;
71  
72  import javax.swing.AbstractAction;
73  import javax.swing.Action;
74  import javax.swing.JMenu;
75  import javax.swing.JPopupMenu;
76  import javax.swing.JToolBar;
77  import java.awt.Color;
78  import java.awt.Cursor;
79  import java.awt.Graphics2D;
80  import java.awt.Rectangle;
81  import java.awt.Shape;
82  import java.awt.Stroke;
83  import java.awt.EventQueue;
84  import java.awt.event.ActionEvent;
85  import java.awt.geom.Rectangle2D;
86  import java.beans.PropertyChangeListener;
87  import java.net.URL;
88  import java.util.ArrayList;
89  import java.util.HashSet;
90  import java.util.List;
91  import java.util.Locale;
92  import java.util.Set;
93  
94  /**
95   * This demo shows how to use the {@link y.layout.hierarchic.IncrementalHierarchicLayouter} together
96   * with sophisticated customized {@link y.view.ViewMode}s.
97   * The application will automatically perform a new layout whenever the user
98   * makes changes to the graph. The layout does not change the layer assignment of the
99   * nodes. Moreover, the sequence of nodes within each layer is preserved.<br/>
100  * It demonstrates how to use a predetermined layering and how the application
101  * can retrieve the layering computed during the layout. The layering information
102  * is visualized using a {@link y.view.Drawable} in the canvas. <br/>
103  * For a simpler demo that depicts the basics of {@link y.layout.hierarchic.IncrementalHierarchicLayouter}
104  * see {@link SimpleIncrementalHierarchicLayouterDemo}. <br/>
105  * The incremental layout aligns the centers of the nodes within a layer. Thus moving a
106  * node up or down a little bit such that its layer assignment does not change is normally
107  * immediately reverted by the following incremental layout. Similarly, moving a node
108  * a little bit to the left or right such that the sequence within its layer does not change
109  * also gets reverted by the following incremental layout.
110  * <p>
111  *   Things to try:
112  * </p>
113  * <ul>
114  *   <li>
115  *     Create new nodes. The mouse location is used to determine the layer for the new node.
116  *     Nodes created far away from the existing nodes are added to a new bottom layer.
117  *   </li>
118  *   <li>
119  *     Move existing nodes. This is treated like creating new nodes. Additionally the target
120  *     layer for the moved node is visually indicated.
121  *   </li>
122  *   <li>
123  *     Create new edges.
124  *   </li>
125  *   <li>
126  *     Move the first/last bend of an edge. This creates a port constraint (PC) which is
127  *     visually indicated.
128  *   </li>
129  *   <li>
130  *     Select a node and choose "Optimize Node". This triggers a layout which tries
131  *     to improve the layout by ignoring the current layer assignment and sequence
132  *     information for the selected node.
133  *   </li>
134  *   <li>
135  *     The context menu for a set of selected nodes provides actions for fixing the layer
136  *     or sequence coordinates of the selected nodes. Fixing the layer coordinates of at least
137  *     two nodes within a layer means that they can be moved such that their centers are at
138  *     different y coordinates and that the next incremental layout will preserve the difference
139  *     of the y coordinates. Similarly, fixing the sequence coordinates of at least two nodes
140  *     within a layer allows to control their distances in the direction of the x axis. Fixing
141  *     the coordinates means fixing layer coordinates as well as sequence coordinates.
142  *   </li>
143  * </ul>
144  * @see y.layout.hierarchic.IncrementalHierarchicLayouter
145  *
146  * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/incremental_hierarchical_layouter#incremental_hierarchical_layouter" target="_blank">Section Hierarchical Layout Style</a> in the yFiles for Java Developer's Guide
147  * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/mvc_controller#cls_ViewMode" target="_blank">Section ViewMode Workings</a> in the yFiles for Java Developer's Guide
148  */
149 public class IncrementalHierarchicLayouterDemo extends DemoBase {
150   private EdgeMap sourcePortMap;
151   private EdgeMap targetPortMap;
152   private LayerDrawable layerDrawable;
153   private NodeMap layerIdMap;
154   private DataMap hintMap;
155 
156   private PortAssignmentMoveSelectionMode paMode;
157 
158   private IncrementalHierarchicLayouter hierarchicLayouter;
159   private IncrementalHintsFactory hintsFactory;
160   private GivenLayersLayerer gll;
161 
162   private boolean loaded;
163   private LayoutAction layoutAction;
164   private Graph2DUndoManager undoManager;
165 
166   public IncrementalHierarchicLayouterDemo() {
167     final Graph2D graph = view.getGraph2D();
168 
169     // make it look nice
170     EdgeRealizer defaultER = graph.getDefaultEdgeRealizer();
171     defaultER.setArrow(Arrow.STANDARD);
172 
173     // enable bridges for PolyLineEdgeRealizer
174     BridgeCalculator bridgeCalculator = new BridgeCalculator();
175     bridgeCalculator.setCrossingMode(BridgeCalculator.CROSSING_MODE_HORIZONTAL_CROSSES_VERTICAL);
176     ((DefaultGraph2DRenderer) view.getGraph2DRenderer()).setBridgeCalculator(bridgeCalculator);
177 
178     // allocate a couple of maps
179     layerIdMap = graph.createNodeMap();
180     sourcePortMap = graph.createEdgeMap();
181     targetPortMap = graph.createEdgeMap();
182     hintMap = Maps.createHashedDataMap();
183 
184     // register them with the graph
185     graph.addDataProvider(PortConstraintKeys.SOURCE_PORT_CONSTRAINT_KEY, sourcePortMap);
186     graph.addDataProvider(PortConstraintKeys.TARGET_PORT_CONSTRAINT_KEY, targetPortMap);
187     graph.addDataProvider(GivenLayersLayerer.LAYER_ID_KEY, layerIdMap);
188     graph.addDataProvider(IncrementalHierarchicLayouter.INCREMENTAL_HINTS_DPKEY, hintMap);
189     graph.addDataProvider(IncrementalHierarchicLayouter.LAYER_VALUE_HOLDER_DPKEY,
190         new IntValueHolderAdapter(layerIdMap));
191 
192     // create a drawable that displays layers
193     this.layerDrawable = new LayerDrawable(graph, layerIdMap);
194     view.addBackgroundDrawable(layerDrawable);
195 
196     // create and configure the layout algorithm
197     hierarchicLayouter = new IncrementalHierarchicLayouter();
198     hierarchicLayouter.setFixedElementsLayerer(gll = new GivenLayersLayerer());
199     hintsFactory = hierarchicLayouter.createIncrementalHintsFactory();
200     hierarchicLayouter.setComponentLayouterEnabled(false);
201     hierarchicLayouter.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_INCREMENTAL);
202 
203     hierarchicLayouter.getEdgeLayoutDescriptor().setSourcePortOptimizationEnabled(true);
204     hierarchicLayouter.getEdgeLayoutDescriptor().setTargetPortOptimizationEnabled(true);
205     hierarchicLayouter.getEdgeLayoutDescriptor().setRoutingStyle(new RoutingStyle(RoutingStyle.EDGE_STYLE_OCTILINEAR));
206 
207     // deferred since the mode is created in the super class's constructor
208     paMode.setSpc(sourcePortMap);
209     paMode.setTpc(targetPortMap);
210 
211     initGraph(graph);
212   }
213 
214   protected void initialize() {
215     super.initialize();
216 
217     // register graph listener to enable the layout button
218     // after the removal of graph elements
219     view.getGraph2D().addGraphListener(new GraphListener() {
220       public void onGraphEvent(GraphEvent e) {
221         final byte eventType = e.getType();
222         if(eventType == GraphEvent.POST_EDGE_REMOVAL
223             || eventType == GraphEvent.POST_NODE_REMOVAL) {
224           layoutAction.setEnabled(true);
225         }
226       }
227     });
228   }
229 
230   private void initGraph(Graph2D graph) {
231     Node n1 = graph.createNode();
232     layerIdMap.setInt(n1, 0);
233     Node n2 = graph.createNode(100.0, 0.0);
234     layerIdMap.setInt(n2, 1);
235     Node n3 = graph.createNode();
236     layerIdMap.setInt(n3, 2);
237     Node n4 = graph.createNode();
238     layerIdMap.setInt(n4, 2);
239     graph.createEdge(n1, n2);
240     graph.createEdge(n2, n4);
241     graph.createEdge(n1, n3);
242     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
243       Node n = nc.node();
244       graph.getRealizer(n).setLabelText(Integer.toString(n.index() + 1));
245     }
246     calcLayout();
247 
248     getUndoManager().resetQueue();
249   }
250 
251   /**
252    * Overwritten to create a custom undo manager that updates the layer drawable after undo and redo.
253    */
254   protected Graph2DUndoManager getUndoManager() {
255     if (undoManager == null) {
256       undoManager = new Graph2DUndoManager(view.getGraph2D()) {
257         public void undo() {
258           super.undo();
259           layerDrawable.updateLayers();
260         }
261 
262         public void redo() {
263           super.redo();
264           layerDrawable.updateLayers();
265         }
266       };
267       undoManager.setViewContainer(view);
268     }
269     return undoManager;
270   }
271 
272   /**
273    * Overwritten to use a custom paste action that also enables the layout button.
274    */
275   protected Action createPasteAction() {
276     final CustomPasteAction action = new CustomPasteAction(getClipboard().getPasteAction());
277     action.putValue(Action.SMALL_ICON, getIconResource("resource/paste.png"));
278     action.putValue(Action.SHORT_DESCRIPTION, "Paste");
279     return action;
280   }
281 
282   /**
283    * Overwritten to replace the default paste action with a custom paste action that also enables the layout button.
284    */
285   protected void registerViewActions() {
286     super.registerViewActions();
287     view.getCanvasComponent().getActionMap().put("PASTE", createPasteAction());
288   }
289 
290   protected void registerViewModes() {
291     EditMode editMode = new IncrementalEditMode();
292     editMode.setMoveSelectionMode(paMode = new IncrementalMoveSelectionMode());
293     editMode.setPopupMode(new IncrementalPopupMode());
294     editMode.setCreateEdgeMode(new IncrementalEdgeCreateMode());
295     editMode.setHotSpotMode(new IncrementalHotSpotMode());
296     view.addViewMode(editMode);
297   }
298 
299   protected JToolBar createToolBar() {
300     JToolBar bar = super.createToolBar();
301     bar.addSeparator();
302     layoutAction = new LayoutAction();
303     bar.add(createActionControl(layoutAction, true));
304     return bar;
305   }
306 
307   protected void loadGraph(URL resource) {
308     loaded = true;
309     layoutAction.setEnabled(true);
310     if (layerDrawable != null) {
311       layerDrawable.clearLayers();
312     }
313     super.loadGraph(resource);
314   }
315 
316   /**
317    * Simple Layout action (incremental)
318    */
319   final class LayoutAction extends AbstractAction {
320     LayoutAction() {
321       super("Layout");
322       this.putValue(Action.SMALL_ICON, getIconResource("resource/layout.png"));
323       this.putValue(Action.SHORT_DESCRIPTION, "Apply first layout when a new graph is loaded.");
324     }
325 
326     public void actionPerformed(ActionEvent ev) {
327       calcLayout();
328     }
329   }
330 
331   /**
332    * Simple Layout action (from scratch)
333    */
334   final class FreshLayoutAction extends AbstractAction {
335     boolean resetPCs;
336 
337     FreshLayoutAction(String name, boolean resetPCs) {
338       super(name);
339       this.resetPCs = resetPCs;
340     }
341 
342     public void actionPerformed(ActionEvent ev) {
343       if (resetPCs) {
344         for (EdgeCursor ec = view.getGraph2D().edges(); ec.ok(); ec.next()) {
345           sourcePortMap.set(ec.edge(), null);
346           targetPortMap.set(ec.edge(), null);
347         }
348       }
349       byte oldMode = hierarchicLayouter.getLayoutMode();
350       hierarchicLayouter.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_FROM_SCRATCH);
351       try {
352         calcLayout();
353       } finally {
354         hierarchicLayouter.setLayoutMode(oldMode);
355       }
356     }
357   }
358 
359   /**
360    * Optimizes nodes (inserts or recalculates layouts incrementally)
361    */
362   final class OptimizeNodesAction extends AbstractAction {
363     private NodeCursor nc;
364     private boolean resetPCs;
365 
366     public OptimizeNodesAction(String name, NodeCursor nodes, boolean resetPCs) {
367       super(name);
368       this.nc = nodes;
369       this.resetPCs = resetPCs;
370     }
371 
372     public void actionPerformed(ActionEvent ae) {
373       for (nc.toFirst(); nc.ok(); nc.next()) {
374         Node v = nc.node();
375         hintMap.set(v, hintsFactory.createLayerIncrementallyHint(v));
376         if (resetPCs) {
377           for (EdgeCursor ec = v.edges(); ec.ok(); ec.next()) {
378             if (ec.edge().source() == v) {
379               sourcePortMap.set(ec.edge(), null);
380             } else {
381               targetPortMap.set(ec.edge(), null);
382             }
383           }
384         }
385       }
386       calcLayout();
387       for (nc.toFirst(); nc.ok(); nc.next()) {
388         Node v = nc.node();
389         hintMap.set(v, null);
390       }
391     }
392   }
393 
394   /**
395    * Fixes nodes (inserts or recalculates layouts incrementally)
396    */
397   final class FixNodesAction extends AbstractAction {
398     private final NodeCursor nc;
399     private final boolean layer;
400     private final boolean sequence;
401 
402     public FixNodesAction(String name, NodeCursor nodes, boolean layer, boolean sequence) {
403       super(name);
404       this.nc = nodes;
405       this.layer = layer;
406       this.sequence = sequence;
407     }
408 
409     public void actionPerformed(ActionEvent ae) {
410       for (nc.toFirst(); nc.ok(); nc.next()) {
411         Node v = nc.node();
412         if (layer && sequence) {
413           hintMap.set(v, hintsFactory.createUseExactCoordinatesHint(v));
414           NodeRealizer realizer = view.getGraph2D().getRealizer(v);
415           realizer.setFillColor(Color.red);
416           realizer.repaint();
417         } else if (layer) {
418           hintMap.set(v, hintsFactory.createUseExactLayerCoordinatesHint(v));
419           NodeRealizer realizer = view.getGraph2D().getRealizer(v);
420           realizer.setFillColor(Color.red.darker());
421           realizer.repaint();
422         } else if (sequence) {
423           hintMap.set(v, hintsFactory.createUseExactSequenceCoordinatesHint(v));
424           NodeRealizer realizer = view.getGraph2D().getRealizer(v);
425           realizer.setFillColor(Color.red.darker().darker());
426           realizer.repaint();
427         } else {
428           hintMap.set(v, null);
429           NodeRealizer realizer = view.getGraph2D().getRealizer(v);
430           realizer.setFillColor(view.getGraph2D().getDefaultNodeRealizer().getFillColor());
431           realizer.repaint();
432         }
433       }
434     }
435   }
436 
437   /**
438    * Optimizes edges (inserts or recalculates layouts incrementally)
439    */
440   final class OptimizeEdgesAction extends AbstractAction {
441     private EdgeCursor ec;
442     private boolean resetPCs;
443 
444     public OptimizeEdgesAction(String name, EdgeCursor edges, boolean resetPCs) {
445       super(name);
446       this.ec = edges;
447       this.resetPCs = resetPCs;
448     }
449 
450     public void actionPerformed(ActionEvent ae) {
451       for (ec.toFirst(); ec.ok(); ec.next()) {
452         final Edge edge = ec.edge();
453         hintMap.set(edge, hintsFactory.createSequenceIncrementallyHint(edge));
454         if (resetPCs) {
455           sourcePortMap.set(edge, null);
456           targetPortMap.set(edge, null);
457         }
458       }
459       calcLayout();
460       for (ec.toFirst(); ec.ok(); ec.next()) {
461         Edge e = ec.edge();
462         hintMap.set(e, null);
463       }
464     }
465   }
466 
467   /**
468    * Drawable implementation and utility functions
469    */
470   static final class LayerDrawable implements Drawable {
471 
472     private static final Color[] colors = {new Color(150,200,255,128), new Color(220,240,240,128)};
473 
474     private List layers = new ArrayList(20);
475     private Rectangle bounds = new Rectangle(20, 20, 200, 200);
476 
477     private Graph2D graph;
478     private NodeMap layerIdMap;
479     private AsIsLayerer ail;
480 
481     LayerDrawable(Graph2D graph, NodeMap layerIdMap) {
482       this.graph = graph;
483       this.layerIdMap = layerIdMap;
484       this.ail = new AsIsLayerer();
485     }
486 
487     public Rectangle getBounds() {
488       return bounds;
489     }
490 
491     public void clearLayers() {
492       layers.clear();
493     }
494 
495     public void updateLayers() {
496       final double spacing = 20.0d;
497       layers.clear();
498       updateLayerIds();
499       if (graph.N() < 1) {
500         return;
501       }
502       double minX = Double.MAX_VALUE, maxX = -Double.MAX_VALUE;
503       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
504         final Node node = nc.node();
505         final int layer = layerIdMap.getInt(node);
506         if (layer < 0) {
507           continue;
508         }
509         while (layers.size() - 1 < layer) {
510           layers.add(new Rectangle2D.Double(0.0, 0.0, -1.0, -1.0));
511         }
512         Rectangle2D.Double layerRect = (Rectangle2D.Double) layers.get(layer);
513         final NodeLayout nl = graph.getNodeLayout(node);
514         if (layerRect.width < 0.0) {
515           layerRect.setFrame(nl.getX(), nl.getY(), nl.getWidth(), nl.getHeight());
516         } else {
517           layerRect.add(nl.getX(), nl.getY());
518           layerRect.add(nl.getX() + nl.getWidth(), nl.getY() + nl.getHeight());
519         }
520         minX = Math.min(nl.getX(), minX);
521         maxX = Math.max(nl.getX() + nl.getWidth(), maxX);
522       }
523 
524       double minY = Double.MAX_VALUE;
525       double maxY = -Double.MAX_VALUE;
526       for (int i = 0; i < layers.size(); i++) {
527         Rectangle2D.Double rect = (Rectangle2D.Double) layers.get(i);
528         rect.x = minX - spacing;
529         rect.width = maxX - minX + spacing * 2.0;
530         if (i == 0) {
531           rect.y -= spacing;
532           rect.height += spacing;
533           minY = rect.y;
534         }
535         if (i == layers.size() - 1) {
536           rect.height += spacing;
537           maxY = rect.height + rect.y;
538         } else if (i < layers.size() - 1) {
539           Rectangle2D.Double nextRect = (Rectangle2D.Double) layers.get(i + 1);
540           final double mid = (rect.getY() + rect.getHeight() + nextRect.getY()) * 0.5d;
541           rect.height += mid - (rect.y + rect.height);
542           final double nextDelta = mid - nextRect.y;
543           nextRect.y += nextDelta;
544           nextRect.height -= nextDelta;
545         }
546       }
547       bounds.setFrame(minX - spacing, minY, maxX - minX + 2.0 * spacing, maxY - minY);
548       graph.updateViews();
549     }
550 
551     private void updateLayerIds() {
552       final EdgeList reversedEdges = new EdgeList();
553 
554       // calculate the layer ids with AsIsLayerer
555       ail.assignNodeLayer(graph, layerIdMap, reversedEdges);
556 
557       // reverse all edges the layerer reversed to restore the original direction
558       for (EdgeCursor ec = reversedEdges.edges(); ec.ok(); ec.next()) {
559         final Edge edge = ec.edge();
560         graph.reverseEdge(edge);
561       }
562     }
563 
564     public final int inset = 8;
565 
566     public int getLayerId(double x, double y) {
567       if (x < bounds.x - outerInsets || x > bounds.x + bounds.width + outerInsets) {
568         return Integer.MAX_VALUE;
569       }
570       if (y < bounds.y + inset) {
571         return -1;
572       }
573       if (y > bounds.y + bounds.height - inset) {
574         return layers.size();
575       }
576       for (int i = 0; i < layers.size(); i++) {
577         final Rectangle2D.Double rect = (Rectangle2D.Double) layers.get(i);
578         if (y >= rect.y + inset && y <= rect.y + rect.height - inset) {
579           return i;
580         } else if (y < rect.y + inset) {
581           return -(i + 1);
582         }
583       }
584       return Integer.MAX_VALUE;
585     }
586 
587     public static final double outerInsets = 40.0;
588 
589     public Rectangle2D getLayerBounds(int layer) {
590       if (layer >= 0 && layer < layers.size()) {
591         Rectangle2D.Double rect = (Rectangle2D.Double) layers.get(layer);
592         rect = new Rectangle2D.Double(rect.x, rect.y + inset, rect.width, rect.height - (double) (2 * inset));
593         return rect;
594       }
595       if (layer == -1) {
596         return new Rectangle2D.Double(bounds.x, bounds.y - outerInsets, bounds.width, outerInsets + inset);
597       }
598       if (layer >= layers.size() && (layer != Integer.MAX_VALUE)) {
599         return new Rectangle2D.Double(bounds.x, bounds.y + bounds.height - inset, bounds.width, outerInsets);
600       }
601       if (layer < 0) {
602         int beforeLayer = -(layer + 1);
603         if (beforeLayer < layers.size()) {
604           Rectangle2D.Double rect = (Rectangle2D.Double) layers.get(beforeLayer);
605           rect = new Rectangle2D.Double(rect.x, rect.y - inset, rect.width, (double) (2 * inset));
606           return rect;
607         }
608       }
609       return new Rectangle2D.Double(bounds.x - 2 * outerInsets, bounds.y - 2 * outerInsets,
610           bounds.width + 4.0 * outerInsets, bounds.height + outerInsets * 4.0);
611     }
612 
613     public void paint(Graphics2D g) {
614       for (int i = 0; i < layers.size(); i++) {
615         Color color = colors[i % colors.length];
616         g.setColor(color);
617         g.fill((Shape) layers.get(i));
618       }
619     }
620   }
621 
622   /**
623    * Animated layout assignment
624    */
625   public void calcLayout() {
626     if (loaded) {
627       hierarchicLayouter.setFixedElementsLayerer(new AsIsLayerer());
628     } else {
629       hierarchicLayouter.setFixedElementsLayerer(gll);
630     }
631     loaded = false;
632     layoutAction.setEnabled(false);
633 
634     if (!view.getGraph2D().isEmpty()) {
635       gll.normalize(view.getGraph2D(), layerIdMap, layerIdMap);
636       Cursor oldCursor = view.getViewCursor();
637       try {
638         view.setViewCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
639         view.applyLayoutAnimated(hierarchicLayouter);
640       } finally {
641         view.setViewCursor(oldCursor);
642       }
643     }
644     layerDrawable.updateLayers();
645     view.updateView();
646   }
647 
648   /**
649    * Recalculate layout after resizing.
650    */
651   final class IncrementalHotSpotMode extends HotSpotMode {
652     public void mousePressedLeft(double x, double y) {
653       final Graph2D graph = getGraph2D();
654       // fire event to mark start of resizing for undo/redo
655       graph.firePreEvent();
656       graph.backupRealizers();
657       super.mousePressedLeft(x, y);
658     }
659 
660     public void mouseReleasedLeft(double x, double y) {
661       super.mouseReleasedLeft(x, y);
662       calcLayout();
663       // fire event to mark end of resizing (including layout) for undo/redo
664       getGraph2D().firePostEvent();
665     }
666   }
667 
668   /**
669    * Recalculate layout after edge creation
670    */
671   final class IncrementalEdgeCreateMode extends CreateEdgeMode {
672     protected Edge createEdge(Graph2D graph, Node startNode, Node targetNode, EdgeRealizer realizer) {
673       // fire event to mark start of edge creation for undo/redo
674       graph.firePreEvent();
675       return super.createEdge(graph, startNode, targetNode, realizer);
676     }
677 
678     protected void edgeCreated(Edge edge) {
679       super.edgeCreated(edge);
680       EdgeRealizer er = view.getGraph2D().getRealizer(edge);
681       if (er.bendCount() > 0) {
682         parseBend(er.getBend(0));
683       }
684       if (er.bendCount() > 1) {
685         parseBend(er.getBend(er.bendCount() - 1));
686       }
687       if (er.bendCount() == 0) {
688         hintMap.set(edge, hintsFactory.createSequenceIncrementallyHint(edge));
689       }
690       calcLayout();
691       hintMap.set(edge, null);
692 
693       // fire event to mark end of edge creation (including layout) for undo/redo
694       getGraph2D().firePostEvent();
695     }
696   }
697 
698   /**
699    * Recalculate layout after node creation
700    */
701   final class IncrementalEditMode extends EditMode {
702     protected Node createNode(Graph2D graph, double x, double y) {
703       // fire event to mark start of edge creation for undo/redo
704       graph.firePreEvent();
705       return super.createNode(graph, x, y);
706     }
707 
708     protected void nodeCreated(Node v) {
709       super.nodeCreated(v);
710       final YPoint center = view.getGraph2D().getCenter(v);
711       int layerId = layerDrawable.getLayerId(center.x, center.y);
712       setLayers(new NodeList(v).nodes(), layerId, Integer.MAX_VALUE);
713       if (lastReleaseEvent.isControlDown()) { // fix the nodes position
714         hintMap.set(v, hintsFactory.createUseExactCoordinatesHint(v));
715         view.getGraph2D().getRealizer(v).setFillColor(Color.red);
716       }
717       calcLayout();
718       // fire event to mark start of node creation (including layout) for undo/redo
719       getGraph2D().firePostEvent();
720     }
721   }
722 
723   /**
724    * Utility method to assign nodes to a new layer.
725    */
726   protected void setLayers(NodeCursor nodes, int newLayer, int previousLayer) {
727     if (!nodes.ok()) {
728       return;
729     }
730     //calculate number of layers to insert
731     int lesserLayers = 0;
732     int greaterLayers = 0;
733     final Set nodeSet = new HashSet();
734     for (nodes.toFirst(); nodes.ok(); nodes.next()) {
735       nodeSet.add(nodes.node());
736     }
737     if (previousLayer != Integer.MAX_VALUE) {
738       for (nodes.toFirst(); nodes.ok(); nodes.next()) {
739         int pLayer = layerIdMap.getInt(nodes.node());
740         if (pLayer < previousLayer) {
741           lesserLayers = Math.max(lesserLayers, previousLayer - pLayer);
742         }
743         if (pLayer > previousLayer) {
744           greaterLayers = Math.max(greaterLayers, pLayer - previousLayer);
745         }
746       }
747     } else {
748       previousLayer = 0;
749     }
750     final int newLayerCount = lesserLayers + greaterLayers + 1;
751     if (newLayer < 0) {
752       int beforeLayer = -(newLayer + 1);
753       for (NodeCursor nc = view.getGraph2D().nodes(); nc.ok(); nc.next()) {
754         if (!nodeSet.contains(nc.node())) {
755           int oldLayer = layerIdMap.getInt(nc.node());
756           if (oldLayer >= beforeLayer) {
757             layerIdMap.setInt(nc.node(), oldLayer + newLayerCount);
758           }
759         }
760       }
761       for (nodes.toFirst(); nodes.ok(); nodes.next()) {
762         int oldLayer = layerIdMap.getInt(nodes.node());
763         layerIdMap.setInt(nodes.node(), beforeLayer + lesserLayers + oldLayer - previousLayer);
764       }
765     } else {
766       if (newLayer == Integer.MAX_VALUE) {
767         int maxLayer = -1;
768         for (NodeCursor nc = view.getGraph2D().nodes(); nc.ok(); nc.next()) {
769           if (!nodeSet.contains(nc.node())) {
770             int layer = layerIdMap.getInt(nc.node());
771             maxLayer = Math.max(layer, maxLayer);
772           }
773         }
774         newLayer = maxLayer + 1;
775       }
776       if (lesserLayers > 0 || greaterLayers > 0) {
777         for (NodeCursor nc = view.getGraph2D().nodes(); nc.ok(); nc.next()) {
778           if (!nodeSet.contains(nc.node())) {
779             int layer = layerIdMap.getInt(nc.node());
780             if (layer == newLayer) {
781               layerIdMap.setInt(nc.node(), layer + lesserLayers);
782             } else if (layer > newLayer) {
783               layerIdMap.setInt(nc.node(), layer + newLayerCount);
784             }
785           }
786         }
787       }
788       for (nodes.toFirst(); nodes.ok(); nodes.next()) {
789         int oldLayer = layerIdMap.getInt(nodes.node());
790         layerIdMap.setInt(nodes.node(), newLayer + lesserLayers + oldLayer - previousLayer);
791       }
792     }
793   }
794 
795   /**
796    * Utility method to assign PCs from the sketch
797    */
798   public void parseBend(Bend b) {
799     Edge e = b.getEdge();
800     EdgeRealizer er = view.getGraph2D().getRealizer(e);
801     if (b == er.getBend(0)) {
802       YPoint center = view.getGraph2D().getCenter(e.source());
803       sourcePortMap.set(e, getPortConstraint(b.getX() - center.x, b.getY() - center.y));
804     }
805     if (b == er.getBend(er.bendCount() - 1)) {
806       YPoint center = view.getGraph2D().getCenter(e.target());
807       targetPortMap.set(e, getPortConstraint(b.getX() - center.x, b.getY() - center.y));
808     }
809   }
810 
811   /**
812    * Helper method to assign PCs from the sketch
813    */
814   private static PortConstraint getPortConstraint(final double bdx, final double bdy) {
815     if (Math.abs(bdx) > Math.abs(bdy)) {
816       return PortConstraint.create(bdx > 0.0 ? PortConstraint.EAST : PortConstraint.WEST);
817     } else {
818       return PortConstraint.create(bdy > 0.0 ? PortConstraint.SOUTH : PortConstraint.NORTH);
819     }
820   }
821 
822   /**
823    * Provides popup menus for all kinds of actions
824    */
825   final class IncrementalPopupMode extends PopupMode {
826 
827     public JPopupMenu getNodePopup(final Node v) {
828       JPopupMenu pm = new JPopupMenu();
829       NodeCursor node = new NodeList(v).nodes();
830       addNodeActions(pm, node);
831       return pm;
832     }
833 
834     private void addNodeActions(JPopupMenu pm, NodeCursor node) {
835       pm.add(new OptimizeNodesAction("Optimize Node", node, false));
836       pm.add(new OptimizeNodesAction("Optimize Node and Reset PCs", node, true));
837       JMenu fixNodesMenu = new JMenu("Fix nodes");
838       pm.add(fixNodesMenu);
839       fixNodesMenu.add(new FixNodesAction("Fix Coordinates", node, true, true));
840       fixNodesMenu.add(new FixNodesAction("Fix Layer Coordinates", node, true, false));
841       fixNodesMenu.add(new FixNodesAction("Fix Sequence Coordinates", node, false, true));
842       fixNodesMenu.add(new FixNodesAction("Unfix Coordinates", node, false, false));
843     }
844 
845     public JPopupMenu getEdgePopup(final Edge e) {
846       JPopupMenu pm = new JPopupMenu();
847       addEdgeActions(pm, new EdgeList(e).edges());
848       return pm;
849     }
850 
851     public JPopupMenu getSelectionPopup(double x, double y) {
852       JPopupMenu pm = new JPopupMenu();
853       final NodeCursor snc = getGraph2D().selectedNodes();
854       if (snc.ok()) {
855         addNodeActions(pm, snc);
856       } else {
857         final EdgeCursor sec = getGraph2D().selectedEdges();
858         if (sec.ok()) {
859           addEdgeActions(pm, sec);
860         } else {
861           return null;
862         }
863       }
864       return pm;
865     }
866 
867     private void addEdgeActions(JPopupMenu pm, EdgeCursor sec) {
868       pm.add(new OptimizeEdgesAction("Optimize Edges", sec, false));
869       pm.add(new OptimizeEdgesAction("Optimize Edges and Reset PCs", sec, true));
870     }
871 
872     public JPopupMenu getPaperPopup(double x, double y) {
873       if (getGraph2D().isEmpty()) {
874         return null;
875       }
876       JPopupMenu pm = new JPopupMenu();
877       pm.add(new FreshLayoutAction("Fresh Layout", false));
878       pm.add(new FreshLayoutAction("Fresh Layout and Reset PCs", true));
879       if (getGraph2D().E() > 0) {
880         addEdgeActions(pm, getGraph2D().edges());
881       }
882       return pm;
883     }
884   }
885 
886   /**
887    * Recalculate layout after selection move
888    */
889   final class IncrementalMoveSelectionMode extends PortAssignmentMoveSelectionMode {
890     private boolean firstTime = true;
891     private MoveSelectionDrawable drawable;
892     private NodeList selectedNodes;
893     private BendList selectedBends;
894 
895     IncrementalMoveSelectionMode() {
896       super(null, null);
897     }
898 
899     protected void selectionMoveStarted(double x, double y) {
900       // fire event to mark start of moving the selection for undo/redo
901       getGraph2D().firePreEvent();
902       super.selectionMoveStarted(x, y);
903     }
904 
905     protected void selectionMovedAction(double dx, double dy, double x, double y) {
906       super.selectionMovedAction(dx, dy, x, y);
907       if (selectedNodes != null) {
908         view.removeBackgroundDrawable(drawable);
909         drawable = null;
910         int newLayer = layerDrawable.getLayerId(x, y);
911         HitInfo hi = this.getLastHitInfo();
912         Node movedNode = hi.getHitNode();
913         int originalLayer = movedNode != null ? layerIdMap.getInt(movedNode) : Integer.MAX_VALUE;
914         if (newLayer != originalLayer) {
915           setLayers(selectedNodes.nodes(), newLayer, originalLayer);
916         }
917         if (newLayer != Integer.MAX_VALUE) {
918           List hints = new ArrayList(128);
919           for (NodeCursor nc = selectedNodes.nodes(); nc.ok(); nc.next()) {
920             for (EdgeCursor edges = nc.node().edges(); edges.ok(); edges.next()) {
921               hints.add(edges.edge());
922               hintMap.set(edges.edge(), hintsFactory.createSequenceIncrementallyHint(edges.edge()));
923             }
924           }
925           calcLayout();
926           for (int i = 0; i < hints.size(); i++) {
927             hintMap.set(hints.get(i), null);
928           }
929         } else {
930           List hints = new ArrayList(128);
931           for (NodeCursor nc = selectedNodes.nodes(); nc.ok(); nc.next()) {
932             hints.add(nc.node());
933             hintMap.set(nc.node(), hintsFactory.createLayerIncrementallyHint(nc.node()));
934           }
935           calcLayout();
936           for (int i = 0; i < hints.size(); i++) {
937             Node node = (Node) hints.get(i);
938             hintMap.set(node, null);
939           }
940           layerDrawable.updateLayers();
941         }
942         selectedNodes = null;
943       } else if (selectedBends != null) {
944         calcLayout();
945       }
946       selectedBends = null;
947       selectedNodes = null;
948       firstTime = true;
949 
950       // fire event to mark end of moving the selection for undo/redo
951       getGraph2D().firePostEvent();
952     }
953 
954     protected void selectionOnMove(double dx, double dy, double x, double y) {
955       if (firstTime) {
956         firstTime = false;
957         Graph2D g = getGraph2D();
958         NodeCursor nc = g.selectedNodes();
959         selectedBends = null;
960         selectedNodes = null;
961         if (nc.ok()) {
962           selectedNodes = new NodeList(nc);
963           drawable = new MoveSelectionDrawable();
964           view.addBackgroundDrawable(drawable);
965         }
966         BendCursor bc = g.selectedBends();
967         if (selectedNodes == null && bc.ok()) {
968           selectedBends = new BendList(bc);
969         }
970       }
971       super.selectionOnMove(dx, dy, x, y);
972       if (selectedNodes != null) {
973         int layer = layerDrawable.getLayerId(x, y);
974         drawable.layer = layer;
975         drawable.layerCount = layerDrawable.layers.size();
976         drawable.drawable = layerDrawable.getLayerBounds(layer);
977       }
978     }
979 
980     final class MoveSelectionDrawable implements Drawable {
981       Shape drawable;
982       int layer;
983       int layerCount;
984       Color color = Color.red;
985       Color color2 = Color.orange;
986       Color color3 = Color.red.darker();
987 
988       public Rectangle getBounds() {
989         return drawable.getBounds();
990       }
991 
992       public void paint(Graphics2D g) {
993         Stroke s = g.getStroke();
994         if (layer == Integer.MAX_VALUE) {
995           g.setColor(color3);
996           g.setStroke(LineType.DOTTED_3);
997           g.draw(drawable);
998         } else {
999           if (layer >= 0 && layer < layerCount) {
1000            g.setColor(color);
1001            g.setStroke(LineType.LINE_3);
1002            g.draw(drawable);
1003          } else {
1004            g.setColor(color2);
1005            g.fill(drawable);
1006          }
1007        }
1008        g.setStroke(s);
1009      }
1010    }
1011  }
1012
1013  /**
1014   * Launches this demo.
1015   */
1016  public static void main(String[] args) {
1017    EventQueue.invokeLater(new Runnable() {
1018      public void run() {
1019        Locale.setDefault(Locale.ENGLISH);
1020        initLnF();
1021        (new IncrementalHierarchicLayouterDemo()).start("Incremental Hierarchic Layouter Demo");
1022      }
1023    });
1024  }
1025
1026  /**
1027   * Decorates {@link y.view.Graph2DClipboard}'s paste action to additionally enable the layout button after the action
1028   * was performed.
1029   */
1030  private class CustomPasteAction implements Action {
1031    private final Action pasteAction;
1032
1033    public CustomPasteAction(Action pasteAction) {
1034      this.pasteAction = pasteAction;
1035    }
1036
1037    public boolean isEnabled() {
1038      return pasteAction.isEnabled();
1039    }
1040
1041    public void setEnabled(boolean b) {
1042      pasteAction.setEnabled(b);
1043    }
1044
1045    public void addPropertyChangeListener(PropertyChangeListener listener) {
1046      pasteAction.addPropertyChangeListener(listener);
1047    }
1048
1049    public void removePropertyChangeListener(PropertyChangeListener listener) {
1050      pasteAction.removePropertyChangeListener(listener);
1051    }
1052
1053    public Object getValue(String key) {
1054      return pasteAction.getValue(key);
1055    }
1056
1057    public void putValue(String key, Object value) {
1058      pasteAction.putValue(key, value);
1059    }
1060
1061    public void actionPerformed(ActionEvent e) {
1062      pasteAction.actionPerformed(e);
1063
1064      // enable action that evokes layout calculation
1065      layoutAction.setEnabled(true);
1066    }
1067  }
1068}
1069