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.advanced.ports;
29  
30  import demo.view.hierarchy.GroupingDemo;
31  import y.base.Edge;
32  import y.base.Node;
33  import y.base.NodeCursor;
34  import y.base.NodeMap;
35  import y.geom.YRectangle;
36  import y.io.GraphMLIOHandler;
37  import y.io.graphml.graph2d.Graph2DGraphMLHandler;
38  import y.layout.LayoutOrientation;                                              
39  import y.layout.Layouter;                                                       
40  import y.layout.NodeHalo;                                                       
41  import y.layout.hierarchic.IncrementalHierarchicLayouter;                       
42  import y.layout.hierarchic.incremental.RoutingStyle;                            
43  import y.layout.hierarchic.incremental.SimplexNodePlacer;                       
44  import y.layout.router.polyline.EdgeRouter;                                     
45  import y.util.Maps;
46  import y.view.EdgeRealizer;
47  import y.view.EditMode;
48  import y.view.GenericNodeRealizer;
49  import y.view.Graph2D;
50  import y.view.Graph2DLayoutExecutor;                                            
51  import y.view.Graph2DViewActions;
52  import y.view.NodeLabel;
53  import y.view.NodePort;
54  import y.view.NodePortLayoutConfigurator;                                       
55  import y.view.NodeRealizer;
56  import y.view.NodeStateChangeEdgeRouter;
57  import y.view.ProxyShapeNodeRealizer;
58  import y.view.ShapeNodePainter;
59  import y.view.TooltipMode;
60  import y.view.hierarchy.DefaultGenericAutoBoundsFeature;
61  import y.view.hierarchy.DefaultHierarchyGraphFactory;
62  import y.view.hierarchy.GenericGroupNodeRealizer;
63  import y.view.hierarchy.GroupNodePainter;
64  import y.view.hierarchy.HierarchyManager;
65  
66  import java.awt.BorderLayout;
67  import java.awt.Color;
68  import java.awt.EventQueue;
69  import java.awt.Graphics2D;
70  import java.awt.Shape;
71  import java.awt.event.ActionEvent;
72  import java.awt.event.ItemEvent;
73  import java.awt.event.ItemListener;
74  import java.awt.geom.Rectangle2D;
75  import java.util.Locale;
76  import java.util.Map;
77  import java.util.WeakHashMap;
78  import javax.swing.AbstractAction;
79  import javax.swing.Action;
80  import javax.swing.ActionMap;
81  import javax.swing.BorderFactory;
82  import javax.swing.ButtonGroup;
83  import javax.swing.JSplitPane;
84  import javax.swing.JToggleButton;
85  import javax.swing.JToolBar;
86  
87  /**
88   * Demonstrates how to use {@link y.view.NodePort}s.
89   * <p>
90   * Things to try:
91   * </p>
92   * <ul>
93   *   <li>Left-click on a port to select it.</li>
94   *   <li>Drag a selected port to move it around.</li>
95   *   <li>Press DELETE to remove all selected ports as well as all edges
96   *       connecting to selected ports and all labels associated top selected
97   *       ports.</li>
98   *   <li>Drag a port that is not selected to start creating an edge from that
99   *       port.</li>
100  *   <li>Right-click on a node to display a context menu that allows for
101  *       adding additional ports to a node.</li>
102  *   <li>Right-click on a port to display a context menu that allows for
103  *     <ul>
104  *       <li>... adding a label that is associated to the port.</li>
105  *       <li>... changing the valid positions of the port.</li>
106  *       <li>... removing the port.</li>
107  *     </ul>
108  *   </li>
109  *   <li>Select one or more ports then press CONTROL+A to select all ports.</li>
110  *   <li>Select one or more ports then use the selection box (by dragging from
111  *       an empty point) to select additional ports.</li>
112  *   <li>Select one or more nodes then press CONTROL+ALT+G to create a common
113  *       parent group node for the selected nodes.
114  * </ul>
115  * <p>
116  * Class {@link PortConfigurations} demonstrate how to customize the visual
117  * appearance of ports by re-using existing visualizations for nodes.
118  * <p>
119  * Nested classes {@link NormalEdgeProcessor} and {@link InterEdgeProcessor}
120  * demonstrate how to update edge-to-port associations when a group node is
121  * closed or a folder node is opened.
122  * Method {@link #createNodeHaloMap()} shows how to use
123  * {@link y.layout.NodeHalo}s to prevent layout and/or edge routing algorithms
124  * from generating node port overlaps.
125  * </p>
126  *
127  * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/realizer_related" target="_blank">Section Realizer-Related Features</a> in the yFiles for Java Developer's Guide
128  */
129 public class NodePortsDemo extends GroupingDemo {
130   private boolean fixPortsForLayout;                                            
131   private boolean sharedPorts;                                                  
132   private boolean groupEdges;                                                   
133 
134   public NodePortsDemo() {
135     this(null);
136   }
137 
138   public NodePortsDemo( final String helpFilePath ) {
139     final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new Palette(view), view);
140     splitPane.setBorder(BorderFactory.createEmptyBorder());
141     contentPane.add(splitPane, BorderLayout.CENTER);
142     addHelpPane(helpFilePath);
143   }
144 
145   /**
146    * Overwritten to initialize the demo's hierarchy manager.
147    */
148   protected void initialize() {
149     super.initialize();
150 
151     new HierarchyManager(view.getGraph2D());
152   }
153 
154   /**
155    * Overwritten to add {@link y.view.NodePort} aware actions to handle closing group nodes and
156    * opening folder nodes.
157    */
158   protected void registerViewActions() {
159     super.registerViewActions();
160 
161     final ActionMap amap = view.getCanvasComponent().getActionMap();
162 
163     final Graph2DViewActions.CloseGroupsAction closeGroups =
164             new Graph2DViewActions.CloseGroupsAction(view);
165     // add callback implementations that ensure edges connected to ports are
166     // correctly reassigned when proxy realizers are used
167     // additionally, InterEdgeProcessor will assign all inter edges to the
168     // corresponding folder node's first port (if there is one)
169     // see class documentation
170     closeGroups.setNodeStateChangeHandler(new MyNodeStateChangeEdgeRouter(
171             new InterEdgeProcessor(new NormalEdgeProcessor())));
172     amap.put(Graph2DViewActions.CLOSE_GROUPS, closeGroups);
173     final Graph2DViewActions.OpenFoldersAction openFolders =
174             new Graph2DViewActions.OpenFoldersAction(view);
175     // add callback implementations that ensure edges connected to ports are
176     // correctly reassigned when proxy realizers are used
177     // see class documentation
178     openFolders.setNodeStateChangeHandler(new MyNodeStateChangeEdgeRouter(
179             new NormalEdgeProcessor()));
180     amap.put(Graph2DViewActions.OPEN_FOLDERS, openFolders);
181   }
182 
183   /**
184    * Overwritten to create a {@link demo.view.advanced.ports.PortEditMode}
185    * instance that provides {@link y.view.NodePort} support.
186    * @return a {@link demo.view.advanced.ports.PortEditMode} instance.
187    */
188   protected EditMode createEditMode() {
189     return new PortEditMode();
190   }
191 
192   /**
193    * Overwritten to enable tooltips only for node ports.
194    * <p>
195    * A tool tip will be displayed for a node port, if the node port has
196    * at least one label. The tool tip text will be the label text.
197    * Tool tip texts for node ports may be customized by overwriting
198    * {@link TooltipMode#getNodePortTip(y.view.NodePort)}.
199    * </p>
200    */
201   protected TooltipMode createTooltipMode() {
202     final TooltipMode tooltipMode = new TooltipMode();
203     tooltipMode.setNodeTipEnabled(false);
204     tooltipMode.setEdgeTipEnabled(false);
205     tooltipMode.setPortTipEnabled(true);
206     return tooltipMode;
207   }
208 
209   /**
210    * Overwritten to add controls for layout and router options.
211    * @return the application tool bar.
212    */
213   protected JToolBar createToolBar() {
214     final Action edgeRoutingAction = new AbstractAction("Route Edges") {                       
215       public void actionPerformed(final ActionEvent e) {                                       
216         routeOrthogonally();                                                                   
217       }                                                                                        
218     };                                                                                         
219     edgeRoutingAction.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);                         
220 
221     final Action layoutAction = new AbstractAction("Layout") {                                 
222       public void actionPerformed(final ActionEvent e) {                                       
223         layoutHierarchically();                                                                
224       }                                                                                        
225     };                                                                                         
226     layoutAction.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);                              
227 
228 
229     final JToolBar jtb = super.createToolBar();
230     jtb.addSeparator();                                                                        
231     jtb.add(createActionControl(layoutAction));                                                
232     jtb.add(createActionControl(edgeRoutingAction));                                           
233     jtb.addSeparator(TOOLBAR_SMALL_SEPARATOR);                                                 
234     final JToggleButton fpJb = new JToggleButton("Fixed Ports");                               
235     fpJb.setToolTipText("Toggle the 'Fixed Ports' feature of the layout algorithms");          
236     fpJb.addItemListener(new ItemListener() {                                                  
237       public void itemStateChanged( final ItemEvent e ) {                                      
238         fixPortsForLayout = ItemEvent.SELECTED == e.getStateChange();                          
239       }                                                                                        
240     });                                                                                        
241     fpJb.doClick();                                                                            
242     jtb.add(fpJb);                                                                             
243     final JToggleButton geJb = new JToggleButton("Edge Grouping");                             
244     geJb.setToolTipText("Toggle the 'Automatic Edge Grouping' feature of the layout algorithms"); 
245     geJb.addItemListener(new ItemListener() {                                                  
246       public void itemStateChanged( final ItemEvent e ) {                                      
247         groupEdges = ItemEvent.SELECTED == e.getStateChange();                                 
248       }                                                                                        
249     });                                                                                        
250     jtb.add(geJb);                                                                             
251     final JToggleButton spJb = new JToggleButton("Shared Ports");                              
252     spJb.setToolTipText("Toggle the 'Automatic Port Grouping' feature of the layout algorithms"); 
253     spJb.addItemListener(new ItemListener() {                                                  
254       public void itemStateChanged( final ItemEvent e ) {                                      
255         sharedPorts = ItemEvent.SELECTED == e.getStateChange();                                
256       }                                                                                        
257     });                                                                                        
258     jtb.add(spJb);                                                                             
259                                                                                                
260     final ButtonGroup buttonGroup = new ButtonGroup();                                         
261     buttonGroup.add(fpJb);                                                                     
262     buttonGroup.add(geJb);                                                                     
263     buttonGroup.add(spJb);                                                                     
264                                                                                                
265     return jtb;
266   }
267 
268   /**
269    * Overwritten to support (de-)serialization of {@link SidePortLocationModel}.
270    * @return a configured {@link GraphMLIOHandler} instance.
271    */
272   protected GraphMLIOHandler createGraphMLIOHandler() {
273     final SidePortLocationModel.Handler modelHandler = new SidePortLocationModel.Handler();
274     final GraphMLIOHandler graphMLIOHandler = super.createGraphMLIOHandler();
275     final Graph2DGraphMLHandler graphMLHandler = graphMLIOHandler.getGraphMLHandler();
276     graphMLHandler.addDeserializationHandler(modelHandler);
277     graphMLHandler.addSerializationHandler(modelHandler);
278     return graphMLIOHandler;
279   }
280 
281   /**
282    * Overwritten to prevent the {@link y.view.hierarchy.HierarchyManager}
283    * instance that is created in {@link #initialize()} from being replaced.
284    * @param rootGraph the graph for which a
285    * {@link y.view.hierarchy.HierarchyManager} has to be created.
286    * @return the {@link y.view.hierarchy.HierarchyManager} for the specified
287    * graph.
288    */
289   protected HierarchyManager createHierarchyManager( final Graph2D rootGraph ) {
290     return rootGraph.getHierarchyManager();
291   }
292 
293   protected void loadInitialGraph() {
294     // ensure that port configurations are already registered
295     PortConfigurations.INSTANCE.getClass();
296 
297     loadGraph("resource/NodePortsDemo.graphml");
298   }
299 
300   /**
301    * Overwritten to change the default group and folder node representations.
302    */
303   protected void configureDefaultGroupNodeRealizers() {
304     //Create additional configuration for default group node realizers
305     Map map = GenericGroupNodeRealizer.createDefaultConfigurationMap();
306 
307     GroupNodePainter gnp = new GroupNodePainter(new GroupShapeNodePainter());
308     map.put(GenericNodeRealizer.Painter.class, gnp);
309     map.put(GenericNodeRealizer.ContainsTest.class, gnp);
310     map.put(GenericNodeRealizer.GenericMouseInputEditorProvider.class, gnp);
311     map.put(GenericNodeRealizer.Initializer.class, gnp);
312 
313     DefaultGenericAutoBoundsFeature abf = new DefaultGenericAutoBoundsFeature();
314     abf.setConsiderNodeLabelSize(true);
315     map.put(GenericGroupNodeRealizer.GenericAutoBoundsFeature.class, abf);
316     map.put(GenericNodeRealizer.GenericSizeConstraintProvider.class, abf);
317     map.put(GenericNodeRealizer.LabelBoundsChangedHandler.class, abf);
318 
319     GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
320     factory.addConfiguration(CONFIGURATION_GROUP, map);
321 
322 
323     GenericGroupNodeRealizer gnr = new GenericGroupNodeRealizer();
324 
325     //Register first, since this will also configure the node label
326     gnr.setConfiguration(CONFIGURATION_GROUP);
327 
328     //Nicer colors
329     gnr.setFillColor(new Color(202,236,255,132));
330     gnr.setLineColor(new Color(102, 102,153,255));
331     NodeLabel label = gnr.getLabel();
332     label.setBackgroundColor(null);
333     label.setTextColor(Color.BLACK);
334     label.setFontSize(15);
335 
336 
337     //Set default group and folder node realizers
338     DefaultHierarchyGraphFactory hgf = (DefaultHierarchyGraphFactory)
339             getHierarchyManager().getGraphFactory();
340 
341     hgf.setProxyNodeRealizerEnabled(true);
342 
343     hgf.setDefaultGroupNodeRealizer(gnr.createCopy());
344     hgf.setDefaultFolderNodeRealizer(gnr.createCopy());
345   }
346 
347 
348 
349   private void layoutHierarchically() {                                         
350     final SimplexNodePlacer placer = new SimplexNodePlacer();                   
351     placer.setBaryCenterModeEnabled(true);                                      
352                                                                                 
353     final IncrementalHierarchicLayouter layouter =                              
354             new IncrementalHierarchicLayouter();                                
355     layouter.setLayoutOrientation(LayoutOrientation.LEFT_TO_RIGHT);             
356     layouter.setNodePlacer(placer);                                             
357                                                                                 
358     // ensures that normal edges, self loops, back loops, same layer edges,     
359     // and edge groups are all routed using the same routing style              
360     final RoutingStyle ers = new RoutingStyle(RoutingStyle.EDGE_STYLE_POLYLINE);
361     layouter.getEdgeLayoutDescriptor().setRoutingStyle(ers);                    
362                                                                                 
363     // a minimum distance of 15 will prevent edges from being routed through    
364     // node ports of adjacent nodes because non-free node ports protrude their  
365     // nodes by 10 size units                                                   
366     layouter.getEdgeLayoutDescriptor().setMinimumDistance(15);                  
367                                                                                 
368     layouter.getEdgeLayoutDescriptor().setMinimumFirstSegmentLength(20);        
369     layouter.getEdgeLayoutDescriptor().setMinimumLastSegmentLength(40);         
370     layouter.setMinimumLayerDistance(60);                                       
371                                                                                 
372     doLayout(layouter, false);                                                  
373   }                                                                             
374                                                                                 
375   private void routeOrthogonally() {                                            
376     doLayout(new EdgeRouter(), true);                                           
377   }                                                                             
378                                                                                 
379   private void doLayout(                                                        
380           final Layouter layouter, final boolean edgeRouting                    
381   ) {                                                                           
382     final Graph2DLayoutExecutor executor = new Graph2DLayoutExecutor();         
383                                                                                 
384     final NodePortLayoutConfigurator configurator =                             
385     executor.getNodePortConfigurator();                                         
386     configurator.setAutomaticPortConstraintsEnabled(fixPortsForLayout);         
387     if (edgeRouting) {                                                          
388       // y.layout.router.polyline.EdgeRouter does not yet support port groups.  
389       // Thus automatic edge grouping is used for shared ports as well.         
390       final boolean grouped = groupEdges || sharedPorts;                        
391       configurator.setAutomaticEdgeGroupsEnabled(grouped);                      
392     } else {                                                                    
393       configurator.setAutomaticEdgeGroupsEnabled(groupEdges);                   
394       configurator.setAutomaticPortGroupsEnabled(sharedPorts);                  
395     }                                                                           
396     configurator.setStrongGroupConstraintsEnabled(edgeRouting);                 
397                                                                                 
398     final NodeMap haloMap = createNodeHaloMap();                                
399     view.getGraph2D().addDataProvider(NodeHalo.NODE_HALO_DPKEY, haloMap);       
400     executor.doLayout(view, layouter);                                          
401     view.getGraph2D().removeDataProvider(NodeHalo.NODE_HALO_DPKEY);             
402   }                                                                             
403 
404   /**                                                                              
405    * Creates a node map with a {@link NodeHalo} for each node with ports.          
406    * A <code>NodeHalo</code> defines a rectangular area around a node.             
407    * Layout algorithms avoid placing other nodes, edges, or labels in this area.   
408    * this method creates node halos that reserve enough space for the visual       
409    * representation of the node ports thereby preventing layout and/or edge        
410    * routing algorithms from generating node port overlaps.                        
411    */                                                                              
412   private NodeMap createNodeHaloMap() {                                            
413     final NodeMap haloMap = Maps.createHashedNodeMap();                            
414     final Graph2D graph = view.getGraph2D();                                       
415                                                                                    
416     // The node ports in this demo extend at most 10 space units beyond the        
417     // node border. Therefore it is possible to use a shared halo instance         
418     // for all nodes.                                                              
419     // In a real world application, it usually will be necessary to create         
420     // distinct halo instances for each node depending on the geometry of the      
421     // node ports of said node.                                                    
422     final NodeHalo sharedHalo = NodeHalo.create(10, 10, 10, 10);                   
423                                                                                    
424     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {                      
425       if (graph.getRealizer(nc.node()).portCount() > 0) {                          
426         haloMap.set(nc.node(), sharedHalo);                                        
427       }                                                                            
428     }                                                                              
429     return haloMap;                                                                
430   }                                                                                
431 
432 
433   public static void main( String[] args ) {
434     EventQueue.invokeLater(new Runnable() {
435       public void run() {
436         Locale.setDefault(Locale.ENGLISH);
437         initLnF();
438         (new NodePortsDemo("resource/nodeportshelp.html")).start();
439       }
440     });
441   }
442 
443 
444   /**
445    * Specifies the contract of a post-processor for
446    * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter}.
447    */
448   private static interface Processor {
449     /**
450      * Post-processing for
451      * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#preEdgeStateChange(y.base.Edge, y.base.Node)}.
452      * @param edge the edge whose state is about to change.
453      * @param groupNode the node whose state change will trigger the edge state
454      * change.
455      */
456     public void preEdgeStateChange( Edge edge, Node groupNode );
457 
458     /**
459      * Post-processing for
460      * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#postEdgeStateChange(y.base.Edge, y.base.Node)}.
461      * @param edge the edge whose state has changed.
462      * @param groupNode the node whose state change has triggered the edge state
463      * change.
464      */
465     public void postEdgeStateChange( Edge edge, Node groupNode );
466 
467     /**
468      * Post-processing for
469      * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#postNodeStateChange(y.base.Node)}.
470      * @param groupNode the node whose state has changed.
471      */
472     public void postNodeStateChange( Node groupNode );
473   }
474 
475   /**
476    * Simple {@link y.view.NodeStateChangeEdgeRouter} implementation that allows
477    * for arbitrary post-processing in
478    * {@link #preEdgeStateChange(y.base.Edge, y.base.Node)},
479    * {@link #postEdgeStateChange(y.base.Edge, y.base.Node)}, and
480    * {@link #postNodeStateChange(y.base.Node)}.
481    */
482   private static final class MyNodeStateChangeEdgeRouter
483           extends NodeStateChangeEdgeRouter {
484     private final Processor impl;
485 
486     MyNodeStateChangeEdgeRouter( final Processor impl ) {
487       this.impl = impl;
488     }
489 
490     /**
491      * Overwritten to allow for post-processing.
492      * @param edge the edge whose state is about to change.
493      * @param groupNode the node whose state change will trigger the edge state
494      * change.
495      */
496     protected void preEdgeStateChange( final Edge edge, final Node groupNode ) {
497       if (impl != null) {
498         impl.preEdgeStateChange(edge, groupNode);
499       }
500 
501       super.preEdgeStateChange(edge, groupNode);
502     }
503 
504     /**
505      * Overwritten to allow for post-processing.
506      * @param edge the edge whose state has changed.
507      * @param groupNode the node whose state change has triggered the edge state
508      * change.
509      */
510     protected void postEdgeStateChange( final Edge edge, final Node groupNode ) {
511       super.postEdgeStateChange(edge, groupNode);
512 
513       if (impl != null) {
514         impl.postEdgeStateChange(edge, groupNode);
515       }
516     }
517 
518     /**
519      * Overwritten to allow for post-processing.
520      * @param groupNode the node whose state has changed.
521      */
522     public void postNodeStateChange( final Node groupNode ) {
523       super.postNodeStateChange(groupNode);
524 
525       if (impl != null) {
526         impl.postNodeStateChange(groupNode);
527       }
528     }
529   }
530 
531   /**
532    * Reassigns node ports of normal edges from one realizer delegate to the
533    * other at nodes that use {@link y.view.ProxyShapeNodeRealizer} upon node
534    * state changes.
535    * By default, edges connecting to the <code>i</code>-th port of one realizer
536    * delegate are assigned to the <code>i</code>-th port of the other realizer
537    * delegate (if it exists).
538    * More sophisticated strategies can be realized by customizing method
539    * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, y.view.NodePort, boolean)}.
540    */
541   private static final class NormalEdgeProcessor implements Processor {
542     private final Map node2edgeState;
543 
544     NormalEdgeProcessor() {
545       node2edgeState = new WeakHashMap();
546     }
547 
548 
549     /**
550      * Stores the current port information of the specified edge.
551      * @param edge the edge whose state is about to change.
552      * @param groupNode the node whose state change will trigger the edge state
553      */
554     public void preEdgeStateChange( final Edge edge, final Node groupNode ) {
555       if (groupNode.getGraph() instanceof Graph2D) {
556         final Graph2D graph = (Graph2D) groupNode.getGraph();
557         final HierarchyManager hm = graph.getHierarchyManager();
558         if (acceptEdge(hm, edge, groupNode)) {
559           Map edge2state = (Map) node2edgeState.get(groupNode);
560           if (edge2state == null) {
561             edge2state = new WeakHashMap();
562             node2edgeState.put(groupNode, edge2state);
563           }
564 
565           if (edge.source() == groupNode) {
566             final NodePort port = NodePort.getSourcePort(graph.getRealizer(edge));
567             if (port != null &&
568                 matches(port.getRealizer(), graph.getRealizer(groupNode))) {
569               EdgeState state = (EdgeState) edge2state.get(edge);
570               if (state == null) {
571                 state = new EdgeState();
572                 edge2state.put(edge, state);
573               }
574               state.sourcePort = port;
575             }
576           }
577           if (edge.target() == groupNode) {
578             final NodePort port = NodePort.getTargetPort(graph.getRealizer(edge));
579             if (port != null &&
580                 matches(port.getRealizer(), graph.getRealizer(groupNode))) {
581               EdgeState state = (EdgeState) edge2state.get(edge);
582               if (state == null) {
583                 state = new EdgeState();
584                 edge2state.put(edge, state);
585               }
586               state.targetPort = port;
587             }
588           }
589         }
590       }
591     }
592 
593     /**
594      * Assigns an appropriate port to the specified edge.
595      * Calls
596      * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, y.view.NodePort, boolean)}
597      * to determine which port is appropriate.
598      * @param edge the edge whose state has changed.
599      * @param groupNode the node whose state change has triggered the edge state
600      */
601     public void postEdgeStateChange( final Edge edge, final Node groupNode ) {
602       if (groupNode.getGraph() instanceof Graph2D) {
603         final Graph2D graph = (Graph2D) groupNode.getGraph();
604         final HierarchyManager hm = graph.getHierarchyManager();
605         if (acceptEdge(hm, edge, groupNode)) {
606           final Map edge2state = (Map) node2edgeState.get(groupNode);
607           final EdgeState state = (EdgeState) edge2state.get(edge);
608           if (state != null) {
609             if (edge.source() == groupNode) {
610               remapPort(
611                       graph.getRealizer(groupNode), graph.getRealizer(edge),
612                       state.sourcePort,
613                       true);
614             }
615             if (edge.target() == groupNode) {
616               remapPort(
617                       graph.getRealizer(groupNode), graph.getRealizer(edge),
618                       state.targetPort,
619                       false);
620             }
621           }
622         }
623       }
624     }
625 
626     /**
627      * Removes the no longer needed stored port information for all edges
628      * related to the specified node.
629      * @param groupNode the node whose state has changed.
630      */
631     public void postNodeStateChange( final Node groupNode ) {
632       node2edgeState.remove(groupNode);
633     }
634 
635 
636     /**
637      * Remaps the specified edge realizer's port to one that belongs to the
638      * specified node realizer.
639      * @param nr the realizer that provides possible new ports.
640      * @param er the realizer whose port has to be remapped.
641      * @param port the old port.
642      * @param source if <code>true</code> the source port has to be remapped;
643      * otherwise the target port has to be remapped.
644      */
645     void remapPort(
646             final NodeRealizer nr,
647             final EdgeRealizer er,
648             final NodePort port,
649             final boolean source
650     ) {
651       if (port != null) {
652         final NodeRealizer oldNr = port.getRealizer();
653         if (!matches(oldNr, nr) &&
654             oldNr != null &&
655             oldNr.portCount() == nr.portCount()) {
656           bindPort(nr.getPort(indexOf(port)), er, source);
657         }
658       }
659     }
660 
661 
662     /**
663      * Binds the specified port to the specified edge.
664      * @param port the port to bind to the specified edge.
665      * @param er the realizer representing the edge.
666      * @param source if <code>true</code> the source port has to be bound;
667      * otherwise the target port has to be bound.
668      */
669     private static void bindPort(
670             final NodePort port,
671             final EdgeRealizer er,
672             final boolean source
673     ) {
674       if (source) {
675         NodePort.bindSourcePort(port, er);
676       } else {
677         NodePort.bindTargetPort(port, er);
678       }
679     }
680 
681     /**
682      * Returns <code>true</code> if the specified edge's port assignment has to
683      * be remapped and <code>false</code> otherwise.
684      * @param hm the nesting structure of the specified edge's graph.
685      * @param edge the edge to check.
686      * @param groupNode the node whose state changes.
687      * @return <code>true</code> if the specified edge's port assignment has to
688      * be remapped and <code>false</code> otherwise.
689      */
690     private static boolean acceptEdge(
691             final HierarchyManager hm,
692             final Edge edge,
693             final Node groupNode
694     ) {
695       if (hm != null && hm.isFolderNode(groupNode) && hm.isInterEdge(edge)) {
696         return hm.getRealSource(edge) == groupNode ||
697                hm.getRealTarget(edge) == groupNode;
698       } else {
699         return edge.source() == groupNode || edge.target() == groupNode;
700       }
701     }
702 
703     /**
704      * Returns the zero-based index of the specified port within its associated
705      * node realizer's collection of ports.
706      * @param port the port whose index is to be determined.
707      * @return the zero-based  index of the specified port within its associated
708      * node realizer's collection of ports or <code>-1>/code> if the port
709      * currently is not associated to any node realizer.
710      */
711     private static int indexOf( final NodePort port ) {
712       final NodeRealizer owner = port.getRealizer();
713       if (owner != null) {
714         for (int i = 0, n = owner.portCount(); i < n; ++i) {
715           if (owner.getPort(i) == port) {
716             return i;
717           }
718         }
719       }
720       return -1;
721     }
722 
723     /**
724      * Returns <code>true</code> if the first realizer equals the second or the
725      * second realizer's delegate and <code>false</code> otherwise.
726      * @param portNr a realizer associated to a {@link y.view.NodePort}.
727      * @param otherNr another realizer, possibly a
728      * {@link y.view.ProxyShapeNodeRealizer}.
729      * @return <code>true</code> if the first realizer equals the second or the
730      * second realizer's delegate and <code>false</code> otherwise.
731      */
732     private static boolean matches(
733             final NodeRealizer portNr,
734             final NodeRealizer otherNr
735     ) {
736       return portNr == otherNr ||
737              (otherNr instanceof ProxyShapeNodeRealizer &&
738               portNr == ((ProxyShapeNodeRealizer) otherNr).getRealizerDelegate());
739     }
740 
741 
742     /**
743      * Stores port information for an edge.
744      */
745     private static final class EdgeState {
746       NodePort sourcePort;
747       NodePort targetPort;
748     }
749   }
750 
751   /**
752    * Reassigns node ports of edges that are converted to inter edges.
753    * By default, edges are automatically assigned to the first port of the
754    * corresponding folder node (if the node has any ports at all).
755    * More sophisticated strategies can be realized by customizing method
756    * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, boolean)}.
757    */
758   private static final class InterEdgeProcessor implements Processor {
759     private final Processor impl;
760 
761     InterEdgeProcessor( final Processor impl ) {
762       this.impl = impl;
763     }
764 
765     public void preEdgeStateChange( final Edge edge, final Node groupNode ) {
766       if (impl != null) {
767         impl.preEdgeStateChange(edge, groupNode);
768       }
769     }
770 
771     /**
772      * Assigns an appropriate port to the specified edge.
773      * Calls
774      * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, boolean)}
775      * to determine which port is appropriate.
776      * @param edge the edge whose state has changed.
777      * @param groupNode the node whose state change has triggered the edge state
778      */
779     public void postEdgeStateChange( final Edge edge, final Node groupNode ) {
780       if (impl != null) {
781         impl.postEdgeStateChange(edge, groupNode);
782       }
783 
784       if (groupNode.getGraph() instanceof Graph2D) {
785         final Graph2D graph = (Graph2D) groupNode.getGraph();
786         final HierarchyManager hm = graph.getHierarchyManager();
787         if (hm != null && hm.isFolderNode(groupNode) && hm.isInterEdge(edge)) {
788           if (edge.source() == groupNode && hm.getRealSource(edge) != groupNode) {
789             remapPort(
790                     graph.getRealizer(groupNode),
791                     graph.getRealizer(edge),
792                     true);
793           }
794           if (edge.target() == groupNode && hm.getRealTarget(edge) != groupNode) {
795             remapPort(
796                     graph.getRealizer(groupNode),
797                     graph.getRealizer(edge),
798                     false);
799           }
800         }
801       }
802     }
803 
804     public void postNodeStateChange( final Node groupNode ) {
805       if (impl != null) {
806         impl.postNodeStateChange(groupNode);
807       }
808     }
809 
810     /**
811      * Remaps the specified edge realizer's port to the first port of the
812      * specified node realizer.
813      * @param nr the realizer that provides possible new ports.
814      * @param er the realizer whose port has to be remapped.
815      * @param source if <code>true</code> the source port has to be remapped;
816      * otherwise the target port has to be remapped.
817      */
818     protected void remapPort(
819             final NodeRealizer nr,
820             final EdgeRealizer er,
821             final boolean source
822     ) {
823       if (nr.portCount() > 0) {
824         bindPort(nr.getPort(0), er, source);
825       }
826     }
827 
828 
829     /**
830      * Binds the specified port to the specified edge.
831      * @param port the port to bind to the specified edge.
832      * @param er the realizer representing the edge.
833      * @param source if <code>true</code> the source port has to be bound;
834      * otherwise the target port has to be bound.
835      */
836     private static void bindPort(
837             final NodePort port,
838             final EdgeRealizer er,
839             final boolean source
840     ) {
841       if (source) {
842         NodePort.bindSourcePort(port, er);
843       } else {
844         NodePort.bindTargetPort(port, er);
845       }
846     }
847   }
848 
849   /**
850    * Painter implementation for group nodes that draws a special header
851    * compartment below the groups default label.
852    * In the default group node configuration, this compartment is drawn by
853    * the default label. This means said compartment will be painted over node
854    * ports because node labels are rendered after node ports.
855    */
856   private static final class GroupShapeNodePainter extends ShapeNodePainter {
857     private static final Color BACKGROUND = new Color(153, 204, 255, 255);
858 
859     GroupShapeNodePainter() {
860       super(ROUND_RECT);
861     }
862 
863     protected void paintFilledShape(
864             final NodeRealizer context,
865             final Graphics2D graphics,
866             final Shape shape
867     ) {
868       super.paintFilledShape(context, graphics, shape);
869 
870       if (context.labelCount() > 0) {
871         final Shape oldClip = graphics.getClip();
872         final Color oldColor = graphics.getColor();
873 
874         final Rectangle2D cb = oldClip.getBounds2D();
875         final YRectangle r = context.getLabel().getBox();
876         graphics.clip(new Rectangle2D.Double(cb.getX(), r.getY(), cb.getWidth(), r.getHeight()));
877         graphics.setColor(BACKGROUND);
878         graphics.fill(shape);
879 
880         graphics.setColor(oldColor);
881         graphics.setClip(oldClip);
882       }
883     }
884   }
885 }
886