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