1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.layout.router;
15  
16  import demo.view.DemoBase;
17  import y.base.Edge;
18  import y.base.EdgeCursor;
19  import y.base.GraphEvent;
20  import y.base.GraphListener;
21  import y.base.Node;
22  import y.base.NodeList;
23  import y.geom.YPoint;
24  import y.util.DataProviderAdapter;
25  import y.util.DataProviders;
26  import y.view.Arrow;
27  import y.view.BridgeCalculator;
28  import y.view.DefaultGraph2DRenderer;
29  import y.view.EdgeRealizer;
30  import y.view.EditMode;
31  import y.view.GenericNodeRealizer;
32  import y.view.Graph2D;
33  import y.view.Graph2DUndoManager;
34  import y.view.Graph2DView;
35  import y.view.Graph2DViewActions;
36  import y.view.HotSpotMode;
37  import y.view.MovePortMode;
38  import y.view.MoveSelectionMode;
39  import y.view.NodePort;
40  import y.view.NodeRealizer;
41  import y.view.SelectionBoxMode;
42  import y.view.ShapeNodePainter;
43  import y.view.ViewMode;
44  
45  import javax.swing.AbstractAction;
46  import javax.swing.Action;
47  import javax.swing.ActionMap;
48  import javax.swing.JComponent;
49  import javax.swing.JMenu;
50  import javax.swing.JMenuBar;
51  import javax.swing.JPanel;
52  import javax.swing.JRootPane;
53  import javax.swing.JSplitPane;
54  import javax.swing.JToolBar;
55  import java.awt.BorderLayout;
56  import java.awt.Color;
57  import java.awt.EventQueue;
58  import java.awt.event.ActionEvent;
59  import java.net.URL;
60  import java.util.Locale;
61  import java.util.Map;
62  
63  /**
64   * Shows the capabilities of the yFiles {@link y.layout.router.BusRouter} and demonstrates specific <em>hub</em> nodes
65   * to ease the usage in an interactive environment.
66   * <p/>
67   * Typically, in a bus, every member node is connected to every other member which results in a large number of edges in
68   * the graph. To disburden users from entering all these edges manually, this application introduces a specific type of
69   * nodes, so-called <em>hubs</em>, which act as interchange points of the bus. A bus consists of all its interconnected
70   * hubs, and all edges and regular nodes connected to them. For convenience, all connectors and edges of the same bus
71   * are drawn in a common color.
72   * <p/>
73   * Regular nodes, hubs and edges can be interactively added and deleted, and snap lines are provided to ease the
74   * editing, see the related help page.
75   * <p/>
76   * There are a number of classes related for this demo:
77   * <ul>
78   *   <li>{@link BusDyer} governs the coloring of the buses</li>
79   *   <li>{@link BusRouterDemoModule} is used as a means to configure the router. If grid is enabled, the router
80   *   calculates grid routes and the view highlights the grid points.</li>
81   *   <li>{@link BusRouterDemoTools} governs the left-side option panel</li>
82   *   <li>{@link HubRoutingSupport} extends the BusRouter for graphs in hub representation</li>
83   * </ul>
84   *
85   * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/orthogonal_bus_router.html">Section Orthogonal Bus-style Edge Routing</a> in the yFiles for Java Developer's Guide
86   */
87  public class BusRouterDemo extends DemoBase {
88    static final String HUB_CONFIGURATION = "BusHub";
89    static final Object HUB_MARKER_DPKEY = "demo.layout.router.BusRouterDemo.HUB_MARKER_DPKEY";
90  
91    static final int MODE_ALL = 0;
92    static final int MODE_SELECTED = 1;
93    static final int MODE_PARTIAL = 2;
94  
95    private final BusDyer busDyer;
96    private JComponent glassPane;
97    private NodeRealizer hubRealizer;
98    private HubRoutingSupport hubRoutingSupport;
99    private Graph2DUndoManager undoManager;
100 
101   /**
102    * Creates a new instance of this demo.
103    */
104   public BusRouterDemo() {
105     this(null);
106   }
107 
108   /**
109    * Creates a new instance of this demo and adds a help pane for the specified file.
110    */
111   public BusRouterDemo(final String helpFilePath) {
112     // create support for colored buses
113     busDyer = createBusDyer();
114     view.getGraph2D().addGraphListener(busDyer);
115 
116     hubRoutingSupport = createHubRoutingSupport();
117 
118     // add and prepare the tool pane
119     BusRouterDemoTools demoTools = new BusRouterDemoTools();
120     demoTools.setViewAndRouter(view, hubRoutingSupport.getModule().getBusRouter());
121     demoTools.updateGrid();
122     demoTools.updateSnapping();
123 
124     JSplitPane mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, demoTools.createOptionComponent(), view);
125     mainSplitPane.setBorder(null);
126     contentPane.add(mainSplitPane, BorderLayout.CENTER);
127 
128     addHelpPane(helpFilePath);
129 
130     EventQueue.invokeLater(new Runnable() {
131       public void run() {
132         loadInitialGraph();
133       }
134     });
135   }
136 
137   protected void loadInitialGraph() {
138     loadGraph("resource/threeBuses.graphml");
139   }
140 
141   /**
142    * Does the bus routing by delegating to {@link HubRoutingSupport#doLayout(y.view.Graph2D, int)}.
143    */
144   public void doLayout(final int mode) {
145     final Graph2D graph = view.getGraph2D();
146     final NodeRealizer oldNodeRealizer = graph.getDefaultNodeRealizer();
147 
148     if (glassPane == null) {
149       // creates a glass pane to lock the GUI during layout
150       glassPane = new JPanel();
151       final JRootPane rootPane = view.getRootPane();
152       rootPane.setGlassPane(glassPane);
153     }
154     glassPane.setEnabled(true);
155 
156     try {
157       // backup the graph state for undo
158       graph.backupRealizers();
159 
160       // disable bridges during the layout animation and set the hub realizer for the new hub node
161       setBridgeCalculatorEnabled(false);
162       graph.setDefaultNodeRealizer(getHubRealizer());
163 
164       hubRoutingSupport.doLayout(graph, mode);
165       busDyer.colorize(null);
166     } finally {
167       graph.setDefaultNodeRealizer(oldNodeRealizer);
168       setBridgeCalculatorEnabled(true);
169 
170       glassPane.setEnabled(false);
171       graph.updateViews();
172     }
173   }
174 
175   /**
176    * Initialize this demo.
177    */
178   protected void initialize() {
179     super.initialize();
180     hubRealizer = createHubRealizer();
181 
182     setBridgeCalculatorEnabled(true);
183 
184     // create a data provider that specifies whether a node is a hub
185     view.getGraph2D().addDataProvider(HUB_MARKER_DPKEY, new DataProviderAdapter() {
186       public boolean getBool(Object dataHolder) {
187         return dataHolder instanceof Node && isHub((Node) dataHolder);
188       }
189     });
190 
191     // create the data provider needed for the Orthogonal Mode
192     view.getGraph2D().addDataProvider(EditMode.ORTHOGONAL_ROUTING_DPKEY,
193         DataProviders.createConstantDataProvider(Boolean.TRUE));
194 
195     undoManager = new Graph2DUndoManager(view.getGraph2D());
196     undoManager.setViewContainer(view);
197   }
198 
199   /**
200    * Registers the default actions, and replaces the delete and select all actions with this class's custom
201    * implementations.
202    */
203   protected void registerViewActions() {
204     super.registerViewActions();
205     // register keyboard actions
206     ActionMap amap = view.getCanvasComponent().getActionMap();
207     if (amap != null) {
208       if (isDeletionEnabled()) {
209         amap.put(Graph2DViewActions.DELETE_SELECTION, createDeleteSelectionAction());
210       }
211 
212       // Prevents the selection of hubs by the select all action
213       amap.put(Graph2DViewActions.SELECT_ALL, new Graph2DViewActions.SelectAllAction(view) {
214         protected void setSelected(Graph2D graph, Node node, boolean flag) {
215           if (!isHub(node)) {
216             super.setSelected(graph, node, flag);
217           }
218         }
219       });
220     }
221   }
222 
223   /**
224    * Creates a delete action which deletes selected elements and remaining stub bus parts.
225    */
226   protected Action createDeleteSelectionAction() {
227     final Action oldAction = super.createDeleteSelectionAction();
228     final Action newAction = new HubDeleteSelectionAction();
229     newAction.putValue(Action.SHORT_DESCRIPTION, oldAction.getValue(Action.SHORT_DESCRIPTION));
230     newAction.putValue(Action.SMALL_ICON, oldAction.getValue(Action.SMALL_ICON));
231     return newAction;
232   }
233 
234   /**
235    * Creates the default toolbar and adds undo/redo and the routing actions.
236    */
237   protected JToolBar createToolBar() {
238     JToolBar toolBar = super.createToolBar();
239     toolBar.setFloatable(false);
240 
241     toolBar.addSeparator();
242 
243     //add undo action to toolbar
244     Action undoAction = undoManager.getUndoAction();
245     undoAction.putValue(Action.SMALL_ICON, getIconResource("resource/undo.png"));
246     undoAction.putValue(Action.SHORT_DESCRIPTION, "Undo");
247     toolBar.add(undoAction);
248 
249     //add redo action to toolbar
250     Action redoAction = undoManager.getRedoAction();
251     redoAction.putValue(Action.SMALL_ICON, getIconResource("resource/redo.png"));
252     redoAction.putValue(Action.SHORT_DESCRIPTION, "Redo");
253     toolBar.add(redoAction);
254 
255     toolBar.addSeparator();
256 
257     // add bus router to toolbar
258     Action routeAllAction = new AbstractAction("Route All") {
259       public void actionPerformed(ActionEvent e) {
260         doLayout(MODE_ALL);
261       }
262     };
263     routeAllAction.putValue(Action.SHORT_DESCRIPTION, "Route all buses");
264     routeAllAction.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);
265     toolBar.add(createActionControl(routeAllAction));
266 
267     // add bus router to toolbar
268     Action routeSelectedAction = new AbstractAction("Route Selected") {
269       public void actionPerformed(ActionEvent e) {
270         doLayout(MODE_SELECTED);
271       }
272     };
273     routeSelectedAction.putValue(Action.SHORT_DESCRIPTION, "Route selected buses");
274     routeSelectedAction.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);
275     toolBar.add(createActionControl(routeSelectedAction, true));
276 
277     // add settings for bus router to toolbar
278     Action propertiesAction = new AbstractAction("Settings...") {
279       public void actionPerformed(ActionEvent e) {
280         OptionSupport.showDialog(hubRoutingSupport.getModule(), view.getGraph2D(), false, view.getFrame());
281       }
282     };
283     propertiesAction.putValue(Action.SHORT_DESCRIPTION, "Configure the bus router");
284     propertiesAction.putValue(Action.SMALL_ICON, getIconResource("resource/properties.png"));
285     toolBar.add(createActionControl(propertiesAction));
286 
287     return toolBar;
288   }
289 
290   /**
291    * Creates the default menu bar and adds an additional menu of examples graphs.
292    */
293   protected JMenuBar createMenuBar() {
294     final JMenuBar menuBar = super.createMenuBar();
295     JMenu menu = new JMenu("Sample Graphs");
296     menuBar.add(menu);
297 
298     menu.add(new EmptyGraphAction("Empty Graph"));
299 
300     menu.add(new AbstractAction("One Bus") {
301       public void actionPerformed(ActionEvent e) {
302         loadGraph("resource/oneBus.graphml");
303       }
304     });
305 
306     menu.add(new AbstractAction("Three Buses") {
307       public void actionPerformed(ActionEvent e) {
308         loadGraph("resource/threeBuses.graphml");
309       }
310     });
311 
312     return menuBar;
313   }
314 
315   /**
316    * Creates a modified edit mode for this demo.
317    */
318   protected EditMode createEditMode() {
319     EditMode editMode = new HubEditMode();
320 
321     // copied from DemoBase
322     if (editMode.getMovePortMode() instanceof MovePortMode) {
323       ((MovePortMode) editMode.getMovePortMode()).setIndicatingTargetNode(true);
324     }
325 
326     //allow moving view port with right drag gesture
327     editMode.allowMovingWithPopup(true);
328     return editMode;
329   }
330 
331   /**
332    * Creates the BusDyer used by this demo.
333    */
334   protected BusDyer createBusDyer() {
335     return new BusDyer(view.getGraph2D());
336   }
337 
338   /**
339    * Creates the HubRoutingSupport used by this demo.
340    */
341   protected HubRoutingSupport createHubRoutingSupport() {
342     return new HubRoutingSupport();
343   }
344 
345   /**
346    * Creates the default realizers but with thick edges which do not display arrow heads.
347    */
348   protected void configureDefaultRealizers() {
349     super.configureDefaultRealizers();
350 
351     EdgeRealizer er = view.getGraph2D().getDefaultEdgeRealizer();
352     er.setTargetArrow(Arrow.NONE);
353     view.getGraph2D().setDefaultEdgeRealizer(er);
354   }
355 
356   /**
357    * Call-back for loading a graph. Overwritten to reset the undo queue.
358    */
359   protected void loadGraph(URL resource) {
360     super.loadGraph(resource);
361     undoManager.resetQueue();
362 
363     EventQueue.invokeLater(new Runnable() {
364       public void run() {
365         doLayout(MODE_ALL);
366       }
367     });
368   }
369 
370   /**
371    * Specifies whether bridges are shown and configures a {@link y.view.BridgeCalculator} accordingly.
372    */
373   private void setBridgeCalculatorEnabled(boolean enable) {
374     if (enable) {
375       // create the BridgeCalculator
376       BridgeCalculator bridgeCalculator = new BridgeCalculator();
377       ((DefaultGraph2DRenderer) view.getGraph2DRenderer()).setBridgeCalculator(bridgeCalculator);
378       bridgeCalculator.setCrossingMode(BridgeCalculator.CROSSING_MODE_ORDER_INDUCED);
379       bridgeCalculator.setCrossingStyle(BridgeCalculator.CROSSING_STYLE_ARC);
380       bridgeCalculator.setOrientationStyle(BridgeCalculator.ORIENTATION_STYLE_UP);
381     } else {
382       ((DefaultGraph2DRenderer) view.getGraph2DRenderer()).setBridgeCalculator(null);
383     }
384   }
385 
386   NodeRealizer getHubRealizer() {
387     return hubRealizer;
388   }
389 
390   /**
391    * Returns whether the specified node is a hub. A node is a hub if its realizer is the one of hubs.
392    */
393   static boolean isHub(final Node node) {
394     final NodeRealizer realizer = ((Graph2D) node.getGraph()).getRealizer(node);
395     return realizer instanceof GenericNodeRealizer
396         && HUB_CONFIGURATION.equals(((GenericNodeRealizer) realizer).getConfiguration());
397   }
398 
399   /**
400    * Creates the realizer which is used for hubs.
401    */
402   static NodeRealizer createHubRealizer() {
403     final GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
404     final Map map = GenericNodeRealizer.getFactory().createDefaultConfigurationMap();
405 
406     map.put(GenericNodeRealizer.Painter.class, new ShapeNodePainter(ShapeNodePainter.RECT));
407     factory.addConfiguration(HUB_CONFIGURATION, map);
408 
409     NodeRealizer nr = new GenericNodeRealizer(HUB_CONFIGURATION);
410     nr.setFillColor(Color.BLACK);
411     nr.setLineColor(null);
412     nr.setSize(5.0, 5.0);
413     nr.removeLabel(nr.getLabel(0));
414     return nr;
415   }
416 
417   /**
418    * A modified edit mode for this demo. After each node movement and node resizing, the connections of the affected
419    * nodes to their buses are rerouted while the other parts remain fixed. If the source of an edge creation is a hub,
420    * the edge is colored in the bus' color. If a new edge is a singleton bus, that is, it connects two regular nodes, it
421    * is instantly routed.
422    */
423   protected class HubEditMode extends EditMode {
424     protected HubEditMode() {
425       setCreateEdgeMode(new AutoRoutingCreateEdgeMode(getHubRealizer()));
426     }
427 
428     /**
429      * After each node movement, the connections of the affected nodes to their buses are rerouted while the other parts
430      * remain fixed.
431      */
432     protected ViewMode createMoveSelectionMode() {
433       return new MoveSelectionMode() {
434         protected void selectionMovedAction(double dx, double dy, double x, double y) {
435           super.selectionMovedAction(dx, dy, x, y);
436           doLayout(MODE_PARTIAL);
437         }
438       };
439     }
440 
441     protected ViewMode createCreateEdgeMode() {
442       return null;
443     }
444 
445     protected ViewMode createMovePortMode() {
446       return null;
447     }
448 
449     /**
450      * Does a partial layout after resizing of nodes.
451      */
452     protected ViewMode createHotSpotMode() {
453       return new HotSpotMode() {
454         private boolean dirty;
455 
456         public void mousePressedLeft(double x, double y) {
457           dirty = false;
458           super.mousePressedLeft(x, y);
459         }
460 
461         public void mouseReleasedLeft(double x, double y) {
462           super.mouseReleasedLeft(x, y);
463           if (dirty) {
464             doLayout(MODE_PARTIAL);
465           }
466           dirty = false;
467         }
468 
469         protected void updateNodeRealizerBounds(NodeRealizer vr, double x, double y, double w, double h) {
470           super.updateNodeRealizerBounds(vr, x, y, w, h);
471           dirty = true;
472         }
473       };
474     }
475 
476     /**
477      * Prevents the selection of hubs by selection boxes.
478      */
479     protected ViewMode createSelectionBoxMode() {
480       return new SelectionBoxMode() {
481         protected void setSelected(Graph2D graph, Node n, boolean state) {
482           if (!isHub(n)) {
483             super.setSelected(graph, n, state);
484           }
485         }
486       };
487     }
488 
489     /**
490      * Prevents the selection of hubs by left mouse clicks.
491      */
492     protected void setSelected(Graph2D graph, Node node, boolean state) {
493       if (!isHub(node)) {
494         super.setSelected(graph, node, state);
495       }
496     }
497 
498     /**
499      * Selects on a click on a hub its edges.
500      */
501     protected void nodeClicked(Graph2D graph, Node node, boolean wasSelected, double x, double y,
502                                boolean modifierSet) {
503       if (isHub(node)) {
504         if (!modifierSet) {
505           graph.unselectAll();
506         }
507 
508         if (!modifierSet || graph.isSelectionEmpty() || graph.selectedEdges().ok()) {
509           for (EdgeCursor edgeCursor = node.edges(); edgeCursor.ok(); edgeCursor.next()) {
510             final Edge edge = edgeCursor.edge();
511             setSelected(graph, edge, true);
512           }
513         }
514         graph.updateViews();
515       } else {
516         super.nodeClicked(graph, node, wasSelected, x, y, modifierSet);
517       }
518     }
519 
520     /**
521      * Enables edge creation for modifier left click on an edge.
522      */
523     public void mouseDraggedLeft(double x, double y) {
524       if (isModifierPressed(lastPressEvent)) {
525         double px = translateX(lastPressEvent.getX());
526         double py = translateY(lastPressEvent.getY());
527         Edge edge = getHitInfo(px, py).getHitEdge();
528         if (edge != null) {
529           setChild(getCreateEdgeMode(), lastPressEvent, lastDragEvent);
530           return;
531         }
532       }
533       super.mouseDraggedLeft(x, y);
534     }
535 
536   }
537 
538   /**
539    * Routes each new edge automatically. If it connects two previously independent buses, the whole resulting bus is
540    * routed anew. If it connects a new node to an existing bus, the existing bus is kept fixed and only the new edge is
541    * routed. If it establishes a bus of its own, that is, it connects two regular nodes which are not associated with a
542    * bus, it is routed as single-edge bus.
543    * <p/>
544    * Additionally, takes care to split a new edge which connects two regular nodes since this demo requires each bus
545    * edge to connect to at least on hub.
546    */
547   protected class AutoRoutingCreateEdgeMode extends HubCreateEdgeMode {
548 
549     protected AutoRoutingCreateEdgeMode(NodeRealizer hubRealizer) {
550       super(hubRealizer);
551     }
552 
553     protected void edgeCreated(final Edge edge) {
554       super.edgeCreated(edge);
555       //noinspection IfStatementWithIdenticalBranches
556       if (!isHub(edge.source()) && !isHub(edge.target())) {
557         // both end nodes are regular nodes -> this is a single-edge bus
558         final Edge edge2 = splitSingleBusEdge(edge);
559         EventQueue.invokeLater(new Runnable() {
560           public void run() {
561             getGraph2D().unselectAll();
562             getGraph2D().setSelected(edge, true);
563             getGraph2D().setSelected(edge2, true);
564             doLayout(MODE_SELECTED);
565           }
566         });
567       } else if (isHub(edge.source()) && isHub(edge.target())) {
568         // both end nodes are hubs -> route the complete bus since partial mode can be used for end-edges only
569         EventQueue.invokeLater(new Runnable() {
570           public void run() {
571             getGraph2D().unselectAll();
572             getGraph2D().setSelected(edge, true);
573             doLayout(MODE_SELECTED);
574           }
575         });
576       } else {
577         // exactly one end node is a hub -> route the new edge and keep the existing bus fixed
578         EventQueue.invokeLater(new Runnable() {
579           public void run() {
580             getGraph2D().unselectAll();
581             getGraph2D().setSelected(edge, true);
582             doLayout(MODE_PARTIAL);
583           }
584         });
585       }
586     }
587 
588     /**
589      * In this demo, every bus edge should connect to at least one hub. Therefore, we split an edge which connect two
590      * regular nodes into two parts by adding a new hub.
591      *
592      * @param edge the edge to split. This edge is changed to connect its source to the new hub.
593      * @return the new edge which connects the new hub two the original edge's target.
594      */
595     protected Edge splitSingleBusEdge(Edge edge) {
596       final Graph2D graph = getGraph2D();
597       final Node oldSource = edge.source();
598       final Node oldTarget = edge.target();
599 
600       graph.firePreEvent();
601       try {
602         // Note: the calculates a reasonable middle point only if the edge is straight-line;
603         //       otherwise we have to split its path and distribute the bends correctly
604 
605         final Node hub = graph.createNode(getHubRealizer().createCopy());
606         graph.setCenter(hub, YPoint.midPoint(graph.getSourcePointAbs(edge), graph.getTargetPointAbs(edge)));
607 
608         graph.changeEdge(edge, oldSource, hub);
609         graph.setTargetPointRel(edge, YPoint.ORIGIN);
610 
611         final EdgeRealizer edgeRealizer = graph.getRealizer(edge);
612         final EdgeRealizer edgeRealizer2 = edgeRealizer.createCopy();
613         final Edge edge2 = graph.createEdge(hub, oldTarget, edgeRealizer2);
614         graph.setSourcePointRel(edge2, YPoint.ORIGIN);
615 
616         final NodePort targetPort = NodePort.getTargetPort(edgeRealizer);
617         if (isNodePortAware() && targetPort != null) {
618           NodePort.bindTargetPort(targetPort, edgeRealizer2);
619         }
620 
621         return edge2;
622       } finally {
623         graph.firePostEvent();
624       }
625     }
626   }
627 
628   /**
629    * Deletes selected graph elements and, additionally, all remaining bus stubs. A bus stub is a part of a bus which is
630    * connected to only one regular node after the deletion of the selected elements.
631    */
632   protected class HubDeleteSelectionAction extends Graph2DViewActions.DeleteSelectionAction {
633 
634     protected HubDeleteSelectionAction() {
635       super(BusRouterDemo.this.view);
636     }
637 
638     public void delete(Graph2DView view) {
639       final Graph2D graph = view.getGraph2D();
640       try {
641         graph.firePreEvent();
642         deleteImpl(view);
643       } finally {
644         graph.firePostEvent();
645       }
646     }
647 
648     /**
649      * Does the deletion. First, deletes the selected elements. Then, deletes iteratively each neighbor of any deleted
650      * edge if it is a hub and has degree lesser than 2. These are exactly the hubs of bus stubs.
651      */
652     private void deleteImpl(Graph2DView view) {
653       final NodeList hubsToDelete = new NodeList();
654 
655       GraphListener listener = new GraphListener() {
656         public void onGraphEvent(GraphEvent e) {
657           if (e.getType() == GraphEvent.PRE_EDGE_REMOVAL) {
658             final Edge edge = (Edge) e.getData();
659             if (isHubToDelete(edge.source())) {
660               hubsToDelete.add(edge.source());
661             }
662             if (isHubToDelete(edge.target())) {
663               hubsToDelete.add(edge.target());
664             }
665           }
666         }
667 
668         private boolean isHubToDelete(Node node) {
669           // edge not deleted yet, therefore test for degree < 3
670           return isHub(node) && node.degree() < 3;
671         }
672       };
673 
674       final Graph2D graph = view.getGraph2D();
675       try {
676         graph.addGraphListener(listener);
677         super.delete(view);
678 
679         while (!hubsToDelete.isEmpty()) {
680           final Node node = hubsToDelete.popNode();
681           if (node.getGraph() != null) {
682             graph.removeNode(node);
683           }
684         }
685       } finally {
686         graph.removeGraphListener(listener);
687       }
688     }
689   }
690 
691   /**
692    * Clears the graph and its undo queue, and sets the view to the initial zoom factor.
693    */
694   protected class EmptyGraphAction extends AbstractAction {
695 
696     protected EmptyGraphAction(String name) {
697       super(name);
698     }
699 
700     public void actionPerformed(ActionEvent e) {
701       view.getGraph2D().clear();
702       view.getGraph2D().setURL(null);
703       view.fitContent();
704       view.updateView();
705       undoManager.resetQueue();
706     }
707   }
708 
709   /**
710    * Runs this demo.
711    *
712    * @param args unused
713    */
714   public static void main(String[] args) {
715     EventQueue.invokeLater(new Runnable() {
716       public void run() {
717         Locale.setDefault(Locale.ENGLISH);
718         initLnF();
719         new BusRouterDemo("resource/busrouterhelp.html").start("Bus Router Demo");
720       }
721     });
722   }
723 }
724