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 java.awt.BasicStroke;
31  import java.awt.BorderLayout;
32  import java.awt.Color;
33  import java.awt.EventQueue;
34  import java.awt.Graphics2D;
35  import java.awt.Stroke;
36  import java.awt.event.MouseEvent;
37  import java.awt.geom.AffineTransform;
38  import java.awt.geom.Ellipse2D;
39  import java.awt.geom.GeneralPath;
40  import java.awt.geom.Line2D;
41  import java.awt.geom.PathIterator;
42  import java.awt.geom.Point2D;
43  import java.awt.geom.RectangularShape;
44  import java.util.ArrayList;
45  import java.util.Collections;
46  import java.util.Iterator;
47  import java.util.List;
48  import java.util.Locale;
49  import java.util.Map;
50  
51  import javax.swing.Icon;
52  import javax.swing.JList;
53  import javax.swing.JScrollPane;
54  import javax.swing.ListSelectionModel;
55  import javax.swing.border.TitledBorder;
56  import javax.swing.event.ListSelectionEvent;
57  import javax.swing.event.ListSelectionListener;
58  
59  import demo.view.DemoBase;
60  import demo.view.DemoDefaults;
61  
62  import y.base.Edge;
63  import y.base.ListCell;
64  import y.geom.YPoint;
65  import y.geom.YVector;
66  import y.option.RealizerCellRenderer;
67  import y.view.Arrow;
68  import y.view.Bend;
69  import y.view.BendCursor;
70  import y.view.BendList;
71  import y.view.EdgeRealizer;
72  import y.view.FramedEdgePainter;
73  import y.view.GenericEdgePainter;
74  import y.view.GenericEdgeRealizer;
75  import y.view.LineType;
76  import y.view.NodeRealizer;
77  import y.view.PolyLinePathCalculator;
78  import y.view.Port;
79  import y.view.TooltipMode;
80  import y.view.YRenderingHints;
81  
82  /**
83   * This class demonstrates various usages of the {@link y.view.GenericEdgeRealizer} class. <br/> Usage: Try adding new
84   * edges and adding bends to existing edges. The list on the left side allows to switch between the possible edge types
85   * for creating new edges.
86   * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/custom_realizers#customization_ger" target="_blank">Section Writing Customized Realizers</a> in the yFiles for Java Developer's Guide
87   */
88  public class GenericEdgeRealizerDemo extends DemoBase {
89    private static final Color defaultEdgeColor = new Color(202, 227, 255);
90    private static final LineType defaultEdgeLineType = LineType.LINE_3;
91  
92    /**
93     * Creates the GenericEdgeRealizer demo.
94     */
95    public GenericEdgeRealizerDemo() {
96      super();
97  
98      //Add a list with available edge realizers to the panel.
99      TitledBorder border = new TitledBorder("Edge Types:");
100     border.setTitlePosition(TitledBorder.BELOW_TOP);
101     JScrollPane scrollPane = new JScrollPane(createEdgeRealizerList());
102     scrollPane.setBorder(border);
103     contentPane.add(scrollPane, BorderLayout.WEST);
104 
105     loadGraph("resource/genericEdgeRealizer.graphml");
106     DemoDefaults.applyRealizerDefaults(view.getGraph2D(), true, true);
107   }
108 
109   protected void configureDefaultRealizers() {
110     super.configureDefaultRealizers();
111 
112     // Get the factory to register custom styles/configurations.
113     GenericEdgeRealizer.Factory factory = GenericEdgeRealizer.getFactory();
114 
115     // Retrieve a map that holds the default GenericEdgeRealizer configuration.
116     // The implementations contained therein can be replaced one by one in order
117     // to create custom configurations...
118     Map implementationsMap = factory.createDefaultConfigurationMap();
119 
120     // The edge path is painted 3D-ish and with a drop shadow.
121     implementationsMap.put(GenericEdgeRealizer.Painter.class, new CustomEdgePainter());
122     // The path is calculated to be undulating.
123     implementationsMap.put(GenericEdgeRealizer.PathCalculator.class, new UndulatingPathCalculator());
124 
125     // Add the first configuration to the factory.
126     factory.addConfiguration("Undulating", implementationsMap);
127 
128     implementationsMap.put(GenericEdgeRealizer.PathCalculator.class, new MyPathCalculator());
129     // Add the second configuration to the factory.
130     // NB: It uses the same type of painter as the previous configuration.
131     factory.addConfiguration("QuadCurve", implementationsMap);
132 
133     // Special behavior for an otherwise normal poly-line edge path calculator:
134     // first and last segment of the edge path are kept axes-parallel.
135     implementationsMap.put(GenericEdgeRealizer.PathCalculator.class,
136         new PortMoverPathCalculator(new PolyLinePathCalculator()));
137     factory.addConfiguration("PolyLineAxesParallel", implementationsMap);
138 
139     // Default edge painter implementation.
140 
141     implementationsMap = factory.createDefaultConfigurationMap();
142     implementationsMap.put(GenericEdgeRealizer.Painter.class, new GenericEdgePainter());
143     implementationsMap.put(GenericEdgeRealizer.PathCalculator.class, new UndulatingPathCalculator());
144     // Bends are rendered differently depending on their selection state.
145     // - normal rendering: blue ellipse (height is half of width)
146     // - rendering when bend is selected: red ellipse
147     implementationsMap.put(GenericEdgeRealizer.BendPainter.class,
148         new CustomBendPainter(new Ellipse2D.Double(0, 0, 10, 5), new Ellipse2D.Double(0, 0, 10, 10), Color.blue,
149             Color.red));
150     factory.addConfiguration("UndulatingCustomBends", implementationsMap);
151 
152     implementationsMap = factory.createDefaultConfigurationMap();
153     implementationsMap.put(GenericEdgeRealizer.ArrowPainter.class, new CenterArrowPainter());
154     implementationsMap.put(GenericEdgeRealizer.PathCalculator.class, new UnclippedPathCalculator());
155     factory.addConfiguration("Unclipped", implementationsMap);
156 
157     implementationsMap = factory.createDefaultConfigurationMap();
158     MultiArrowPainter arrowPainter = new MultiArrowPainter();
159     implementationsMap.put(GenericEdgeRealizer.ArrowPainter.class, arrowPainter);
160     factory.addConfiguration("MultiArrow", implementationsMap);
161 
162 
163     implementationsMap = factory.createDefaultConfigurationMap();
164     implementationsMap.put(GenericEdgeRealizer.Painter.class, new MultiColorEdgePainter());
165     factory.addConfiguration("MultiColorSegments", implementationsMap);
166 
167     implementationsMap = factory.createDefaultConfigurationMap();
168     implementationsMap.put(GenericEdgeRealizer.Painter.class, new SignatureEdgePainter("foo", 50));
169     factory.addConfiguration("EdgeSignature", implementationsMap);
170 
171     implementationsMap = factory.createDefaultConfigurationMap();
172     implementationsMap.put(GenericEdgeRealizer.Painter.class, new DoubleStrokedPainter());
173     factory.addConfiguration("DoubleStroked", implementationsMap);
174 
175     implementationsMap = factory.createDefaultConfigurationMap();
176     implementationsMap.put(GenericEdgeRealizer.Painter.class, new FramedEdgePainter());
177     factory.addConfiguration("Framed", implementationsMap);
178 
179     implementationsMap = factory.createDefaultConfigurationMap();
180     implementationsMap.put(GenericEdgeRealizer.Painter.class, new DashedFrameEdgePainter());
181     factory.addConfiguration("FramedDashed", implementationsMap);
182 
183     // Initialize the GenericEdgeRealizer instance to one of the types we just
184     // registered with the factory.
185 
186     // Take a default GenericEdgeRealizer...
187     GenericEdgeRealizer ger = new GenericEdgeRealizer();
188     // ... and make it real flashy.
189     ger.setLineType(defaultEdgeLineType);
190     ger.setLineColor(defaultEdgeColor);
191     ger.setConfiguration("Framed");
192     ger.setUserData("This is my own userData object.");
193     view.getGraph2D().setDefaultEdgeRealizer(ger);
194   }
195 
196   protected TooltipMode createTooltipMode() {
197     TooltipMode tooltipMode = new TooltipMode() {
198       /**
199        * Overwrites {@link TooltipMode#getNodeTip(y.base.Node)} to set a tooltip text with the configuration of the
200        * {@link GenericEdgeRealizer} associated with the edge.
201        * @param edge the edge for which the tooltip is set.
202        * @return a tooltip text with the realizers configuration if the edge has a <code>GenericNodeRealizer</code>,
203        * <code>null</code> otherwise.
204        */
205       protected String getEdgeTip(Edge edge) {
206         String tipText = null;
207         if (getGraph2D().getRealizer(edge) instanceof GenericEdgeRealizer) {
208           GenericEdgeRealizer realizer = (GenericEdgeRealizer) getGraph2D().getRealizer(edge);
209           tipText = realizer.getConfiguration();
210         }
211         return tipText;
212       }
213     };
214     tooltipMode.setNodeTipEnabled(false);
215     return tooltipMode;
216   }
217 
218   /**
219    * Creates a JList with different edge realizers.
220    *
221    * @return a JList with edge realizers.
222    */
223   protected JList createEdgeRealizerList() {
224     final List configurations = new ArrayList(GenericEdgeRealizer.getFactory().getAvailableConfigurations());
225     Collections.sort(configurations);
226 
227     // Create a list with different edge realizers.
228     final List edgeRealizers = new ArrayList();
229     for (Iterator iterator = configurations.iterator(); iterator.hasNext(); ) {
230       final String configuration = (String) iterator.next();
231       final GenericEdgeRealizer realizer = new GenericEdgeRealizer(configuration);
232       if ("DoubleStroked".equals(configuration)) {
233         realizer.setLineType(LineType.LINE_2);
234       } else if (configuration.startsWith("Framed")) {
235         realizer.setLineType(LineType.LINE_3);
236         realizer.setTargetArrow(Arrow.STANDARD);
237       } else if (configuration.startsWith("Undulating")) {
238         realizer.setStyleProperty("MyFunnyPathCalculator.Wavelength", new Integer(10));
239       } else {
240         realizer.setLineType(defaultEdgeLineType);
241       }
242       realizer.setLineColor(defaultEdgeColor);
243       edgeRealizers.add(realizer);
244     }
245 
246     // Create a JList with the edge realizers.
247     final JList result = new JList(edgeRealizers.toArray()) {
248       public String getToolTipText(MouseEvent evt) {
249         int index = locationToIndex(evt.getPoint());
250         return (String) configurations.get(index);
251       }
252     };
253 
254     // A special cell renderer is used here to show the edge realizers and its connecting nodes as well.
255     final int CELL_WIDTH = 150;
256     final int CELL_HEIGHT = 60;
257     result.setCellRenderer(new RealizerCellRenderer(CELL_WIDTH, CELL_HEIGHT) {
258       protected Icon createEdgeRealizerIcon(EdgeRealizer realizer, int iconWidth, int iconHeight) {
259         RealizerCellRenderer.EdgeRealizerIcon icon = new RealizerCellRenderer.EdgeRealizerIcon(realizer, iconWidth,
260             iconHeight) {
261           protected void paintRealizer(EdgeRealizer edgeRealizer, Graphics2D gfx) {
262             edgeRealizer.getPath();
263 
264             NodeRealizer sourceNodeRealizer = edgeRealizer.getSourceRealizer();
265             sourceNodeRealizer.setSize(15.0, 15.0);
266             sourceNodeRealizer.setCenter(15.0, calculateSourceBend(edgeRealizer, CELL_WIDTH, CELL_HEIGHT).getY());
267             sourceNodeRealizer.setFillColor(DemoDefaults.DEFAULT_NODE_COLOR);
268             sourceNodeRealizer.setLineColor(DemoDefaults.DEFAULT_NODE_LINE_COLOR);
269             sourceNodeRealizer.setVisible(true);
270             sourceNodeRealizer.paint(gfx);
271 
272             NodeRealizer targetNodeRealizer = edgeRealizer.getTargetRealizer();
273             targetNodeRealizer.setSize(15.0, 15.0);
274             targetNodeRealizer.setCenter(CELL_WIDTH - 15.0,
275                 calculateTargetBend(edgeRealizer, CELL_WIDTH, CELL_HEIGHT).getY());
276             targetNodeRealizer.setFillColor(DemoDefaults.DEFAULT_NODE_COLOR);
277             targetNodeRealizer.setLineColor(DemoDefaults.DEFAULT_NODE_LINE_COLOR);
278             targetNodeRealizer.setVisible(true);
279             targetNodeRealizer.paint(gfx);
280 
281             edgeRealizer.paint(gfx);
282           }
283         };
284         icon.setDrawingBends(false);
285         return icon;
286       }
287     });
288 
289     // Use the selected list item as edge realizer.
290     result.addListSelectionListener(new ListSelectionListener() {
291       public void valueChanged(ListSelectionEvent e) {
292         EdgeRealizer edgeRealizer = (EdgeRealizer) result.getSelectedValue();
293         view.getGraph2D().setDefaultEdgeRealizer(edgeRealizer);
294       }
295     });
296 
297     // Allow only single selection.
298     result.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
299 
300     final int idx = indexOf(edgeRealizers, view.getGraph2D().getDefaultEdgeRealizer());
301     if (-1 < idx && idx < edgeRealizers.size()) {
302       result.setSelectedIndex(idx);
303     } else {
304       result.setSelectedIndex(0);
305     }
306     return result;
307   }
308 
309   /**
310    * Finds the first occurrence of a edge realizer with the same configuration
311    * as the specified edge realizer. 
312    * @param haystack the list of (generic) edge realizer to search in.
313    * @param needle the edge realizer to search for.
314    * @return the index of the first occurrence of a matching edge realizer or
315    * <code>-1</code> if no such occurrence can be found.
316    */
317   private static int indexOf( final List haystack, final EdgeRealizer needle ) {
318     final String c =
319             needle instanceof GenericEdgeRealizer
320             ? ((GenericEdgeRealizer) needle).getConfiguration()
321             : null;
322 
323     int i = 0;
324     for (Iterator it = haystack.iterator(); it.hasNext(); ++i) {
325       final GenericEdgeRealizer next = (GenericEdgeRealizer) it.next();
326       if (c == null ? null == next.getConfiguration() : c.equals(next.getConfiguration())) {
327         return i;
328       }
329     }
330     return -1;
331   }
332 
333   /**
334    * A custom EdgePainter implementation that draws the edge path 3D-ish and adds a drop shadow also.
335    */
336   static final class CustomEdgePainter extends GenericEdgePainter {
337     protected void paintPath(EdgeRealizer context, BendList bends, GeneralPath path, Graphics2D gfx, boolean selected) {
338       Stroke s = gfx.getStroke();
339       Color oldColor = gfx.getColor();
340       if (s instanceof BasicStroke) {
341         Color c;
342         if (selected) {
343           initializeSelectionLine(context, gfx, selected);
344           c = gfx.getColor();
345         } else {
346           initializeLine(context, gfx, selected);
347           c = gfx.getColor();
348           gfx.setColor(new Color(0, 0, 0, 64));
349           gfx.translate(4, 4);
350           gfx.draw(path);
351           gfx.translate(-4, -4);
352         }
353         Color newC = selected ? Color.RED : c;
354         gfx.setColor(new Color(128 + newC.getRed() / 2, 128 + newC.getGreen() / 2, 128 + newC.getBlue() / 2));
355         gfx.translate(-1, -1);
356         gfx.draw(path);
357         gfx.setColor(new Color(newC.getRed() / 2, newC.getGreen() / 2, newC.getBlue() / 2));
358         gfx.translate(2, 2);
359         gfx.draw(path);
360         gfx.translate(-1, -1);
361         gfx.setColor(c);
362         gfx.draw(path);
363         gfx.setColor(oldColor);
364       } else {
365         gfx.draw(path);
366       }
367     }
368   }
369 
370   static final class MultiColorEdgePainter extends GenericEdgePainter {
371     protected void paintPath(EdgeRealizer context, BendList bends, GeneralPath path, Graphics2D gfx, boolean selected) {
372       Color oldColor = gfx.getColor();
373       if (selected) {
374         initializeSelectionLine(context, gfx, selected);
375       } else {
376         initializeLine(context, gfx, selected);
377       }
378       double[] segmentCoords = new double[6];
379       PathIterator iterator = path.getPathIterator(new AffineTransform());
380       double lastX = 0;
381       double lastY = 0;
382       final Line2D.Double doubleLine = new Line2D.Double();
383       while (!iterator.isDone()) {
384         int type = iterator.currentSegment(segmentCoords);
385         switch (type) {
386           case PathIterator.SEG_MOVETO:
387             lastX = segmentCoords[0];
388             lastY = segmentCoords[1];
389             break;
390           case PathIterator.SEG_LINETO:
391             doubleLine.x1 = lastX;
392             doubleLine.y1 = lastY;
393             doubleLine.x2 = segmentCoords[0];
394             doubleLine.y2 = segmentCoords[1];
395             gfx.draw(doubleLine);
396             lastX = segmentCoords[0];
397             lastY = segmentCoords[1];
398             gfx.setColor(getNextColor(gfx.getColor()));
399             break;
400           default:
401             break;
402         }
403         iterator.next();
404       }
405       gfx.setColor(oldColor);
406     }
407 
408     /**
409      * Calculates the color for the next edge segment.
410      * <br/>
411      * This implementation returns the next darker version of the current color.
412      * <br/>
413      * If the color shall change in a different way, this method needs to be overwritten.
414      *
415      * @param currentColor the color of the current segment
416      * @return the color of the next segment
417      */
418     protected Color getNextColor(Color currentColor) {
419       float[] colorInHsb = Color.RGBtoHSB(currentColor.getRed(), currentColor.getGreen(), currentColor.getBlue(), null);
420       final float step = 0.07f;
421       return Color.getHSBColor(colorInHsb[0], colorInHsb[1], Math.max(0, colorInHsb[2] - step));
422     }
423   }
424 
425   /**
426    * Simple painter that displays a double stroked poly-line edge.
427    */
428   static final class DoubleStrokedPainter extends GenericEdgePainter {
429     protected void renderPath(
430             final EdgeRealizer context,
431             final Graphics2D gfx,
432             final GeneralPath path,
433             final boolean selected
434     ) {
435       final PathIterator it = path.getPathIterator(null);
436       if (!it.isDone()) {
437         final Line2D.Double line = new Line2D.Double();
438         final double[] buffer = new double[6];
439 
440         while (!it.isDone()) {
441           switch (it.currentSegment(buffer)) {
442             case PathIterator.SEG_MOVETO:
443               line.x2 = buffer[0];
444               line.y2 = buffer[1];
445               break;
446             case PathIterator.SEG_LINETO:
447               line.x2 = buffer[0];
448               line.y2 = buffer[1];
449               renderSegment(gfx, line);
450               break;
451             case PathIterator.SEG_QUADTO:
452               // ignore curve, draw a line segment to the curve's endpoint
453               line.x2 = buffer[2];
454               line.y2 = buffer[3];
455               renderSegment(gfx, line);
456               break;
457             case PathIterator.SEG_CUBICTO:
458               // ignore curve, draw a line segment to the curve's endpoint
459               line.x2 = buffer[4];
460               line.y2 = buffer[5];
461               renderSegment(gfx, line);
462               break;
463 //            case PathIterator.SEG_CLOSE:
464 //              // path should not be closed
465 //              break;
466           }
467 
468           it.next();
469           line.x1 = line.x2;
470           line.y1 = line.y2;
471         }
472       }
473     }
474 
475     /**
476      * Renders a single line segment in a double stroked way (if possible).
477      * @param gfx the graphics context to draw upon.
478      * @param line the line segment to draw.
479      */
480     private void renderSegment( final Graphics2D gfx, final Line2D.Double line ) {
481       final double x1 = line.x1;
482       final double y1 = line.y1;
483       final double x2 = line.x2;
484       final double y2 = line.y2;
485 
486       try {
487         final double ox = y2 - y1;
488         final double oy = x1 - x2;
489         final double ol = Math.sqrt(ox * ox + oy * oy);
490 
491         if (ol > 0) {
492           final double xOffset = (2 * ox) / ol;
493           final double yOffset = (2 * oy) / ol;
494           line.x1 = x1 + xOffset;
495           line.y1 = y1 + yOffset;
496           line.x2 = x2 + xOffset;
497           line.y2 = y2 + yOffset;
498           gfx.draw(line);
499           line.x1 = x1 - xOffset;
500           line.y1 = y1 - yOffset;
501           line.x2 = x2 - xOffset;
502           line.y2 = y2 - yOffset;
503           gfx.draw(line);
504         } else {
505           gfx.draw(line);
506         }
507       } finally {
508         line.x1 = x1;
509         line.y1 = y1;
510         line.x2 = x2;
511         line.y2 = y2;
512       }
513     }
514   }
515   
516   static final class SignatureEdgePainter extends GenericEdgePainter {
517     private String signature;
518     private double distance;
519 
520     public SignatureEdgePainter(String signature, double distance) {
521       this.signature = signature;
522       if (distance <= 0d) {
523         throw new IllegalArgumentException("distance <= 0");
524       }
525       this.distance = distance;
526     }
527 
528     protected void paintPath(EdgeRealizer context, BendList bends, GeneralPath path, Graphics2D gfx, boolean selected) {
529       // Draw path "normally".
530       super.paintPath(context, bends, path, gfx, selected);
531 
532       // Add signatures.
533       Color oldColor = gfx.getColor();
534       gfx.setColor(Color.BLACK);
535       double length = getPathLength(path);
536       int numberOfSignatures = (int) Math.floor(length / distance);
537       if (numberOfSignatures < 1) {
538         return;
539       }
540       double slack = length - numberOfSignatures * distance;
541       double distToGo = 0.5 * (slack + distance);
542       double currentLength = 0.0;
543       for (CustomPathIterator pi = new CustomPathIterator(path, 1.0); pi.ok(); pi.next()) {
544         YPoint segmentStart = pi.segmentStart();
545         YVector segmentDirection = pi.segmentDirection();
546         double segmentLength = segmentDirection.length();
547         while (distToGo < segmentLength && currentLength + distToGo < length) {
548           segmentDirection.scale(distToGo / segmentLength);
549           YPoint location = segmentStart.moveBy(segmentDirection.getX(), segmentDirection.getY());
550           AffineTransform oldTransform = gfx.getTransform();
551           AffineTransform newTransform = gfx.getTransform();
552           double theta = Math.atan2(segmentDirection.getY(), segmentDirection.getX());
553           newTransform.rotate(theta, location.getX(), location.getY());
554           gfx.setTransform(newTransform);
555           gfx.drawString(signature, (float) location.getX(), (float) location.getY());
556           gfx.setTransform(oldTransform);
557           segmentDirection = pi.segmentDirection();
558           distToGo += distance;
559         }
560         distToGo -= segmentLength;
561         currentLength += segmentLength;
562       }
563       gfx.setColor(oldColor);
564     }
565   }
566 
567   /**
568    * Displays a framed edge with a dash pattern along the edge path.
569    */
570   public static final class DashedFrameEdgePainter extends FramedEdgePainter {
571     protected void paintForeground(
572             final EdgeRealizer context,
573             final Graphics2D gfx,
574             final GeneralPath path, final GeneralPath outline,
575             final boolean selected
576     ) {
577       final LineType s = context.getLineType();
578       gfx.setStroke(createInnerPattern(s.getLineWidth()));
579       gfx.draw(path);
580 
581       super.paintForeground(context, gfx, path, outline, selected);
582     }
583 
584     /**
585      * Creates a dashed stroke with the specified width. The dash pattern
586      * depends on the stroke width.
587      * @param w the width of the new stroke.
588      * @return a dashed stroke with the specified width.
589      */
590     protected BasicStroke createInnerPattern( final float w ) {
591       final float dash = w * 1.5f;
592       return new BasicStroke(
593               w,
594               BasicStroke.CAP_BUTT,
595               BasicStroke.JOIN_MITER,
596               2,
597               new float[]{dash, w*2},
598               dash);
599     }
600   }
601 
602   /**
603    * A custom PathCalculator implementation that keeps the first and last segment of an edge path axes-parallel. To
604    * achieve this behavior, the edge's source port and target port are moved to match any movement of the bend at the
605    * opposite of the respective segment.
606    * <p/>
607    * If the edge path has only a single segment, it is drawn axes-parallel as soon as the projections of the two nodes
608    * overlap on either x-axis or y-axis.
609    */
610   static final class PortMoverPathCalculator implements GenericEdgeRealizer.PathCalculator {
611     private GenericEdgeRealizer.PathCalculator innerCalculator;
612 
613     PortMoverPathCalculator(GenericEdgeRealizer.PathCalculator innerCalculator) {
614       this.innerCalculator = innerCalculator;
615     }
616 
617     public byte calculatePath(EdgeRealizer context, BendList bends, GeneralPath path,
618                               Point2D sourceIntersectionPointOut,
619                               Point2D targetIntersectionPointOut) {
620       final Port sp = context.getSourcePort();
621       final Port tp = context.getTargetPort();
622       final NodeRealizer snr = context.getSourceRealizer();
623       final NodeRealizer tnr = context.getTargetRealizer();
624       if (bends.size() > 0) {
625         adjustPort(bends.firstBend(), snr, sp);
626         adjustPort((Bend) bends.last(), tnr, tp);
627       } else {
628         double minx = Math.max(snr.getX(), tnr.getX());
629         double maxx = Math.min(snr.getX() + snr.getWidth(), tnr.getX() + tnr.getWidth());
630         if (maxx >= minx) {
631           double pos = (minx + maxx) * 0.5d;
632           sp.setOffsetX(pos - snr.getCenterX());
633           tp.setOffsetX(pos - tnr.getCenterX());
634         }
635         double miny = Math.max(snr.getY(), tnr.getY());
636         double maxy = Math.min(snr.getY() + snr.getHeight(), tnr.getY() + tnr.getHeight());
637         if (maxy >= miny) {
638           double pos = (miny + maxy) * 0.5d;
639           sp.setOffsetY(pos - snr.getCenterY());
640           tp.setOffsetY(pos - tnr.getCenterY());
641         }
642       }
643       return innerCalculator.calculatePath(context, bends, path, sourceIntersectionPointOut,
644           targetIntersectionPointOut);
645     }
646 
647     private void adjustPort(Bend b, NodeRealizer realizer, Port port) {
648       double x = b.getX();
649       double y = b.getY();
650       boolean inXRange = x >= realizer.getX() && x <= realizer.getX() + realizer.getWidth();
651       boolean inYRange = y >= realizer.getY() && y <= realizer.getY() + realizer.getHeight();
652       if (inXRange && !inYRange) {
653         port.setOffsetX(x - realizer.getCenterX());
654       }
655       if (inYRange && !inXRange) {
656         port.setOffsetY(y - realizer.getCenterY());
657       }
658     }
659   }
660 
661   /**
662    * A custom PathCalculator implementation that draws a quad curve edge path.
663    */
664   static final class MyPathCalculator extends PolyLinePathCalculator implements GenericEdgeRealizer.PathCalculator {
665     private final GeneralPath scratch = new GeneralPath();
666 
667 
668     public byte calculatePath(EdgeRealizer context, BendList bends, GeneralPath path,
669                               Point2D sourceIntersectionPointOut,
670                               Point2D targetIntersectionPointOut) {
671       if (bends.size() == 0) {
672         return super.calculatePath(context, bends, path, sourceIntersectionPointOut, targetIntersectionPointOut);
673       } else {
674         final int npoints = bends.size();
675 
676         path.reset();
677         scratch.reset();
678 
679         NodeRealizer nr = context.getSourceRealizer();
680         Port pp = context.getSourcePort();
681         float lastPointx;
682         float lastPointy;
683         float secondLastPointx;
684         float secondLastPointy;
685         scratch.moveTo(lastPointx = (float) pp.getX(nr), lastPointy = (float) pp.getY(nr));
686 
687         int index = 0;
688 
689         secondLastPointx = lastPointx;
690         secondLastPointy = lastPointy;
691 
692         BendCursor bc = bends.bends();
693 
694         {
695           Bend b = bc.bend();
696           lastPointx = (float) b.getX();
697           lastPointy = (float) b.getY();
698           bc.next();
699           index++;
700         }
701 
702         for (; index < npoints; bc.next(), index++) {
703           Bend b = bc.bend();
704           float nextPointx = (float) b.getX();
705           float nextPointy = (float) b.getY();
706           {
707             final float sx = 0.5f * lastPointx + secondLastPointx * 0.5f;
708             final float sy = 0.5f * lastPointy + secondLastPointy * 0.5f;
709             scratch.lineTo(sx, sy);
710           }
711           {
712             final float sx = 0.5f * nextPointx + lastPointx * 0.5f;
713             final float sy = 0.5f * nextPointy + lastPointy * 0.5f;
714             scratch.quadTo(lastPointx, lastPointy, sx, sy);
715             secondLastPointx = lastPointx;
716             secondLastPointy = lastPointy;
717             lastPointx = nextPointx;
718             lastPointy = nextPointy;
719           }
720         }
721 
722         nr = context.getTargetRealizer();
723         pp = context.getTargetPort();
724 
725         {
726           float nextPointx = (float) pp.getX(nr);
727           float nextPointy = (float) pp.getY(nr);
728           {
729             final float sx = 0.5f * lastPointx + secondLastPointx * 0.5f;
730             final float sy = 0.5f * lastPointy + secondLastPointy * 0.5f;
731             scratch.lineTo(sx, sy);
732           }
733           {
734             final float sx = 0.5f * nextPointx + lastPointx * 0.5f;
735             final float sy = 0.5f * nextPointy + lastPointy * 0.5f;
736             scratch.quadTo(lastPointx, lastPointy, sx, sy);
737           }
738           scratch.lineTo(nextPointx, nextPointy);
739         }
740         path.append(scratch.getPathIterator(null, 1.0), false);
741       }
742       return EdgeRealizer.calculateClippingAndIntersection(context, path, path, sourceIntersectionPointOut,
743           targetIntersectionPointOut);
744     }
745   }
746 
747   /**
748    * A custom PathCalculator implementation that draws an undulating edge path.
749    */
750   static final class UndulatingPathCalculator extends PolyLinePathCalculator implements GenericEdgeRealizer.PathCalculator {
751     private final GeneralPath scratch = new GeneralPath();
752 
753     public byte calculatePath(EdgeRealizer context, BendList bends, GeneralPath path,
754                               Point2D sourceIntersectionPointOut,
755                               Point2D targetIntersectionPointOut) {
756       scratch.reset();
757 
758       NodeRealizer nr = context.getSourceRealizer();
759       Port pp = context.getSourcePort();
760       float lastPointX;
761       float lastPointY;
762       scratch.moveTo(lastPointX = (float) pp.getX(nr), lastPointY = (float) pp.getY(nr));
763 
764       int wobbleCount = 0;
765       for (BendCursor bc = bends.bends(); bc.ok(); bc.next()) {
766         Bend b = bc.bend();
767         float nextPointX = (float) b.getX();
768         float nextPointY = (float) b.getY();
769         float dx = nextPointX - lastPointX;
770         float dy = nextPointY - lastPointY;
771         float len = (float) Math.sqrt(dx * dx + dy * dy);
772         if (len > 0) {
773           int count = (int) (len / getWavelength(context)) + 1;
774           for (int i = 0; i < count; i++) {
775             final float height = wobbleCount % 2 == 0 ? 10 : -10;
776             wobbleCount++;
777             scratch.quadTo(lastPointX + (i + 0.5f) / ((float) count) * dx + dy * height / len,
778                 lastPointY + (i + 0.5f) / ((float) count) * dy - dx * height / len,
779                 lastPointX + (i + 1) / ((float) count) * dx, lastPointY + (i + 1) / ((float) count) * dy);
780           }
781         } else {
782           scratch.lineTo(nextPointX, nextPointY);
783         }
784         lastPointX = nextPointX;
785         lastPointY = nextPointY;
786       }
787 
788       nr = context.getTargetRealizer();
789       pp = context.getTargetPort();
790 
791       {
792         float nextPointX = (float) pp.getX(nr);
793         float nextPointY = (float) pp.getY(nr);
794         float dx = nextPointX - lastPointX;
795         float dy = nextPointY - lastPointY;
796         float len = (float) Math.sqrt(dx * dx + dy * dy);
797         if (len > 0) {
798           int count = (int) (len / getWavelength(context)) + 1;
799           for (int i = 0; i < count; i++) {
800             final float height = wobbleCount % 2 == 0 ? 10 : -10;
801             wobbleCount++;
802             scratch.quadTo(lastPointX + (i + 0.5f) / ((float) count) * dx + dy * height / len,
803                 lastPointY + (i + 0.5f) / ((float) count) * dy - dx * height / len,
804                 lastPointX + (i + 1) / ((float) count) * dx, lastPointY + (i + 1) / ((float) count) * dy);
805           }
806         } else {
807           scratch.lineTo(nextPointX, nextPointY);
808         }
809       }
810       path.reset();
811       return EdgeRealizer.calculateClippingAndIntersection(context, scratch, path, sourceIntersectionPointOut,
812           targetIntersectionPointOut);
813     }
814 
815     protected double getWavelength(EdgeRealizer context) {
816       Object o = ((GenericEdgeRealizer) context).getStyleProperty("MyFunnyPathCalculator.Wavelength");
817       if (o instanceof Number) {
818         return ((Number) o).doubleValue();
819       }
820       return 30;
821     }
822   }
823 
824 
825   /**
826    * A custom BendPainter implementation that renders bends differently depending on the bend's selection state, but
827    * also the edge's selection state.
828    */
829   static final class CustomBendPainter implements GenericEdgeRealizer.BendPainter {
830     private RectangularShape shape;
831     private RectangularShape selectedShape;
832     private Color fillColor;
833     private Color selectedFillColor;
834 
835     CustomBendPainter(RectangularShape shape, RectangularShape selectedShape, Color fillColor,
836                       Color selectedFillColor) {
837       this.selectedShape = selectedShape;
838       this.shape = shape;
839       this.fillColor = fillColor;
840       this.selectedFillColor = selectedFillColor;
841     }
842 
843     public void paintBends(EdgeRealizer context, BendList bends, GeneralPath path, Graphics2D gfx, boolean selected) {
844       if (!bends.isEmpty()) {
845         final boolean useSelectionStyle = YRenderingHints.isSelectionPaintingEnabled(gfx);
846 
847         final Color oldColor = gfx.getColor();
848         for (BendCursor bendCursor = bends.bends(); bendCursor.ok(); bendCursor.next()) {
849           Bend b = bendCursor.bend();
850           gfx.setColor(((selected || b.isSelected()) && useSelectionStyle) ? this.selectedFillColor : this.fillColor);
851           final double x = b.getX();
852           final double y = b.getY();
853           RectangularShape shape = selected ? this.selectedShape : this.shape;
854           shape.setFrame(x - shape.getWidth() / 2, y - shape.getHeight() / 2, shape.getWidth(), shape.getHeight());
855           gfx.fill(shape);
856         }
857         gfx.setColor(oldColor);
858       }
859     }
860   }
861 
862   /**
863    * A simple ArrowPainter implementation that paints the arrow at the center of the segment in the middle of the
864    * poly-line control path.
865    */
866   public static final class CenterArrowPainter implements GenericEdgeRealizer.ArrowPainter {
867     public void paintArrows(EdgeRealizer context, BendList bends, GeneralPath path, Graphics2D gfx) {
868       Arrow targetArrow = context.getTargetArrow();
869       if (targetArrow != null) {
870 
871         Point2D sourceIntersection = context.getSourceIntersection();
872         Point2D targetIntersection = context.getTargetIntersection();
873 
874         if (bends.size() > 0) {
875           int mid = bends.size() / 2;
876           if (mid > 0) {
877             Bend bend = context.getBend(mid - 1);
878             sourceIntersection.setLocation(bend.getX(), bend.getY());
879           }
880           {
881             Bend bend = context.getBend(mid);
882             targetIntersection.setLocation(bend.getX(), bend.getY());
883           }
884         }
885 
886         double centerX = (targetIntersection.getX() + sourceIntersection.getX()) * 0.5d;
887         double centerY = (targetIntersection.getY() + sourceIntersection.getY()) * 0.5d;
888         double dx = (targetIntersection.getX() - sourceIntersection.getX());
889         double dy = (targetIntersection.getY() - sourceIntersection.getY());
890         double l = Math.sqrt(dx * dx + dy * dy);
891         double arrowScaleFactor = context.getArrowScaleFactor();
892         if (l > 0) {
893           targetArrow.paint(gfx, centerX, centerY, arrowScaleFactor * dx / l, arrowScaleFactor * dy / l);
894         }
895       }
896     }
897   }
898 
899   /**
900    * An ArrowPainter implementation that paints an arrow at the center of each long enough segment of an edge. The edge
901    * is assumed to be a straight line edge.
902    */
903   public static final class MultiArrowPainter implements GenericEdgeRealizer.ArrowPainter {
904     private double threshold = 50d;
905     private Arrow arrow = Arrow.DELTA;
906     private Color color = Color.LIGHT_GRAY;
907 
908     /**
909      * returns the arrow used for drawing
910      * 
911      * @return the arrow used for drawing
912      */
913     public Arrow getArrow() {
914       return arrow;
915     }
916 
917     /**
918      * sets the arrow used for drawing
919      *
920      * @param arrow an arrow
921      */
922     public void setArrow(Arrow arrow) {
923       this.arrow = arrow;
924     }
925 
926 
927     /**
928      * returns the color used for drawing the arrows
929      *
930      * @return the color of the arrows
931      */
932     public Color getColor() {
933       return color;
934     }
935 
936     /**
937      * sets the color used for drawing the arrows
938      *
939      * @param color a color
940      */
941     public void setColor(Color color) {
942       this.color = color;
943     }
944 
945     /**
946      * paints arrows at each segment of an edge, which is long enough
947      *
948      * @param context the realizer of the edge
949      * @param bends   the bends of the edge
950      * @param path    the path of the edge
951      * @param gfx     the graphics to paint on
952      * @see #setColor(Color)
953      * @see #setArrow(Arrow)
954      */
955     public void paintArrows(EdgeRealizer context, BendList bends, GeneralPath path, Graphics2D gfx) {
956       if (arrow != null) {
957 
958         PathIterator iter = path.getPathIterator(null, 1);
959         double[] curSeg = new double[2];
960         if (!iter.isDone()) {
961           iter.currentSegment(curSeg);
962           Point2D p1 = new Point2D.Double(curSeg[0], curSeg[1]);
963           Point2D p0 = new Point2D.Double();
964           for (iter.next(); !iter.isDone(); iter.next()) {
965             p0.setLocation(p1);
966             iter.currentSegment(curSeg);
967             p1.setLocation(curSeg[0], curSeg[1]);
968             paintArrow(p1, p0, context, gfx);
969           }
970         }
971       }
972     }
973 
974     private void paintArrow(Point2D p1, Point2D p0, EdgeRealizer context, Graphics2D gfx) {
975       if (arrow != null) {
976         double centerX = (p1.getX() + p0.getX()) * 0.5d;
977         double centerY = (p1.getY() + p0.getY()) * 0.5d;
978         double dx = (p1.getX() - p0.getX());
979         double dy = (p1.getY() - p0.getY());
980         double l = Math.sqrt(dx * dx + dy * dy);
981         double dxNormalized = dx / l;
982         double dyNormalized = dy / l;
983         if (l > threshold) {
984           double arrowScaleFactor = context.getArrowScaleFactor();
985           double offset = arrowScaleFactor * (arrow.getArrowLength() + arrow.getClipLength()) * 0.5d;
986           double x = centerX + offset * dxNormalized;
987           double y = centerY + offset * dyNormalized;
988           Color oldColor = gfx.getColor();
989           gfx.setColor(color);
990           arrow.paint(gfx, x, y, arrowScaleFactor * dxNormalized, arrowScaleFactor * dyNormalized);
991           gfx.setColor(oldColor);
992         }
993       }
994     }
995   }
996 
997   /**
998    * A simple custom PathCalculator implementation that performs no clipping of the ends at the adjacent nodes.
999    */
1000  public static final class UnclippedPathCalculator implements GenericEdgeRealizer.PathCalculator {
1001    public byte calculatePath(EdgeRealizer context, BendList bends, GeneralPath path,
1002                              Point2D sourceIntersectionPointOut,
1003                              Point2D targetIntersectionPointOut) {
1004      sourceIntersectionPointOut.setLocation(context.getSourcePort().getX(context.getSourceRealizer()),
1005          context.getSourcePort().getY(context.getSourceRealizer()));
1006      targetIntersectionPointOut.setLocation(context.getTargetPort().getX(context.getTargetRealizer()),
1007          context.getTargetPort().getY(context.getTargetRealizer()));
1008      path.reset();
1009      path.moveTo((float) sourceIntersectionPointOut.getX(), (float) sourceIntersectionPointOut.getY());
1010      for (ListCell cell = bends.firstCell(); cell != null; cell = cell.succ()) {
1011        Bend b = (Bend) cell.getInfo();
1012        path.lineTo((float) b.getX(), (float) b.getY());
1013      }
1014      path.lineTo((float) targetIntersectionPointOut.getX(), (float) targetIntersectionPointOut.getY());
1015
1016      return EdgeRealizer.PATH_CLIPPED_AT_SOURCE_AND_TARGET;
1017    }
1018  }
1019
1020  /**
1021   * Launcher method. Execute this class to see sample instantiations of {@link GenericEdgeRealizer} in action.
1022   */
1023  public static void main(String[] args) {
1024    EventQueue.invokeLater(new Runnable() {
1025      public void run() {
1026        Locale.setDefault(Locale.ENGLISH);
1027        initLnF();
1028        new GenericEdgeRealizerDemo().start("GenericEdgeRealizer Demo");
1029      }
1030    });
1031  }
1032
1033  private static double getPathLength(GeneralPath path) {
1034    double length = 0.0;
1035    for (CustomPathIterator pi = new CustomPathIterator(path, 1.0); pi.ok(); pi.next()) {
1036      length += pi.segmentDirection().length();
1037    }
1038    return length;
1039  }
1040
1041  /**
1042   * This class iterates over the segments in a flattened general path.
1043   */
1044  static class CustomPathIterator {
1045    private double[] cachedSegment;
1046    private boolean moreToGet;
1047    private PathIterator pathIterator;
1048
1049    public CustomPathIterator(GeneralPath path, double flatness) {
1050      // copy the path, thus the original may safely change during iteration
1051      pathIterator = (new GeneralPath(path)).getPathIterator(new AffineTransform(), flatness);
1052      cachedSegment = new double[4];
1053      getFirstSegment();
1054    }
1055
1056    public boolean ok() {
1057      return moreToGet;
1058    }
1059
1060    public boolean isDone() {
1061      return !moreToGet;
1062    }
1063
1064    public final double[] segment() {
1065      if (moreToGet) {
1066        return cachedSegment;
1067      } else {
1068        return null;
1069      }
1070    }
1071
1072    public YPoint segmentStart() {
1073      if (moreToGet) {
1074        return new YPoint(cachedSegment[0], cachedSegment[1]);
1075      } else {
1076        return null;
1077      }
1078    }
1079
1080    public YPoint segmentEnd() {
1081      if (moreToGet) {
1082        return new YPoint(cachedSegment[2], cachedSegment[3]);
1083      } else {
1084        return null;
1085      }
1086    }
1087
1088    public YVector segmentDirection() {
1089      if (moreToGet) {
1090        return new YVector(segmentEnd(), segmentStart());
1091      } else {
1092        return null;
1093      }
1094    }
1095
1096    public void next() {
1097      if (!pathIterator.isDone()) {
1098        float[] curSeg = new float[6];
1099        cachedSegment[0] = cachedSegment[2];
1100        cachedSegment[1] = cachedSegment[3];
1101        pathIterator.currentSegment(curSeg);
1102        cachedSegment[2] = curSeg[0];
1103        cachedSegment[3] = curSeg[1];
1104        pathIterator.next();
1105      } else {
1106        moreToGet = false;
1107      }
1108    }
1109
1110    private void getFirstSegment() {
1111      float[] curSeg = new float[6];
1112      if (!pathIterator.isDone()) {
1113        pathIterator.currentSegment(curSeg);
1114        cachedSegment[0] = curSeg[0];
1115        cachedSegment[1] = curSeg[1];
1116        pathIterator.next();
1117        moreToGet = true;
1118      } else {
1119        moreToGet = false;
1120      }
1121      if (!pathIterator.isDone()) {
1122        pathIterator.currentSegment(curSeg);
1123        cachedSegment[2] = curSeg[0];
1124        cachedSegment[3] = curSeg[1];
1125        pathIterator.next();
1126        moreToGet = true;
1127      } else {
1128        moreToGet = false;
1129      }
1130    }
1131  }
1132}
1133