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.labeling;
29  
30  import demo.view.DemoBase;
31  import demo.view.DemoDefaults;
32  import y.base.DataMap;
33  import y.base.GraphEvent;
34  import y.base.GraphListener;
35  import y.base.Node;
36  import y.base.NodeCursor;
37  import y.layout.AbstractLayoutStage;
38  import y.layout.DiscreteNodeLabelModel;
39  import y.layout.LabelCandidate;
40  import y.layout.LayoutGraph;
41  import y.layout.Layouter;
42  import y.layout.NodeLabelModel;
43  import y.layout.NodeLayout;
44  import y.layout.ProfitModel;
45  import y.layout.labeling.AbstractLabelingAlgorithm;
46  import y.layout.labeling.GreedyMISLabeling;
47  import y.layout.labeling.MISLabelingAlgorithm;
48  import y.layout.NodeLabelLayout;
49  import y.layout.BufferedLayouter;
50  import y.option.EditorFactory;
51  import y.option.OptionGroup;
52  import y.option.OptionHandler;
53  import y.option.DefaultEditorFactory;
54  import y.option.Editor;
55  import y.option.ItemEditor;
56  import y.option.CompoundEditor;
57  import y.util.DataProviderAdapter;
58  import y.util.Maps;
59  import y.view.DefaultBackgroundRenderer;
60  import y.view.EditMode;
61  import y.view.Graph2D;
62  import y.view.Graph2DLayoutExecutor;
63  import y.view.NodeLabel;
64  import y.view.NodeRealizer;
65  import y.view.PopupMode;
66  import y.view.SmartNodeLabelModel;
67  import y.view.YLabel;
68  import y.view.DefaultLabelConfiguration;
69  import y.geom.YPoint;
70  import y.geom.YRectangle;
71  import y.geom.LineSegment;
72  
73  import javax.swing.AbstractAction;
74  import javax.swing.Action;
75  import javax.swing.JPopupMenu;
76  import javax.swing.JToolBar;
77  import javax.swing.JPanel;
78  import javax.swing.JComponent;
79  import java.awt.Color;
80  import java.awt.Dimension;
81  import java.awt.EventQueue;
82  import java.awt.Graphics2D;
83  import java.awt.BorderLayout;
84  import java.awt.geom.Line2D;
85  import java.awt.event.ActionEvent;
86  import java.net.URL;
87  import java.util.Arrays;
88  import java.util.HashMap;
89  import java.util.HashSet;
90  import java.util.List;
91  import java.util.Locale;
92  import java.util.Map;
93  import java.util.ArrayList;
94  import java.util.Iterator;
95  import java.beans.PropertyChangeEvent;
96  import java.beans.PropertyChangeListener;
97  import java.util.Set;
98  
99  /**
100  * This demo shows how to configure node labels and the corresponding label models as well as how to apply the
101  * generic node label placement algorithm.
102  * <p>
103  * A new city (node) can be added by left-clicking on the corresponding map location. To edit a node label right-click
104  * on the label or the corresponding node and choose item "Edit Label". Node labels can be moved to an arbitrary
105  * position using drag and drop.
106  * </p><p>
107  * To manually start the generic labeling algorithm click on the "Do Generic Labeling" button. Note: after changing one
108  * of the node label properties, the generic labeling algorithm is applied automatically.
109  * </p><p>
110  * <b>Third Party Licenses:</b><br/>
111  * The USA map that is used as background in this demo is based on
112  * <a href="http://commons.wikimedia.org/wiki/File:Blank_US_Map.svg">Blank_US_Map.svg by Theshibboleth</a>
113  * and licensed under the
114  * <a href="http://creativecommons.org/licenses/by-sa/3.0/">Creative Commons Attribution-ShareAlike 3.0 Unported</a>
115  * license.
116  * </p>
117  *
118  * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/labeling#labeling" target="_blank">Section Automatic Label Placement</a> in the yFiles for Java Developer's Guide
119  */
120 public class NodeLabelingDemo extends DemoBase {
121   private static final String LABELING_MODEL_STRING = "Labeling Model";
122   private static final String LABEL_SIZE_STRING = "Font Size";
123   private static final String PROPERTIES_GROUP = "Node Label Properties";
124 
125   //node label model constants
126   private static final String MODEL_CORNERS = "Corners";  
127   private static final String MODEL_SANDWICH = "Sandwich";
128   private static final String MODEL_SIDE = "Side";
129   private static final String MODEL_FREE = "Free";
130   private static final String MODEL_EIGHT_POS = "8 Pos";
131   private static final String[] NODE_LABEL_MODELS = {
132       MODEL_CORNERS, MODEL_SANDWICH, MODEL_SIDE, MODEL_FREE, MODEL_EIGHT_POS
133   };
134 
135   private static final int TOOLS_PANEL_WIDTH = 350;
136 
137   private Map label2Model; //stores (for each label) the label model used by the labeling algorithm
138   private MyGenericNodeLabelingAlgorithm labelLayouter;
139   private OptionHandler optionHandler;
140 
141   public NodeLabelingDemo() {
142     this(null);
143   }
144 
145   public NodeLabelingDemo(final String helpFilePath) {
146     // render a map of the USA in the background
147     DefaultBackgroundRenderer renderer = new DefaultBackgroundRenderer(view);
148 
149     // resource/usamap.png licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported license
150     URL bgImage = getSharedResource("resource/usamap.png");
151 
152     renderer.setImageResource(bgImage);
153     renderer.setMode(DefaultBackgroundRenderer.DYNAMIC);
154     renderer.setColor(Color.white);
155     view.setBackgroundRenderer(renderer);
156     view.setPreferredSize(new Dimension(650, 400));
157     view.setWorldRect(0, 0, 650, 400);
158 
159     contentPane.add(createToolsPanel(helpFilePath), BorderLayout.EAST);
160 
161     loadGraph("resource/uscities.graphml");
162 
163     //after creating a new node the label should be placed accordingly
164     view.getGraph2D().addGraphListener(new GraphListener() {
165       public void onGraphEvent(GraphEvent e) {
166         if (e.getType() == GraphEvent.NODE_CREATION) {
167           final Graph2D graph = view.getGraph2D();
168           final Node node = (Node) e.getData();
169           final NodeLabelLayout[] nll = graph.getNodeLabelLayout(node);
170 
171           //set the label model and size
172           final int labelSize = optionHandler.getInt(LABEL_SIZE_STRING);
173           final NodeLabelModel labelingModel = getModel(
174               optionHandler.getEnum(LABELING_MODEL_STRING)); //the model used by the labeling algorithm
175           for (int i = 0; i < nll.length; i++) {
176             label2Model.put(nll[i], labelingModel);
177             ((NodeLabel) nll[i]).setFontSize(labelSize);
178           }
179 
180           //mark the new labels and place them
181           final Set newLabels = new HashSet(Arrays.asList(nll));
182           graph.addDataProvider("SELECTED_LABELS", new DataProviderAdapter() {
183             public boolean getBool(Object dataHolder) {
184               return newLabels.contains(dataHolder);
185             }
186           });
187           final Object oldSelectionKey = labelLayouter.getSelectionKey();
188           labelLayouter.setSelection("SELECTED_LABELS");
189           new BufferedLayouter(labelLayouter).doLayout(graph);
190           labelLayouter.setSelection(oldSelectionKey);
191           graph.removeDataProvider("SELECTED_LABELS");
192         }
193       }
194     });
195 
196     // do initial label placement
197     doLabelPlacement();
198 
199     getUndoManager().resetQueue();
200   }
201 
202   protected void initialize() {
203     optionHandler = createOptionHandler();
204 
205     labelLayouter = new MyGenericNodeLabelingAlgorithm();
206 
207     //register the data-provider that stores for each node label the label model that is used by the labeling algorithm
208     label2Model = new HashMap();
209     view.getGraph2D().addDataProvider(AbstractLabelingAlgorithm.LABEL_MODEL_DPKEY, new DataProviderAdapter() {
210       public Object get(Object dataHolder) {
211         return label2Model.get(dataHolder);
212       }
213     });
214   }
215 
216   /**
217    * Customized labeling algorithm that prefers node labels that are placed at the top of the corresponding node.
218    * The algorithm therefore wraps the GreedyMISLabeling algorithm and adds a corresponding profit model to it.
219    */
220   static class MyGenericNodeLabelingAlgorithm extends AbstractLayoutStage {
221     private final GreedyMISLabeling coreLabeling;
222 
223     public MyGenericNodeLabelingAlgorithm() {
224       this(null);
225     }
226 
227     public MyGenericNodeLabelingAlgorithm(Layouter core) {
228       super(core);
229       this.coreLabeling = new GreedyMISLabeling();
230       coreLabeling.setOptimizationStrategy(MISLabelingAlgorithm.OPTIMIZATION_BALANCED);
231       coreLabeling.setPlaceEdgeLabels(false);
232       coreLabeling.setPlaceNodeLabels(true);
233       coreLabeling.setApplyPostprocessing(true);
234     }
235 
236     /**
237      * Sets the <code>DataProvider</code> key, under which the labeling selection can be retrieved. The data provider
238      * registered with this key has to return <code>true</code> for labels that should be placed and
239      * <code>false</code> for all other labels.
240      *
241      * @param key The key for a <code>DataProvider</code>.
242      */
243     public void setSelection(Object key) {
244       coreLabeling.setSelection(key);
245     }
246 
247     /**
248      * Returns the labeling selection <code>DataProvider</code> key.
249      */
250     public Object getSelectionKey() {
251       return coreLabeling.getSelectionKey();
252     }
253 
254     public boolean canLayout(LayoutGraph graph) {
255       return canLayoutCore(graph);
256     }
257 
258     public void doLayout(LayoutGraph graph) {
259       doLayoutCore(graph);
260 
261       //add profit model that prefers labels at the north of the corresponding node
262       coreLabeling.setProfitModel(new PreferNorthLabelsProfitModel(graph));
263 
264       //call core labeling algorithm
265       coreLabeling.doLayout(graph);
266     }
267 
268     /**
269      * Profit model that assigns larger profits to labels that are placed at the north (outside) of the corresponding
270      * node.
271      */
272     private static class PreferNorthLabelsProfitModel implements ProfitModel {
273       private static final double MAX_DISTANCE_WITH_PROFIT = 10;
274       private static final double MAX_HORIZONTAL_CENTER_DISTANCE_WITH_PROFIT = 10;
275       private DataMap label2NodeLayout;
276 
277       public PreferNorthLabelsProfitModel(final LayoutGraph graph) {
278         updateLabel2NodeLayoutMap(graph);
279       }
280 
281       /**
282        * Creates a mapping between the labels and the corresponding node layout. It is used to determine
283        * the profit of the labels.
284        */
285       private void updateLabel2NodeLayoutMap(final LayoutGraph graph) {
286         this.label2NodeLayout = Maps.createHashedDataMap();
287         for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
288           final NodeLayout nLayout = graph.getNodeLayout(nc.node());
289           final NodeLabelLayout[] nll = graph.getLabelLayout(nc.node());
290           for (int i = 0; i < nll.length; i++) {
291             label2NodeLayout.set(nll[i], nLayout);
292           }
293         }
294       }
295 
296       private static boolean isLabelAboveNode(final YRectangle labelBox, final NodeLayout nodeLayout) {
297         return nodeLayout.getY() > labelBox.getY() + labelBox.getHeight();
298       }
299 
300       private static double calcHorizontalCenterDistance(final YRectangle labelBox, final NodeLayout nodeLayout) {
301         return Math.abs((nodeLayout.getX() + nodeLayout.getWidth() * 0.5) - (labelBox.getX() + labelBox.getWidth() * 0.5));
302       }
303 
304       private static double calcDistance(YRectangle r1, YRectangle r2) {
305         if (YRectangle.intersects(r1, r2)) {
306           return 0.0;
307         } else {
308           final double distVertical = calculateOrthogonalDifference(r1, r2, false);
309           final double distHoriztonal = calculateOrthogonalDifference(r1, r2, true);
310           return Math.sqrt(distVertical * distVertical + distHoriztonal * distHoriztonal);
311         }
312       }
313 
314       private static double calculateOrthogonalDifference(YRectangle rect1, YRectangle rect2, boolean horizontal) {
315         final double rect1Min = horizontal ? rect1.getX() : rect1.getY();
316         final double rect1Max = horizontal ? rect1.getX() + rect1.getWidth() : rect1.getY() + rect1.getHeight();
317         final double rect2Min = horizontal ? rect2.getX() : rect2.getY();
318         final double rect2Max = horizontal ? rect2.getX() + rect2.getWidth() : rect2.getY() + rect2.getHeight();
319 
320         if (rect2Max < rect1Min) {
321           // complete rectangle at lower coordinate
322           return rect2Max - rect1Min;
323         } else if (rect1Max < rect2Min) {
324           // complete rectangle at higher coordinate
325           return rect2Min - rect1Max;
326         } else {
327           // intersection of elements
328           return 0.0;
329         }
330       }
331 
332       private static YRectangle getBox(final NodeLayout nodeLayout) {
333         return new YRectangle(nodeLayout.getX(), nodeLayout.getY(), nodeLayout.getWidth(), nodeLayout.getHeight());
334       }
335 
336       /**
337        * The profit value of all labels (i.e., labelBoxes) that are placed above the given node box (i.e., nodeLayout)
338        * lies in [0.5,1]. The profit of the other labels is 0.
339        * For all labels above the given node box, the precise value depends on the distance between the label and the
340        * node box.
341        */
342       private static double calcProfit(final YRectangle labelBox, final NodeLayout nodeLayout) {
343         if (!isLabelAboveNode(labelBox, nodeLayout)) {
344           return 0;
345         }
346 
347         double profit = 0.5;
348         final double dist = calcDistance(labelBox, getBox(nodeLayout));
349         if (dist < MAX_DISTANCE_WITH_PROFIT) {
350           //we prefer candidates that are close to the given node box
351           profit += 0.25 * (MAX_DISTANCE_WITH_PROFIT - dist) / MAX_DISTANCE_WITH_PROFIT;
352         }
353         final double horizontalCenterDist = calcHorizontalCenterDistance(labelBox, nodeLayout);
354         if (horizontalCenterDist < MAX_HORIZONTAL_CENTER_DISTANCE_WITH_PROFIT) {
355           //we prefer candidates with small horizontal center offset
356           profit += 0.25 * (MAX_HORIZONTAL_CENTER_DISTANCE_WITH_PROFIT - horizontalCenterDist) / MAX_HORIZONTAL_CENTER_DISTANCE_WITH_PROFIT;
357         }
358         return profit;
359       }
360 
361       public double getProfit(LabelCandidate candidate) {
362         final NodeLayout nodeLayout = (NodeLayout) label2NodeLayout.get(candidate.getOwner());
363         return (nodeLayout == null) ? 0 : calcProfit(candidate.getBoundingBox(), nodeLayout);
364       }
365     }
366   }
367 
368   /**
369    * Does the label placement using the generic labeling algorithm. Before this, the model and size of the labels is
370    * set according to the option handlers settings.
371    */
372   private void doLabelPlacement() {
373     // update node label model as well as node label size
374     final Graph2D graph = view.getGraph2D();
375 
376     //update the label size and the labeling model
377     final int labelSize = optionHandler.getInt(LABEL_SIZE_STRING);
378     final NodeLabelModel labelingModel = getModel(optionHandler.getEnum(LABELING_MODEL_STRING)); //the model used by the labeling algorithm
379     label2Model.clear();
380     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
381       NodeLabelLayout[] nll = graph.getNodeLabelLayout(nc.node());
382       for (int i = 0; i < nll.length; i++) {
383         label2Model.put(nll[i], labelingModel);
384         ((NodeLabel) nll[i]).setFontSize(labelSize);
385       }
386     }
387 
388     // update the default node realizer
389     final NodeRealizer defaultNodeRealizer = graph.getDefaultNodeRealizer();
390     final NodeLabel nl = defaultNodeRealizer.getLabel();
391     nl.setFontSize(labelSize);
392     final SmartNodeLabelModel model = new SmartNodeLabelModel();
393     nl.setLabelModel(model, model.getDefaultParameter()); //the model that specifies the dynamic behavior of the label
394 
395     new Graph2DLayoutExecutor().doLayout(view, labelLayouter);
396 
397     view.updateView();
398   }
399 
400   /**
401    * Creates an option handler with settings for label model and label size.
402    */
403   private OptionHandler createOptionHandler() {
404     final OptionHandler oh = new OptionHandler("Options");
405     oh.addEnum(LABELING_MODEL_STRING, NODE_LABEL_MODELS, 2);
406     oh.addInt(LABEL_SIZE_STRING, 12, 10, 25);
407 
408     OptionGroup og = new OptionGroup();
409     og.setAttribute(OptionGroup.ATTRIBUTE_TITLE, PROPERTIES_GROUP);
410     og.addItem(oh.getItem(LABELING_MODEL_STRING));
411     og.addItem(oh.getItem(LABEL_SIZE_STRING));
412 
413     oh.addChildPropertyChangeListener(new PropertyChangeListener() {
414       public void propertyChange(PropertyChangeEvent evt) {
415         //apply generic labeling after each change
416         doLabelPlacement();
417       }
418     });
419 
420     return oh;
421   }
422 
423   /**
424    * Returns the label model for the specified index.
425    */
426   private static NodeLabelModel getModel(int index) {
427     if (index < 0 || index >= NODE_LABEL_MODELS.length) {
428       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SANDWICH_MASK);
429     }
430 
431     final String modelString = NODE_LABEL_MODELS[index];
432     if (MODEL_CORNERS.equals(modelString)) {
433       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.CORNER_MASK);
434     } else if (MODEL_EIGHT_POS.equals(modelString)) {
435       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.EIGHT_POS_MASK);
436     } else if (MODEL_FREE.equals(modelString)) {
437       return new SmartNodeLabelModel();
438     } else if (MODEL_SIDE.equals(modelString)) {
439       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SIDES_MASK);
440     } else {
441       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SANDWICH_MASK);
442     }
443   }
444 
445   /**
446    * Creates the tools panel containing the settings and the help panel.
447    */
448   private JPanel createToolsPanel(String helpFilePath) {
449     JPanel toolsPanel = new JPanel(new BorderLayout());
450     toolsPanel.add(createOptionHandlerComponent(optionHandler), BorderLayout.NORTH);
451 
452     if (helpFilePath != null) {
453       final URL url = getResource(helpFilePath);
454       if (url == null) {
455         System.err.println("Could not locate help file: " + helpFilePath);
456       } else {
457         JComponent helpPane = createHelpPane(url);
458         if (helpPane != null) {
459           helpPane.setMinimumSize(new Dimension(200, 200));
460           helpPane.setPreferredSize(new Dimension(TOOLS_PANEL_WIDTH, 400));
461           toolsPanel.add(helpPane, BorderLayout.CENTER);
462         }
463       }
464     }
465 
466     return toolsPanel;
467   }
468 
469   protected void configureDefaultRealizers() {
470     super.configureDefaultRealizers();
471 
472     //customize label configuration
473     final YLabel.Factory factory = NodeLabel.getFactory();
474     final Map implementationsMap = factory.createDefaultConfigurationMap();
475     implementationsMap.put(YLabel.Painter.class, new MyPainter());
476     factory.addConfiguration("Customized", implementationsMap);
477 
478     //set customized configuration as default
479     NodeRealizer nodeRealizer = view.getGraph2D().getDefaultNodeRealizer();
480     nodeRealizer.setSize(10.0, 10.0);
481     nodeRealizer.getLabel().setText("City");
482     nodeRealizer.getLabel().setConfiguration("Customized");
483   }
484 
485   protected EditMode createEditMode() {
486     //configure edit mode
487     final EditMode mode = super.createEditMode();
488     mode.allowEdgeCreation(false);
489     mode.allowMoveSelection(false);
490     mode.setSnappingEnabled(false);
491     mode.allowResizeNodes(false);
492     mode.setPopupMode(new DemoPopupMode());
493     return mode;
494   }
495 
496   protected JToolBar createToolBar() {
497     JToolBar bar = super.createToolBar();
498     bar.addSeparator();
499     bar.add(createActionControl(new LayoutAction()));
500     return bar;
501   }
502 
503   /**
504    * Loads a graph and applies the label configuration to the existing labels.
505    */
506   protected void loadGraph(URL resource) {
507     super.loadGraph(resource);
508 
509     final Graph2D graph2D = view.getGraph2D();
510     DemoDefaults.applyRealizerDefaults(graph2D);
511     for (NodeCursor nc = graph2D.nodes(); nc.ok(); nc.next()) {
512       final NodeLabelLayout[] nll = graph2D.getNodeLabelLayout(nc.node());
513       for (int i = 0; i < nll.length; i++) {
514         ((NodeLabel) nll[i]).setConfiguration("Customized");
515       }
516     }
517   }
518 
519   /**
520    * Performs the generic labeling.
521    */
522   class LayoutAction extends AbstractAction {
523     LayoutAction() {
524       super("Place Labels", SHARED_LAYOUT_ICON);
525       putValue(Action.SHORT_DESCRIPTION, "Place labels");
526     }
527 
528     public void actionPerformed(ActionEvent e) {
529       doLabelPlacement();
530     }
531   }
532 
533   /**
534    * Customized popup mode.
535    */
536   class DemoPopupMode extends PopupMode {
537     /**
538      * Popup menu for a hit node
539      */
540     public JPopupMenu getNodePopup(Node v) {
541       JPopupMenu pm = new JPopupMenu();
542       NodeRealizer r = this.view.getGraph2D().getRealizer(v);
543       YLabel label = r.getLabel();
544       pm.add(new EditLabel(label));
545       return pm;
546     }
547 
548     /**
549      * Popup menu for a hit node label
550      */
551     public JPopupMenu getNodeLabelPopup(NodeLabel label) {
552       JPopupMenu pm = new JPopupMenu();      
553       pm.add(new EditLabel(label));
554       return pm;
555     }
556   }
557 
558   /**
559    * Opens a text editor for the specified label.
560    */
561   class EditLabel extends AbstractAction {
562     YLabel label;
563 
564     EditLabel(YLabel l) {
565       super("Edit Label");
566       label = l;
567     }
568 
569     public void actionPerformed(ActionEvent e) {
570       view.openLabelEditor(label, label.getTextLocation().getX(), label.getTextLocation().getY());
571     }
572   }
573 
574   /**
575    * A simple YLabel.Painter implementation that reuses most of the default painting behavior from
576    * DefaultLabelConfiguration and additionally draws a line between the node and its label.
577    */
578   static final class MyPainter extends DefaultLabelConfiguration {
579     /** Overwrite the painting of the background only. */
580     public void paintBox(YLabel label, Graphics2D gfx, double x, double y, double width, double height) {
581       super.paintBox(label, gfx, x, y, width, height);
582       if (label instanceof NodeLabel) {
583         //determine the line connecting the node center with the center of the corresponding node label
584         final Node node = ((NodeLabel) label).getNode();
585         final Graph2D graph2D = (Graph2D) node.getGraph();
586         final LineSegment connectingLine = new LineSegment(new YPoint(x + width * 0.5d, y + height * 0.5d),
587             graph2D.getCenter(node));
588 
589         //determine start/end point of the line (project the connecting line onto the label/node box)
590         final YRectangle labelBox = new YRectangle(x, y, width, height);
591         YPoint startPoint = calcBorderIntersectionPoints(labelBox, connectingLine);
592         final YRectangle nodeBox = graph2D.getRectangle(node);
593         YPoint endPoint = calcBorderIntersectionPoints(nodeBox, connectingLine);
594 
595         //draw the line
596         if (startPoint != null && endPoint != null) {
597           Line2D line = new Line2D.Double(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
598           gfx.setColor(new Color(0, 0, 0, 150));
599           gfx.draw(line);
600         }
601       }
602     }
603   }
604 
605   /**
606    * Creates a component for the specified option handler using the default editor factory and sets all of its items
607    * to auto adopt and auto commit.
608    */
609   private static JComponent createOptionHandlerComponent(OptionHandler oh) {
610     final EditorFactory defaultEditorFactory = new DefaultEditorFactory();
611     final Editor editor = defaultEditorFactory.createEditor(oh);
612 
613     //propagate auto adopt and auto commit to editor and its children
614     final List stack = new ArrayList();
615     stack.add(editor);
616     while(!stack.isEmpty()) {
617       Object editorObj = stack.remove(stack.size() - 1);
618       if(editorObj instanceof ItemEditor) {
619         ((ItemEditor) editorObj).setAutoAdopt(true);
620         ((ItemEditor) editorObj).setAutoCommit(true);
621       }
622       if(editorObj instanceof CompoundEditor) {
623         for (Iterator iter = ((CompoundEditor) editorObj).editors(); iter.hasNext(); ) {
624           stack.add(iter.next());
625         }
626       }
627     }
628 
629     //build and return component
630     JComponent optionComponent = editor.getComponent();
631     optionComponent.setMinimumSize(new Dimension(200, 50));
632     return optionComponent;
633   }
634 
635   /**
636    * Calculates the intersection point between the given line segment l and the given rectangle r.
637    * We assume that at least one endpoint lies inside r -> at most one intersection point.
638    */
639   private static YPoint calcBorderIntersectionPoints(YRectangle r, LineSegment l) {
640     if(!r.contains(l.getFirstEndPoint()) && !r.contains(l.getSecondEndPoint())) {
641       throw new RuntimeException("Input no valid!");
642     }    
643 
644     //check if l intersects a side of r
645     final YPoint[] rCorners = new YPoint[4]; 
646     rCorners[0] = r.getLocation();
647     rCorners[1] = new YPoint(rCorners[0].x, rCorners[0].y + r.getHeight());
648     rCorners[2] = new YPoint(rCorners[1].x + r.getWidth(), rCorners[1].y);
649     rCorners[3] = new YPoint(rCorners[2].x, rCorners[0].y);
650     for(int i = 0; i < rCorners.length; i++) {
651       final LineSegment rSide = new LineSegment(rCorners[i], rCorners[(i + 1) % 4]);
652       YPoint intersectionPoint = LineSegment.getIntersection(rSide, l);
653       if(intersectionPoint != null) {
654         return intersectionPoint; //found the intersection
655       }
656     }
657     
658     //check special case were l intersects a corner of the rectangle
659     for(int i = 0; i < rCorners.length; i++) {
660       if(l.intersects(rCorners[i])) {
661         return rCorners[i];
662       }
663     }
664 
665     return null; //no intersection
666   }
667 
668   public static void main(String[] args) {
669     EventQueue.invokeLater(new Runnable() {
670       public void run() {
671         Locale.setDefault(Locale.ENGLISH);
672         initLnF();
673         (new NodeLabelingDemo("resource/nodelabelingdemohelp.html")).start("Labeling Demo");
674       }
675     });
676   }
677 }
678 
679 
680       
681