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