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.view.realizer;
15  
16  import demo.view.DemoBase;
17  import demo.view.DemoDefaults;
18  
19  import y.base.Edge;
20  import y.base.Node;
21  import y.geom.AffineLine;
22  import y.geom.YPoint;
23  import y.geom.YVector;
24  import y.view.DefaultLabelConfiguration;
25  import y.view.EdgeLabel;
26  import y.view.EdgeRealizer;
27  import y.view.Graph2D;
28  import y.view.LineType;
29  import y.view.NodeLabel;
30  import y.view.NodeRealizer;
31  import y.view.SmartEdgeLabelModel;
32  import y.view.SmartNodeLabelModel;
33  import y.view.YLabel;
34  import y.view.Arrow;
35  import y.view.YRenderingHints;
36  
37  import java.awt.Color;
38  import java.awt.EventQueue;
39  import java.awt.Graphics2D;
40  import java.awt.Shape;
41  import java.awt.Stroke;
42  import java.awt.geom.Area;
43  import java.awt.geom.GeneralPath;
44  import java.awt.geom.Line2D;
45  import java.awt.geom.PathIterator;
46  import java.awt.geom.RoundRectangle2D;
47  import java.awt.geom.Point2D;
48  import java.util.Locale;
49  import java.util.Map;
50  
51  /**
52   * This class demonstrates the usages of {@link YLabel}'s configuration feature.
53   *
54   * @see YLabel#setConfiguration(String)
55   * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/realizer_related.html#labels_customization">Section Realizer-Related Features</a> in the yFiles for Java Developer's Guide
56   */
57  public class YLabelConfigurationDemo extends DemoBase {
58    /**
59     * Launcher method. Execute this class to see sample instantiations of {@link YLabel}s using a custom
60     * configuration in action.
61     */
62    public static void main(String[] args) {
63      EventQueue.invokeLater(new Runnable() {
64        public void run() {
65          Locale.setDefault(Locale.ENGLISH);
66          initLnF();
67          (new YLabelConfigurationDemo()).start();
68        }
69      });
70    }
71  
72    /** Creates the YLabelConfigurationDemo demo. */
73    public YLabelConfigurationDemo() {
74      super();
75      Graph2D graph2D;
76      {
77        // Get the factory to register custom styles/configurations.
78        YLabel.Factory factory = NodeLabel.getFactory();
79  
80        // Retrieve a map that holds the default NodeLabel configuration.
81        // The implementations contained therein can be replaced one by one in order
82        // to create custom configurations...
83        Map implementationsMap = factory.createDefaultConfigurationMap();
84  
85        // We will just customize the painting so register our custom painter
86        implementationsMap.put(YLabel.Painter.class, new MyPainter());
87  
88        // Add the first configuration to the factory.
89        factory.addConfiguration("Bubble", implementationsMap);
90  
91        // configure the default label to use our new configuration and give it a funky color and style
92        graph2D = view.getGraph2D();
93        NodeRealizer realizer = graph2D.getDefaultNodeRealizer();
94        NodeLabel label = realizer.getLabel();
95        label.setLabelModel(new SmartNodeLabelModel());
96        label.setConfiguration("Bubble");
97        label.setLineColor(Color.DARK_GRAY);
98        label.setBackgroundColor(new Color(202,227,255));
99      }
100 
101     {
102       // Make a similar configuration for edge labels.
103       YLabel.Factory factory = EdgeLabel.getFactory();
104       Map implementationsMap = factory.createDefaultConfigurationMap();
105       implementationsMap.put(YLabel.Painter.class, new MyPainter());
106       factory.addConfiguration("Bubble", implementationsMap);
107       graph2D = view.getGraph2D();
108       EdgeRealizer realizer = graph2D.getDefaultEdgeRealizer();
109       EdgeLabel label = realizer.getLabel();
110       label.setLabelModel(new SmartEdgeLabelModel());
111       label.setDistance(30);
112       label.setConfiguration("Bubble");
113       label.setLineColor(Color.DARK_GRAY);
114       label.setBackgroundColor(new Color(202,227,255));
115     }
116 
117     // load a sample...
118     loadGraph("resource/bubble.graphml");
119     DemoDefaults.applyRealizerDefaults(view.getGraph2D(), true, true);
120     view.getGraph2D().getDefaultEdgeRealizer().setTargetArrow(Arrow.NONE);
121   }
122 
123 
124   /**
125    * A simple YLabel.Painter implementation that reuses most of the default painting behavior from
126    * DefaultLabelConfiguration and just changes the way the background is painted.
127    */
128   static final class MyPainter extends DefaultLabelConfiguration {
129 
130     private static final Color SELECTION_COLOR = new Color(0, 40, 158);
131 
132     /** Overwrite the painting of the background only. */
133     public void paintBox(YLabel label, Graphics2D gfx, double x, double y, double width, double height) {
134       // store old graphics values
135       Color oldColor = gfx.getColor();
136       Stroke oldStroke = gfx.getStroke();
137 
138       // calculate the bubble
139       Shape shape = new RoundRectangle2D.Double(x, y, width, height, Math.min(width / 3, 10), Math.min(height / 3, 10));
140 
141       double cx = x + width * 0.5d;
142       double cy = y + height * 0.5d;
143 
144       if (label instanceof NodeLabel) {
145         // calculate a wedge connecting the node and the rounded rectangle around the label text
146         NodeRealizer labelRealizer = ((NodeLabel) label).getRealizer();
147         Node node = ((NodeLabel) label).getNode();
148         Graph2D graph2D = ((Graph2D) node.getGraph());
149         NodeRealizer nodeRealizer = graph2D.getRealizer(node);
150 
151         double tx = graph2D.getCenterX(node);
152         double ty = graph2D.getCenterY(node);
153 
154         // calculate an offset for the tip of the wedge
155         if(!nodeRealizer.contains(cx, cy)) {
156           double dirX = cx - labelRealizer.getCenterX();
157           double dirY = cy - labelRealizer.getCenterY();
158           Point2D result = new Point2D.Double();
159           nodeRealizer.findIntersection(tx, ty, cx, cy, result);
160           double l0 = Math.sqrt(dirX * dirX + dirY * dirY);
161           if(l0 > 0) {
162             double halfNodeWidth = nodeRealizer.getWidth() * 0.5 + 5;
163             halfNodeWidth = (dirX > 0) ? halfNodeWidth : -1.0 * halfNodeWidth;
164             tx = result.getX() + 5 * dirX / l0;
165             ty = result.getY() + 5 * dirY / l0;
166           }
167         }
168 
169         // add the wedge to the bubble shape
170         double dx = cx - tx;
171         double dy = cy - ty;
172         double l = Math.sqrt(dx * dx + dy * dy);
173         if (l > 0) {
174           double size = Math.min(width, height) * 0.25;
175           GeneralPath p = new GeneralPath();
176           p.moveTo((float) tx, (float) ty);
177           p.lineTo((float) (cx + dy * size / l), (float) (cy - dx * size / l));
178           p.lineTo((float) (cx - dy * size / l), (float) (cy + dx * size / l));
179           p.closePath();
180           Area area = new Area(shape);
181           area.add(new Area(p));
182           shape = area;
183         }
184 
185       } else if (label instanceof EdgeLabel) {
186         // calculate an anchor line connecting the edge and the rounded rectangle around the label text
187         Edge edge = ((EdgeLabel) label).getEdge();
188         Graph2D graph2D = ((Graph2D) edge.getGraph());
189         EdgeRealizer edgeRealizer = graph2D.getRealizer(edge);
190         GeneralPath path = edgeRealizer.getPath();
191         double[] result = PointPathProjector.calculateClosestPathPoint(path, cx, cy);
192         double dx = cx - result[0];
193         double dy = cy - result[1];
194         double l = Math.sqrt(dx * dx + dy * dy);
195 
196         // draw the anchor line with an offset to the edge
197         if (l > 0) {
198           double tx = result[0] + 5 * dx / l;
199           double ty = result[1] + 5 * dy / l;
200           Line2D line = new Line2D.Double(cx, cy, tx, ty);
201           gfx.setColor(new Color(0, 0, 0, 64));
202           gfx.draw(line);
203         }
204       }
205 
206       // paint the bubble using the colors of the label
207       Color backgroundColor = label.getBackgroundColor();
208       if (backgroundColor != null) {
209         // shadow
210         gfx.setColor(new Color(0, 0, 0, 64));
211         gfx.translate(5, 5);
212         gfx.fill(shape);
213         gfx.translate(-5, -5);
214         // and background
215         gfx.setColor(backgroundColor);
216         gfx.fill(shape);
217       }
218 
219       // line
220       Color lineColor = label.getLineColor();
221       if (label.isSelected()
222           && YRenderingHints.isSelectionPaintingEnabled(gfx)) {
223         lineColor = SELECTION_COLOR;
224         gfx.setStroke(LineType.LINE_2);
225       }
226       if (lineColor != null) {
227         gfx.setColor(lineColor);
228         gfx.draw(shape);
229       }
230 
231       gfx.setColor(oldColor);
232       gfx.setStroke(oldStroke);
233     }
234 
235   }
236 
237   /** Helper class that provides diverse services related to working with points on a path. */
238   static class PointPathProjector {
239     private PointPathProjector() {
240     }
241 
242     /**
243      * Calculates the point on the path which is closest to the given point. Ties are broken arbitrarily.
244      *
245      * @param path where to look for the closest point
246      * @param px   x coordinate of query point
247      * @param py   y coordinate of query point
248      * @return double[6] <ul> <li>x coordinate of the closest point</li> <li>y coordinate of the closest point</li>
249      *         <li>distance of the closest point to given point</li> <li>index of the segment of the path including the
250      *         closest point (as a double starting with 0.0, segments are computed with a path iterator with flatness
251      *         1.0)</li> <li>ratio of closest point on the the including segment (between 0.0 and 1.0)</li> <li>ratio of
252      *         closest point on the entire path (between 0.0 and 1.0)</li> </ul>
253      */
254     static double[] calculateClosestPathPoint(GeneralPath path, double px, double py) {
255       double[] result = new double[6];
256       YPoint point = new YPoint(px, py);
257       double pathLength = 0;
258 
259       CustomPathIterator pi = new CustomPathIterator(path, 1.0);
260       double[] curSeg = new double[4];
261       double minDist;
262       if (pi.ok()) {
263         curSeg = pi.segment();
264         minDist = YPoint.distance(px, py, curSeg[0], curSeg[1]);
265         result[0] = curSeg[0];
266         result[1] = curSeg[1];
267         result[2] = minDist;
268         result[3] = 0.0;
269         result[4] = 0.0;
270         result[5] = 0.0;
271       } else {
272         // no points in GeneralPath: should not happen in this context
273         throw new IllegalStateException("path without any coordinates");
274       }
275 
276       int segmentIndex = 0;
277       double lastPathLength = 0.0;
278       do {
279         YPoint segmentStart = new YPoint(curSeg[0], curSeg[1]);
280         YPoint segmentEnd = new YPoint(curSeg[2], curSeg[3]);
281         YVector segmentDirection = new YVector(segmentEnd, segmentStart);
282         double segmentLength = segmentDirection.length();
283         pathLength += segmentLength;
284         segmentDirection.norm();
285 
286         AffineLine currentSegment = new AffineLine(segmentStart, segmentDirection);
287         AffineLine throughPoint = new AffineLine(point, YVector.orthoNormal(segmentDirection));
288         YPoint crossing = AffineLine.getCrossing(currentSegment, throughPoint);
289         YVector crossingVector = new YVector(crossing, segmentStart);
290 
291         YVector segmentVector = new YVector(segmentEnd, segmentStart);
292         double indexEnd = YVector.scalarProduct(segmentVector, segmentDirection);
293         double indexCrossing = YVector.scalarProduct(crossingVector, segmentDirection);
294 
295         double dist;
296         double segmentRatio;
297         YPoint nearestOnSegment;
298         if (indexCrossing <= 0.0) {
299           dist = YPoint.distance(point, segmentStart);
300           nearestOnSegment = segmentStart;
301           segmentRatio = 0.0;
302         } else if (indexCrossing >= indexEnd) {
303           dist = YPoint.distance(point, segmentEnd);
304           nearestOnSegment = segmentEnd;
305           segmentRatio = 1.0;
306         } else {
307           dist = YPoint.distance(point, crossing);
308           nearestOnSegment = crossing;
309           segmentRatio = indexCrossing / indexEnd;
310         }
311 
312         if (dist < minDist) {
313           minDist = dist;
314           result[0] = nearestOnSegment.getX();
315           result[1] = nearestOnSegment.getY();
316           result[2] = minDist;
317           result[3] = segmentIndex;
318           result[4] = segmentRatio;
319           result[5] = segmentLength * segmentRatio + lastPathLength;
320         }
321 
322         segmentIndex++;
323         lastPathLength = pathLength;
324         pi.next();
325       } while (pi.ok());
326 
327       if (pathLength > 0) {
328         result[5] = result[5] / pathLength;
329       } else {
330         result[5] = 0.0;
331       }
332       return result;
333     }
334 
335     /** Helper class used by PointPathProjector. */
336     static class CustomPathIterator {
337       private double[] cachedSegment;
338       private boolean moreToGet;
339       private PathIterator pathIterator;
340 
341       public CustomPathIterator(GeneralPath path, double flatness) {
342         // copy the path, thus the original may safely change during iteration
343         pathIterator = (new GeneralPath(path)).getPathIterator(null, flatness);
344         cachedSegment = new double[4];
345         getFirstSegment();
346       }
347 
348       public boolean ok() {
349         return moreToGet;
350       }
351 
352       public final double[] segment() {
353         if (moreToGet) {
354           return cachedSegment;
355         } else {
356           return null;
357         }
358       }
359 
360       public void next() {
361         if (!pathIterator.isDone()) {
362           float[] curSeg = new float[2];
363           cachedSegment[0] = cachedSegment[2];
364           cachedSegment[1] = cachedSegment[3];
365           pathIterator.currentSegment(curSeg);
366           cachedSegment[2] = curSeg[0];
367           cachedSegment[3] = curSeg[1];
368           pathIterator.next();
369         } else {
370           moreToGet = false;
371         }
372       }
373 
374       private void getFirstSegment() {
375         float[] curSeg = new float[2];
376         if (!pathIterator.isDone()) {
377           pathIterator.currentSegment(curSeg);
378           cachedSegment[0] = curSeg[0];
379           cachedSegment[1] = curSeg[1];
380           pathIterator.next();
381           moreToGet = true;
382         } else {
383           moreToGet = false;
384         }
385         if (!pathIterator.isDone()) {
386           pathIterator.currentSegment(curSeg);
387           cachedSegment[2] = curSeg[0];
388           cachedSegment[3] = curSeg[1];
389           pathIterator.next();
390           moreToGet = true;
391         } else {
392           moreToGet = false;
393         }
394       }
395     }
396   }
397 }
398