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.viewmode;
29  
30  import demo.view.DemoBase;
31  import y.base.Edge;
32  import y.base.Node;
33  import y.base.YCursor;
34  import y.base.YList;
35  import y.geom.OrientedRectangle;
36  import y.geom.YPoint;
37  import y.geom.YRectangle;
38  import y.layout.LabelCandidate;
39  import y.view.Graph2D;
40  import y.view.HitInfo;
41  import y.view.HitInfoFactories;
42  import y.view.SmartEdgeLabelModel;
43  import y.view.SmartNodeLabelModel;
44  import y.option.OptionHandler;
45  import y.view.DefaultLabelConfiguration;
46  import y.view.EdgeLabel;
47  import y.view.EdgeRealizer;
48  import y.view.EditMode;
49  import y.view.LabelSnapContext;
50  import y.view.MoveLabelMode;
51  import y.view.NodeLabel;
52  import y.view.NodeRealizer;
53  import y.view.PopupMode;
54  import y.view.YLabel;
55  
56  import javax.swing.AbstractAction;
57  import javax.swing.JPopupMenu;
58  import java.awt.Color;
59  import java.awt.Dimension;
60  import java.awt.EventQueue;
61  import java.awt.RenderingHints;
62  import java.awt.event.ActionEvent;
63  import java.util.Locale;
64  import java.util.Map;
65  
66  
67  /**
68   * Demonstrates how to use {@link y.view.SmartNodeLabelModel} and
69   * {@link y.view.SmartEdgeLabelModel}.
70   * <p>
71   * Both models allow for labels to be placed freely at every position.
72   * By default, node labels using <code>SmartNodeLabelModel</code> snap to the
73   * borders and the center of the node as well as the node's other labels.
74   * Edge labels using <code>SmartEdgeLabelModel</code> snap to the edge itself as
75   * well as an imaginary snap segment to the left and an imaginary snap segment
76   * to the right of the edge.
77   * </p>
78   * <p>
79   * Snapping can be disabled by pressing the control key while dragging a label.
80   * </p>
81   * <p>
82   * Method {@link #createEditMode()} shows how to configure {@link MoveLabelMode}
83   * for snapping.
84   * </p>
85   * @see #createEditMode()
86   * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/mvc_controller#cls_MoveLabelMode" target="_blank">Section User Interaction</a> in the yFiles for Java Developer's Guide
87   * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/realizer_related#node_label_model" target="_blank">Section Realizer-Related Features: Node Label Models</a> in the yFiles for Java Developer's Guide
88   * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/realizer_related#edge_label_model" target="_blank">Section Realizer-Related Features: Edge Label Models</a> in the yFiles for Java Developer's Guide
89   */
90  public class SmartLabelModelDemo extends DemoBase {
91    private static final Color LABEL_LINE_COLOR = new Color(153, 204, 255, 255);
92    private static final Color LABEL_BACKGROUND_COLOR = Color.WHITE;
93    private static final String AUTO_FLIPPING_CONFIG = "AutoFlipConfig";
94    private boolean fractionMetricsForSizeCalculationEnabled;
95  
96    public SmartLabelModelDemo() {
97      this(null);
98    }
99  
100   public SmartLabelModelDemo( final String helpFilePath ) {
101     loadInitialGraph();
102     addHelpPane(helpFilePath);
103   }
104 
105   /**
106    * Loads a sample graph.
107    */
108   protected void loadInitialGraph() {
109     loadGraph("resource/SmartLabelModelDemo.graphml");
110   }
111 
112   protected void initialize() {
113     super.initialize();
114     view.setPreferredSize(new Dimension(700, 650));
115     view.setHitInfoFactory(new HitInfoFactories.DefaultHitInfoFactory(view) {
116       public HitInfo createHitInfo(
117               final double x, final double y,
118               final int types,
119               final boolean firstHitOnly
120       ) {
121         return createHitInfo(getView(), getGraph(), x, y, types, firstHitOnly);
122       }
123     });
124 
125     // Ensures that text always fits into label bounds independent of zoom level.
126     view.getRenderingHints().put(
127         RenderingHints.KEY_FRACTIONALMETRICS,
128         RenderingHints.VALUE_FRACTIONALMETRICS_ON);
129     // Store value to be able to reset it when running the demo in the DemoBrowser,
130     // so this setting cannot effect other demos.
131     fractionMetricsForSizeCalculationEnabled = YLabel.isFractionMetricsForSizeCalculationEnabled();
132     YLabel.setFractionMetricsForSizeCalculationEnabled(true);
133   }
134 
135   /**
136    * Cleans up.
137    * This method is called by the demo browser when the demo is stopped or another demo starts.
138    */
139   public void dispose() {
140     YLabel.setFractionMetricsForSizeCalculationEnabled(fractionMetricsForSizeCalculationEnabled);
141   }
142 
143   /**
144    * Configures the default realizers to use labels with
145    * {@link y.view.SmartNodeLabelModel} and
146    * {@link y.view.SmartEdgeLabelModel} as appropriate.
147    */
148   protected void configureDefaultRealizers() {
149     super.configureDefaultRealizers();
150 
151     NodeRealizer nodeRealizer = view.getGraph2D().getDefaultNodeRealizer();
152     nodeRealizer.setSize(200, 100);
153     final NodeLabel nodeLabel = nodeRealizer.getLabel();
154     nodeLabel.setText("Smart Node Label");
155     nodeLabel.setLineColor(LABEL_LINE_COLOR);
156     nodeLabel.setBackgroundColor(LABEL_BACKGROUND_COLOR);
157     final SmartNodeLabelModel nodeLabelModel = new SmartNodeLabelModel();
158     nodeLabel.setLabelModel(nodeLabelModel, nodeLabelModel.getDefaultParameter());
159 
160     final YLabel.Factory factory = EdgeLabel.getFactory();
161     final Map defaultConfigImplementationsMap = factory.createDefaultConfigurationMap();
162     DefaultLabelConfiguration customLabelConfig = new DefaultLabelConfiguration();
163     customLabelConfig.setAutoFlippingEnabled(true);
164     defaultConfigImplementationsMap.put(YLabel.Painter.class, customLabelConfig);
165     defaultConfigImplementationsMap.put(YLabel.Layout.class, customLabelConfig);
166     defaultConfigImplementationsMap.put(YLabel.BoundsProvider.class, customLabelConfig);
167     factory.addConfiguration(AUTO_FLIPPING_CONFIG, defaultConfigImplementationsMap);
168 
169     EdgeRealizer edgeRealizer = view.getGraph2D().getDefaultEdgeRealizer();
170     final EdgeLabel edgeLabel = edgeRealizer.getLabel();
171     edgeLabel.setText("Smart Edge Label");
172     edgeLabel.setLineColor(LABEL_LINE_COLOR);
173     edgeLabel.setBackgroundColor(LABEL_BACKGROUND_COLOR);
174     final SmartEdgeLabelModel edgeLabelModel = new SmartEdgeLabelModel();
175     edgeLabel.setLabelModel(edgeLabelModel, edgeLabelModel.createDiscreteModelParameter(SmartEdgeLabelModel.POSITION_CENTER));
176     edgeLabel.setConfiguration(AUTO_FLIPPING_CONFIG);
177   }
178 
179   /**
180    * Creates a edit mode that is configured to use snapping for label movements
181    * and to provide custom context menus for edge, nodes, and their labels.
182    * @return an appropriately configured {@link EditMode} instance.
183    */
184   protected EditMode createEditMode() {
185     final EditMode mode = super.createEditMode();
186     mode.setCyclicSelectionEnabled(true);   // To be able to select labels in front of a node
187     mode.setPopupMode(new DemoPopupMode());
188     final SnappingConfiguration snappingConfiguration = DemoBase.createDefaultSnappingConfiguration();
189     snappingConfiguration.configureView(view);
190     snappingConfiguration.configureEditMode(mode);
191 
192     // configure snap context for label snapping
193     final LabelSnapContext snapContext = new LabelSnapContext(view);
194     ((MoveLabelMode) mode.getMoveLabelMode()).setSnapContext(snapContext);
195     snapContext.setNodeLabelSnapDistance(10);
196     snapContext.setEdgeLabelSnapDistance(30);
197     snapContext.setSnapLineColor(Color.BLUE);
198 
199     return mode;
200   }
201 
202   /**
203    * Launches <code>SmartLabelModelDemo</code>.
204    * @param args not used
205    */
206   public static void main( final String[] args ) {
207     EventQueue.invokeLater(new Runnable() {
208       public void run() {
209         Locale.setDefault(Locale.ENGLISH);
210         initLnF();
211         (new SmartLabelModelDemo("resource/smartlabelmodeldemo.html")).start();
212       }
213     });
214   }
215 
216 
217   /**
218    * A PopupMode for adding or editing node labels and edge labels.
219    */
220   private static final class DemoPopupMode extends PopupMode {
221     /**
222      * Creates a context menu for nodes that allows for adding labels.
223      * @param node the node whose context actions have to be displayed.
224      * @return a context menu for nodes that allows for adding labels.
225      */
226     public JPopupMenu getNodePopup( final Node node ) {
227       JPopupMenu pm = new JPopupMenu();
228       pm.add(new AbstractAction("Add Label") {
229         public void actionPerformed(ActionEvent e) {
230           if (node == null) {
231             return;
232           }
233 
234           final SmartNodeLabelModel nodeLabelModel = new SmartNodeLabelModel();
235           final NodeLabel label = new NodeLabel("Smart Node Label");
236           label.setLineColor(LABEL_LINE_COLOR);
237           label.setBackgroundColor(LABEL_BACKGROUND_COLOR);
238           label.setLabelModel(nodeLabelModel, nodeLabelModel.getDefaultParameter());
239           getGraph2D().getRealizer(node).addLabel(label);
240           getGraph2D().updateViews();
241         }
242       });
243       return pm;
244     }
245 
246     /**
247      * Creates a context menu for node labels that allows for editing label
248      * properties such as text, font size, and rotation.
249      * @param label the label whose context actions have to be displayed.
250      * @return a context menu for node labels that allows for editing label
251      * properties.
252      */
253     public JPopupMenu getNodeLabelPopup( final NodeLabel label ) {
254       JPopupMenu pm = new JPopupMenu();
255       pm.add(new AbstractAction("Edit Label") {
256         public void actionPerformed(ActionEvent e) {
257           if (label == null) {
258             return;
259           }
260 
261           LabelOptionHandler oh = new LabelOptionHandler(label, getGraph2D());
262           oh.showEditor(null, OptionHandler.OK_CANCEL_BUTTONS);
263         }
264       });
265       return pm;
266     }
267 
268     /**
269      * Creates a context menu for edges that allows for adding labels.
270      * @param edge the edge whose context actions have to be displayed.
271      * @return a context menu for edges that allows for adding labels.
272      */
273     public JPopupMenu getEdgePopup( final Edge edge ) {
274       JPopupMenu pm = new JPopupMenu();
275       pm.add(new AbstractAction("Add Label") {
276         public void actionPerformed(ActionEvent e) {
277           if (edge == null) {
278             return;
279           }
280 
281           final SmartEdgeLabelModel edgeLabelModel = new SmartEdgeLabelModel();
282           final EdgeLabel label = new EdgeLabel("Smart Edge Label");
283           label.setConfiguration(AUTO_FLIPPING_CONFIG);
284           label.setLineColor(LABEL_LINE_COLOR);
285           label.setBackgroundColor(LABEL_BACKGROUND_COLOR);
286           label.setLabelModel(edgeLabelModel, edgeLabelModel.getDefaultParameter());
287           getGraph2D().getRealizer(edge).addLabel(label);
288 
289           final Object parameter = calculateBestParameter(edge, label);
290           if (parameter != null) {
291             label.setModelParameter(parameter);
292           }
293           getGraph2D().updateViews();
294         }
295       });
296       return pm;
297     }
298 
299     /**
300      * Creates a context menu for edge labels that allows for editing label
301      * properties such as text, font size, and rotation.
302      * @param label the label whose context actions have to be displayed.
303      * @return a context menu for edge labels that allows for editing label
304      * properties.
305      */
306     public JPopupMenu getEdgeLabelPopup( final EdgeLabel label ) {
307       JPopupMenu pm = new JPopupMenu();
308       pm.add(new AbstractAction("Edit Label") {
309         public void actionPerformed(ActionEvent e) {
310           if (label == null) {
311             return;
312           }
313 
314           LabelOptionHandler oh = new LabelOptionHandler(label, getGraph2D());
315           oh.showEditor(null, OptionHandler.OK_CANCEL_BUTTONS);
316         }
317       });
318       return pm;
319     }
320 
321     /**
322      * Determines a model parameter that represents a label position that is not
323      * already occupied by another label and lies near the current mouse
324      * position.
325      * @param e The edge where the label is set
326      * @param label The current label layout
327      * @return A model parameter with the new label position
328      */
329     private Object calculateBestParameter( final Edge e, final EdgeLabel label ) {
330       if (lastPressEvent == null) {
331         return null;
332       }
333 
334       //identify occupied boxes
335       YList occupiedRectList = new YList();
336       Graph2D graph = view.getGraph2D();
337       EdgeRealizer er = graph.getRealizer(e);
338       for (int i = 0, n = er.labelCount(); i < n; ++i) {
339         EdgeLabel el = er.getLabel(i);
340         if (el != label) {
341           occupiedRectList.add(el.getOrientedBox());
342         }
343       }
344 
345       //find label candidates with non-occupied boxes and low distance to point "mousePressedEventLocation"
346       final YPoint mousePressedEventLocation = new YPoint(
347               view.toWorldCoordX(lastPressEvent.getX()),
348               view.toWorldCoordY(lastPressEvent.getY()));
349       double minDist = Double.MAX_VALUE;
350       LabelCandidate chosen = null;
351       YList candidates = label.getLabelModel().getLabelCandidates(
352               label, er, er.getSourceRealizer(), er.getTargetRealizer());
353       for (YCursor cu = candidates.cursor(); cu.ok(); cu.next()) {
354         LabelCandidate lc = (LabelCandidate) cu.current();
355 
356         boolean isOccupied = false;
357         YRectangle bBox = lc.getBox().getBoundingBox();
358         for (YCursor cur = occupiedRectList.cursor(); cur.ok() && !isOccupied; cur.next()) {
359           if (OrientedRectangle.intersects((OrientedRectangle) cur.current(), bBox, 0)) {
360             isOccupied = true;
361             break;
362           }
363         }
364 
365         if (!isOccupied) {
366           double dist = YPoint.distance(mousePressedEventLocation, lc.getBox().getCenter());
367           if (dist < minDist) {
368             minDist = dist;
369             chosen = lc;
370           }
371         }
372       }
373 
374       return chosen == null ? null : chosen.getModelParameter();
375     }
376   }
377 
378   /**
379    * An option handler for editing node or edge labels.
380    * It is possible to edit the label text, the font size and
381    * the rotation angle.
382    */
383   private static final class LabelOptionHandler extends OptionHandler {
384     static final String LABEL_TEXT = "Label Text";
385     static final String FONT_SIZE = "Font Size";
386     static final String ROTATION_ANGLE = "Rotation Angle";
387     static final String AUTO_ROTATION = "Auto Rotation";
388 
389     private final YLabel label;
390     private final Graph2D graph;
391     private final boolean isEdgeLabel;
392 
393     /**
394      * Initializes a new <code>LabelOptionHandler</code> for the specified
395      * label and view.
396      * @param label the label to edit.
397      * @param graph the label's associated graph.
398      */
399     LabelOptionHandler( final YLabel label, final Graph2D graph ) {
400       super("Label Options");
401       this.label = label;
402       this.graph = graph;
403       this.isEdgeLabel = label instanceof EdgeLabel;
404       addString(LABEL_TEXT, label.getText(), 5);
405       addInt(FONT_SIZE, label.getFontSize());
406       int value = 0;
407       if (isEdgeLabel) {
408         final EdgeLabel el = (EdgeLabel) label;
409         if (el.getLabelModel() instanceof SmartEdgeLabelModel) {
410           // OrientedRectangle interprets angles in counter-clockwise direction
411           // but label models and their parameters interpret angles in
412           // clockwise direction therefore the parameter angle has to be
413           // translated from clockwise to counter-clockwise here
414           value = toDegrees(2*Math.PI - SmartEdgeLabelModel.getAngle(el.getModelParameter()));
415         }
416       } else {
417         value = toDegrees(label.getOrientedBox().getAngle());
418       }
419       while (value < 0) {
420         value += 360;
421       }
422       addInt(ROTATION_ANGLE, value, 0, 360);
423       if (isEdgeLabel) {
424         final EdgeLabel el = (EdgeLabel) label;
425         if (el.getLabelModel() instanceof SmartEdgeLabelModel) {
426           SmartEdgeLabelModel model = (SmartEdgeLabelModel) el.getLabelModel();
427           addBool(AUTO_ROTATION, model.isAutoRotationEnabled());
428         }
429       }
430     }
431 
432     public void commitValues() {
433       super.commitValues();
434       label.setText(getString(LABEL_TEXT));
435       label.setFontSize(getInt(FONT_SIZE));
436 
437       final OrientedRectangle box = label.getOrientedBox();
438       final YPoint center = box.getCenter();
439       box.setAngle(Math.toRadians(getInt(ROTATION_ANGLE)));
440       box.setCenter(center);
441 
442       if (isEdgeLabel && ((EdgeLabel) label).getLabelModel() instanceof SmartEdgeLabelModel) {
443         final EdgeLabel el = (EdgeLabel) label;
444         final EdgeRealizer er = el.getRealizer();
445         final SmartEdgeLabelModel model = (SmartEdgeLabelModel) el.getLabelModel();
446         model.setAutoRotationEnabled(getBool(AUTO_ROTATION));
447         el.setModelParameter(model.createRelativeModelParameter(
448             box,
449             er,
450             er.getSourceRealizer(),
451             er.getTargetRealizer()));
452       } else {
453         label.setModelParameter(label.getBestModelParameterForBounds(box));
454       }
455 
456       graph.updateViews();
457     }
458 
459     private static int toDegrees( final double angle ) {
460       return ((int) Math.rint(Math.toDegrees(angle))) % 360;
461     }
462   }
463 }
464