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.labeling;
15  
16  import demo.view.DemoBase;
17  import demo.view.DemoDefaults;
18  import y.base.GraphEvent;
19  import y.base.GraphListener;
20  import y.base.Node;
21  import y.base.NodeCursor;
22  import y.layout.DiscreteNodeLabelModel;
23  import y.layout.NodeLabelModel;
24  import y.layout.labeling.AbstractLabelingAlgorithm;
25  import y.layout.labeling.GreedyMISLabeling;
26  import y.layout.labeling.MISLabelingAlgorithm;
27  import y.layout.NodeLabelLayout;
28  import y.layout.BufferedLayouter;
29  import y.option.EditorFactory;
30  import y.option.OptionGroup;
31  import y.option.OptionHandler;
32  import y.option.DefaultEditorFactory;
33  import y.option.Editor;
34  import y.option.ItemEditor;
35  import y.option.CompoundEditor;
36  import y.util.DataProviderAdapter;
37  import y.view.DefaultBackgroundRenderer;
38  import y.view.EditMode;
39  import y.view.Graph2D;
40  import y.view.NodeLabel;
41  import y.view.NodeRealizer;
42  import y.view.PopupMode;
43  import y.view.SmartNodeLabelModel;
44  import y.view.YLabel;
45  import y.view.DefaultLabelConfiguration;
46  import y.geom.YPoint;
47  import y.geom.YRectangle;
48  import y.geom.LineSegment;
49  
50  import javax.swing.AbstractAction;
51  import javax.swing.Action;
52  import javax.swing.JPopupMenu;
53  import javax.swing.JToolBar;
54  import javax.swing.JPanel;
55  import javax.swing.JComponent;
56  import java.awt.Color;
57  import java.awt.Dimension;
58  import java.awt.EventQueue;
59  import java.awt.Graphics2D;
60  import java.awt.BorderLayout;
61  import java.awt.Shape;
62  import java.awt.geom.Line2D;
63  import java.awt.event.ActionEvent;
64  import java.net.URL;
65  import java.util.Arrays;
66  import java.util.HashMap;
67  import java.util.HashSet;
68  import java.util.List;
69  import java.util.Locale;
70  import java.util.Map;
71  import java.util.ArrayList;
72  import java.util.Iterator;
73  import java.beans.PropertyChangeEvent;
74  import java.beans.PropertyChangeListener;
75  import java.util.Set;
76  
77  /**
78   * This demo shows how to configure node labels and the corresponding label models as well as how to apply the
79   * generic node label placement algorithm.
80   *
81   * A new city (node) can be added by left-clicking on the corresponding map location. To edit a node label right-click
82   * on the label or the corresponding node and choose item "Edit Label". Node labels can be moved to an arbitrary
83   * position using drag and drop.
84   *
85   * To manually start the generic labeling algorithm click on the "Do Generic Labeling" button. Note: after changing one
86   * of the node label properties, the generic labeling algorithm is applied automatically.
87   *
88   *
89   * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/labeling.html#labeling">Section Automatic Label Placement</a> in the yFiles for Java Developer's Guide
90   */
91  public class NodeLabelingDemo extends DemoBase {
92    private static final String LABELING_MODEL_STRING = "Labeling Model";
93    private static final String LABEL_SIZE_STRING = "Font Size";
94    private static final String PROPERTIES_GROUP = "Node Label Properties";
95  
96    //node label model constants
97    private static final String MODEL_CORNERS = "Corners";  
98    private static final String MODEL_SANDWICH = "Sandwich";
99    private static final String MODEL_SIDE = "Side";
100   private static final String MODEL_FREE = "Free";
101   private static final String MODEL_EIGHT_POS = "8 Pos";
102   private static final String[] NODE_LABEL_MODELS = {
103       MODEL_CORNERS, MODEL_SANDWICH, MODEL_SIDE, MODEL_FREE, MODEL_EIGHT_POS
104   };
105 
106   private static final int TOOLS_PANEL_WIDTH = 350;
107 
108   private Map label2Model; //stores (for each label) the label model used by the labeling algorithm
109   private GreedyMISLabeling labelLayouter;
110   private OptionHandler optionHandler;
111 
112   public NodeLabelingDemo() {
113     this(null);
114   }
115 
116   public NodeLabelingDemo(final String helpFilePath) {
117     // render a map of the USA in the background
118     DefaultBackgroundRenderer renderer = new DefaultBackgroundRenderer(view);
119     URL bgImage = getClass().getResource("resource/usamap.gif");
120     renderer.setImageResource(bgImage);
121     renderer.setMode(DefaultBackgroundRenderer.DYNAMIC);
122     renderer.setColor(Color.white);
123     view.setBackgroundRenderer(renderer);
124     view.setPreferredSize(new Dimension(650, 400));
125     view.setWorldRect(0, 0, 650, 400);
126 
127     contentPane.add(createToolsPanel(helpFilePath), BorderLayout.EAST);
128 
129     loadGraph("resource/uscities.graphml");
130 
131     //after creating a new node the label should be placed accordingly
132     view.getGraph2D().addGraphListener(new GraphListener() {
133       public void onGraphEvent(GraphEvent e) {
134         if (e.getType() == GraphEvent.NODE_CREATION) {
135           final Graph2D graph = view.getGraph2D();
136           final Node node = (Node) e.getData();
137           final NodeLabelLayout[] nll = graph.getNodeLabelLayout(node);
138 
139           //set the label model and size
140           final int labelSize = optionHandler.getInt(LABEL_SIZE_STRING);
141           final NodeLabelModel labelingModel = getModel(
142               optionHandler.getEnum(LABELING_MODEL_STRING)); //the model used by the labeling algorithm
143           for (int i = 0; i < nll.length; i++) {
144             label2Model.put(nll[i], labelingModel);
145             ((NodeLabel) nll[i]).setFontSize(labelSize);
146           }
147 
148           //mark the new labels and place them
149           final Set newLabels = new HashSet(Arrays.asList(nll));
150           graph.addDataProvider("SELECTED_LABELS", new DataProviderAdapter() {
151             public boolean getBool(Object dataHolder) {
152               return newLabels.contains(dataHolder);
153             }
154           });
155           final Object oldSelectionKey = labelLayouter.getSelectionKey();
156           labelLayouter.setSelection("SELECTED_LABELS");
157           new BufferedLayouter(labelLayouter).doLayout(graph);
158           labelLayouter.setSelection(oldSelectionKey);
159           graph.removeDataProvider("SELECTED_LABELS");
160         }
161       }
162     });
163 
164     // do initial label placement
165     doLabelPlacement();
166   }
167 
168   protected void initialize() {
169     optionHandler = createOptionHandler();
170 
171     labelLayouter = new GreedyMISLabeling();
172     labelLayouter.setOptimizationStrategy(MISLabelingAlgorithm.OPTIMIZATION_BALANCED);
173     labelLayouter.setPlaceEdgeLabels(false);
174     labelLayouter.setPlaceNodeLabels(true);
175     labelLayouter.setApplyPostprocessing(true);
176 
177     //register the data-provider that stores for each node label the label model that is used by the labeling algorithm
178     label2Model = new HashMap();
179     view.getGraph2D().addDataProvider(AbstractLabelingAlgorithm.LABEL_MODEL_DPKEY, new DataProviderAdapter() {
180       public Object get(Object dataHolder) {
181         return label2Model.get(dataHolder);
182       }
183     });
184   }
185 
186   /**
187    * Does the label placement using the generic labeling algorithm. Before this, the model and size of the labels is
188    * set according to the option handlers settings.
189    */
190   private void doLabelPlacement() {
191     // update node label model as well as node label size
192     final Graph2D graph = view.getGraph2D();
193 
194     //update the label size and the labeling model
195     final int labelSize = optionHandler.getInt(LABEL_SIZE_STRING);
196     final NodeLabelModel labelingModel = getModel(optionHandler.getEnum(LABELING_MODEL_STRING)); //the model used by the labeling algorithm
197     label2Model.clear();
198     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
199       NodeLabelLayout[] nll = graph.getNodeLabelLayout(nc.node());
200       for (int i = 0; i < nll.length; i++) {
201         label2Model.put(nll[i], labelingModel);
202         ((NodeLabel) nll[i]).setFontSize(labelSize);
203       }
204     }
205 
206     // update the default node realizer
207     final NodeRealizer defaultNodeRealizer = graph.getDefaultNodeRealizer();
208     defaultNodeRealizer.getLabel().setFontSize(labelSize);
209     defaultNodeRealizer.getLabel().setLabelModel(new SmartNodeLabelModel()); //the model that specifies the dynamic behavior of the label
210 
211     new BufferedLayouter(labelLayouter).doLayout(view.getGraph2D());
212 
213     view.updateView();
214   }
215 
216   /**
217    * Creates an option handler with settings for label model and label size.
218    */
219   private OptionHandler createOptionHandler() {
220     final OptionHandler oh = new OptionHandler("Options");
221     oh.addEnum(LABELING_MODEL_STRING, NODE_LABEL_MODELS, 2);
222     oh.addInt(LABEL_SIZE_STRING, 12, 10, 25);
223 
224     OptionGroup og = new OptionGroup();
225     og.setAttribute(OptionGroup.ATTRIBUTE_TITLE, PROPERTIES_GROUP);
226     og.addItem(oh.getItem(LABELING_MODEL_STRING));
227     og.addItem(oh.getItem(LABEL_SIZE_STRING));
228 
229     oh.addChildPropertyChangeListener(new PropertyChangeListener() {
230       public void propertyChange(PropertyChangeEvent evt) {
231         //apply generic labeling after each change
232         doLabelPlacement();
233       }
234     });
235 
236     return oh;
237   }
238 
239   /**
240    * Returns the label model for the specified index.
241    */
242   private static NodeLabelModel getModel(int index) {
243     if (index < 0 || index >= NODE_LABEL_MODELS.length) {
244       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SANDWICH_MASK);
245     }
246 
247     final String modelString = NODE_LABEL_MODELS[index];
248     if (MODEL_CORNERS.equals(modelString)) {
249       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.CORNER_MASK);
250     } else if (MODEL_EIGHT_POS.equals(modelString)) {
251       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.EIGHT_POS_MASK);
252     } else if (MODEL_FREE.equals(modelString)) {
253       return new SmartNodeLabelModel();
254     } else if (MODEL_SIDE.equals(modelString)) {
255       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SIDES_MASK);
256     } else {
257       return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SANDWICH_MASK);
258     }
259   }
260 
261   /**
262    * Creates the tools panel containing the settings and the help panel.
263    */
264   private JPanel createToolsPanel(String helpFilePath) {
265     JPanel toolsPanel = new JPanel(new BorderLayout());
266     toolsPanel.add(createOptionHandlerComponent(optionHandler), BorderLayout.NORTH);
267 
268     if (helpFilePath != null) {
269       final URL url = getClass().getResource(helpFilePath);
270       if (url == null) {
271         System.err.println("Could not locate help file: " + helpFilePath);
272       } else {
273         JComponent helpPane = createHelpPane(url);
274         if (helpPane != null) {
275           helpPane.setMinimumSize(new Dimension(200, 200));
276           helpPane.setPreferredSize(new Dimension(TOOLS_PANEL_WIDTH, 400));
277           toolsPanel.add(helpPane, BorderLayout.CENTER);
278         }
279       }
280     }
281 
282     return toolsPanel;
283   }
284 
285   protected void configureDefaultRealizers() {
286     super.configureDefaultRealizers();
287 
288     //customize label configuration
289     final YLabel.Factory factory = NodeLabel.getFactory();
290     final Map implementationsMap = factory.createDefaultConfigurationMap();
291     implementationsMap.put(YLabel.Painter.class, new MyPainter());
292     factory.addConfiguration("Customized", implementationsMap);
293 
294     //set customized configuration as default
295     NodeRealizer nodeRealizer = view.getGraph2D().getDefaultNodeRealizer();
296     nodeRealizer.setSize(10.0, 10.0);
297     nodeRealizer.getLabel().setText("City");
298     nodeRealizer.getLabel().setConfiguration("Customized");
299   }
300 
301   protected EditMode createEditMode() {
302     //configure edit mode
303     final EditMode mode = super.createEditMode();
304     mode.allowEdgeCreation(false);
305     mode.allowMoveSelection(false);
306     mode.setSnappingEnabled(false);
307     mode.allowResizeNodes(false);
308     mode.setPopupMode(new DemoPopupMode());
309     return mode;
310   }
311 
312   protected JToolBar createToolBar() {
313     JToolBar bar = super.createToolBar();
314     bar.addSeparator();
315     bar.add(createActionControl(new LayoutAction()));
316     return bar;
317   }
318 
319   /**
320    * Loads a graph and applies the label configuration to the existing labels.
321    */
322   protected void loadGraph(URL resource) {
323     super.loadGraph(resource);
324 
325     final Graph2D graph2D = view.getGraph2D();
326     DemoDefaults.applyRealizerDefaults(graph2D);
327     for (NodeCursor nc = graph2D.nodes(); nc.ok(); nc.next()) {
328       final NodeLabelLayout[] nll = graph2D.getNodeLabelLayout(nc.node());
329       for (int i = 0; i < nll.length; i++) {
330         ((NodeLabel) nll[i]).setConfiguration("Customized");
331       }
332     }
333   }
334 
335   /**
336    * Performs the generic labeling.
337    */
338   class LayoutAction extends AbstractAction {
339     LayoutAction() {
340       super("Place Labels", SHARED_LAYOUT_ICON);
341       putValue(Action.SHORT_DESCRIPTION, "Place labels");
342     }
343 
344     public void actionPerformed(ActionEvent e) {
345       doLabelPlacement();
346     }
347   }
348 
349   /**
350    * Customized popup mode.
351    */
352   class DemoPopupMode extends PopupMode {
353     /**
354      * Popup menu for a hit node
355      */
356     public JPopupMenu getNodePopup(Node v) {
357       JPopupMenu pm = new JPopupMenu();
358       NodeRealizer r = this.view.getGraph2D().getRealizer(v);
359       YLabel label = r.getLabel();
360       pm.add(new EditLabel(label));
361       return pm;
362     }
363 
364     /**
365      * Popup menu for a hit node label
366      */
367     public JPopupMenu getNodeLabelPopup(NodeLabel label) {
368       JPopupMenu pm = new JPopupMenu();      
369       pm.add(new EditLabel(label));
370       return pm;
371     }
372   }
373 
374   /**
375    * Opens a text editor for the specified label.
376    */
377   class EditLabel extends AbstractAction {
378     YLabel label;
379 
380     EditLabel(YLabel l) {
381       super("Edit Label");
382       label = l;
383     }
384 
385     public void actionPerformed(ActionEvent e) {
386       view.openLabelEditor(label, label.getTextLocation().getX(), label.getTextLocation().getY());     
387     }
388   }
389 
390   /**
391    * A simple YLabel.Painter implementation that reuses most of the default painting behavior from
392    * DefaultLabelConfiguration and additionally draws a line between the node and its label.
393    */
394   static final class MyPainter extends DefaultLabelConfiguration {
395     /** Overwrite the painting of the background only. */
396     public void paintBox(YLabel label, Graphics2D gfx, double x, double y, double width, double height) {
397       super.paintBox(label, gfx, x, y, width, height);
398       if (label instanceof NodeLabel) {
399         //determine the line connecting the node center with the center of the corresponding node label
400         final Node node = ((NodeLabel) label).getNode();
401         final Graph2D graph2D = (Graph2D) node.getGraph();
402         final LineSegment connectingLine = new LineSegment(new YPoint(x + width * 0.5d, y + height * 0.5d),
403             graph2D.getCenter(node));
404 
405         //determine start/end point of the line (project the connecting line onto the label/node box)
406         final YRectangle labelBox = new YRectangle(x, y, width, height);
407         YPoint startPoint = calcBorderIntersectionPoints(labelBox, connectingLine);
408         final YRectangle nodeBox = graph2D.getRectangle(node);
409         YPoint endPoint = calcBorderIntersectionPoints(nodeBox, connectingLine);
410 
411         //draw the line
412         if (startPoint != null && endPoint != null) {
413           Line2D line = new Line2D.Double(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
414           gfx.setColor(new Color(0, 0, 0, 150));
415           gfx.draw(line);
416         }
417       }
418     }
419   }
420 
421   /**
422    * Creates a component for the specified option handler using the default editor factory and sets all of its items
423    * to auto adopt and auto commit.
424    */
425   private static JComponent createOptionHandlerComponent(OptionHandler oh) {
426     final EditorFactory defaultEditorFactory = new DefaultEditorFactory();
427     final Editor editor = defaultEditorFactory.createEditor(oh);
428 
429     //propagate auto adopt and auto commit to editor and its children
430     final List stack = new ArrayList();
431     stack.add(editor);
432     while(!stack.isEmpty()) {
433       Object editorObj = stack.remove(stack.size() - 1);
434       if(editorObj instanceof ItemEditor) {
435         ((ItemEditor) editorObj).setAutoAdopt(true);
436         ((ItemEditor) editorObj).setAutoCommit(true);
437       }
438       if(editorObj instanceof CompoundEditor) {
439         for (Iterator iter = ((CompoundEditor) editorObj).editors(); iter.hasNext(); ) {
440           stack.add(iter.next());
441         }
442       }
443     }
444 
445     //build and return component
446     JComponent optionComponent = editor.getComponent();
447     optionComponent.setMinimumSize(new Dimension(200, 50));
448     return optionComponent;
449   }
450 
451   /**
452    * Calculates the intersection point between the given line segment l and the given rectangle r.
453    * We assume that at least one endpoint lies inside r -> at most one intersection point.
454    */
455   private static YPoint calcBorderIntersectionPoints(YRectangle r, LineSegment l) {
456     if(!r.contains(l.getFirstEndPoint()) && !r.contains(l.getSecondEndPoint())) {
457       throw new RuntimeException("Input no valid!");
458     }    
459 
460     //check if l intersects a side of r
461     final YPoint[] rCorners = new YPoint[4]; 
462     rCorners[0] = r.getLocation();
463     rCorners[1] = new YPoint(rCorners[0].x, rCorners[0].y + r.getHeight());
464     rCorners[2] = new YPoint(rCorners[1].x + r.getWidth(), rCorners[1].y);
465     rCorners[3] = new YPoint(rCorners[2].x, rCorners[0].y);
466     for(int i = 0; i < rCorners.length; i++) {
467       final LineSegment rSide = new LineSegment(rCorners[i], rCorners[(i + 1) % 4]);
468       YPoint intersectionPoint = LineSegment.getIntersection(rSide, l);
469       if(intersectionPoint != null) {
470         return intersectionPoint; //found the intersection
471       }
472     }
473     
474     //check special case were l intersects a corner of the rectangle
475     for(int i = 0; i < rCorners.length; i++) {
476       if(l.intersects(rCorners[i])) {
477         return rCorners[i];
478       }
479     }
480 
481     return null; //no intersection
482   }
483 
484   public static void main(String[] args) {
485     EventQueue.invokeLater(new Runnable() {
486       public void run() {
487         Locale.setDefault(Locale.ENGLISH);
488         initLnF();
489         (new NodeLabelingDemo("resource/nodelabelingdemohelp.html")).start("Labeling Demo");
490       }
491     });
492   }
493 }
494 
495 
496       
497