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.advanced;
15  
16  import demo.view.DemoBase;
17  import y.base.Edge;
18  import y.base.EdgeCursor;
19  import y.base.EdgeList;
20  import y.base.Graph;
21  import y.base.GraphEvent;
22  import y.base.GraphListener;
23  import y.base.Node;
24  import y.base.NodeCursor;
25  import y.base.NodeList;
26  import y.base.YList;
27  import y.geom.AffineLine;
28  import y.geom.YPoint;
29  import y.geom.YVector;
30  import y.util.Tuple;
31  import y.view.Bend;
32  import y.view.BendCursor;
33  import y.view.BendList;
34  import y.view.CreateEdgeMode;
35  import y.view.DefaultGraph2DRenderer;
36  import y.view.EditMode;
37  import y.view.Graph2D;
38  import y.view.Graph2DViewActions;
39  import y.view.HitInfo;
40  import y.view.MovePortMode;
41  import y.view.MoveSelectionMode;
42  import y.view.NodeRealizer;
43  import y.view.Port;
44  import y.view.ShapeNodeRealizer;
45  import y.view.Util;
46  
47  import java.awt.Color;
48  import java.awt.Graphics2D;
49  import java.awt.geom.GeneralPath;
50  import java.awt.geom.PathIterator;
51  import java.awt.geom.Point2D;
52  import java.util.Iterator;
53  import java.util.HashMap;
54  import java.util.Map;
55  import java.util.WeakHashMap;
56  
57  import javax.swing.ActionMap;
58  import javax.swing.InputMap;
59  import javax.swing.JComponent;
60  
61  /**
62   * Class that shows how to mimic node-to-edge and edge-to-edge connections. In this demo an edge that connects
63   * to a node or to another edge is modeled as a normal edge that has a special node as its end point. That special
64   * node is located on the path of the edge. When moving the edge path the special node will also be moved. Thus,
65   * it looks and feels like a proper edge connection to an edge.
66   * <p>
67   * Usage: to create an edge that starts at another edge, shift-press on the edge to initiate the
68   * edge creation gesture, then drag the mouse. To create an edge that ends at another edge,
69   * shift-release the mouse on the edge.
70   * </p>
71   */
72  public class EdgeConnectorDemo extends DemoBase {
73    protected void initialize() {
74      super.initialize();
75      view.setAntialiasedPainting(true);
76      EdgeConnectorGraph2DRenderer r = new EdgeConnectorGraph2DRenderer();
77      r.setDrawEdgesFirst(true);
78      view.setGraph2DRenderer(r);
79  
80      view.getGraph2D().addGraphListener(new EdgeConnectorListener());
81    }
82  
83    protected void registerViewModes() {
84      EditMode editMode = new EdgeConnectorEditMode();
85      editMode.setCreateEdgeMode(new CreateEdgeConnectorMode());
86      editMode.setMoveSelectionMode(new EdgeConnectorMoveSelectionMode());
87      editMode.setMovePortMode(new EdgeConnectorMovePortMode());
88      view.addViewMode(editMode);
89    }
90  
91    /**
92     * Special Graph2DRenderer that updates the edge connector locations before graph elements
93     * are rendered to the view.
94     */
95    static class EdgeConnectorGraph2DRenderer extends DefaultGraph2DRenderer {
96      public void paint(final Graphics2D gfx, final Graph2D graph) {
97        for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
98          Node n = nc.node();
99          if(graph.getRealizer(n) instanceof EdgeConnectorRealizer) {
100           EdgeConnectorRealizer edgeConnectorRealizer = (EdgeConnectorRealizer) graph.getRealizer(n);
101           edgeConnectorRealizer.updateLocation();
102         }
103       }
104       super.paint(gfx, graph);
105     }
106   }
107 
108   protected void registerViewActions() {
109     //register keyboard actions
110     Graph2DViewActions actions = new Graph2DViewActions(view);
111     ActionMap amap = actions.createActionMap();
112     InputMap imap = actions.createDefaultInputMap(amap);
113     if (!isDeletionEnabled()) {
114       amap.remove(Graph2DViewActions.DELETE_SELECTION);
115     }
116     view.getCanvasComponent().setActionMap(amap);
117     view.getCanvasComponent().setInputMap(JComponent.WHEN_FOCUSED, imap);
118   }
119 
120   /**
121    * Manages edge-to-edge dependency information.
122    */
123   static class EdgeConnectorManager {
124     static final Map map = new WeakHashMap();
125 
126     private EdgeConnectorManager() {
127     }
128 
129     static void addEdgeConnection(Node connector, Edge edge, double pathRatio) {
130       map.put(connector, Tuple.create(edge, new Double(pathRatio)));
131     }
132 
133     static Edge getEdgeConnection(Node connector) {
134       Tuple tuple = (Tuple) map.get(connector);
135       if (tuple != null) {
136         return (Edge) tuple.o1;
137       }
138       return null;
139     }
140 
141     static double getEdgeConnectionRatio(Node connector) {
142       Tuple tuple = (Tuple) map.get(connector);
143       if (tuple != null) {
144         return ((Double) tuple.o2).doubleValue();
145       }
146       return 0.0;  //should throw an exception
147     }
148 
149     static NodeList getConnectorNodes(Edge edge) {
150       NodeList result = new NodeList();
151       for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
152         Map.Entry entry = (Map.Entry) iter.next();
153         Tuple value = (Tuple) entry.getValue();
154         if (value.o1 == edge) {
155           result.add(entry.getKey());
156         }
157       }
158       return result;
159     }
160   }
161 
162   /**
163    * Graph listener that automatically removes edges that connect to edges that
164    * are to be removed.
165    * This implementation assumes that all edge removal operations are triggered
166    * through user interaction, i.e. that <em>all</em> edge removal events are
167    * bracketed in <code>PRE</code> and <code>POST</code> events.
168    */
169   static class EdgeConnectorListener implements GraphListener {
170     /** The current event block */
171     private int block;
172     /** Stores edges by event block */
173     private Map block2edges;
174     /** Stores the active/inactive state of this listener */
175     private boolean armed;
176 
177     EdgeConnectorListener() {
178       armed = true;
179     }
180 
181     public void onGraphEvent(final GraphEvent e) {
182       if (!armed) {
183         return;
184       }
185 
186       switch (e.getType()) {
187         case GraphEvent.PRE_EVENT:
188           ++block;
189           break;
190         case GraphEvent.POST_EVENT:
191           handleBlock();
192           --block;
193           break;
194         case GraphEvent.POST_EDGE_REMOVAL:
195           storeForHandleBlock((Edge) e.getData());
196           break;
197       }
198     }
199 
200     /**
201      * Stores the specified edge for later processing upon completion of the
202      * current event block.
203      */
204     private void storeForHandleBlock( final Edge e ) {
205       if (block2edges == null) {
206         block2edges = new HashMap();
207       }
208       final Integer key = new Integer(block);
209       EdgeList edges = (EdgeList) block2edges.get(key);
210       if (edges == null) {
211         edges = new EdgeList();
212         block2edges.put(key, edges);
213       }
214       edges.add(e);
215     }
216 
217     /**
218      * Handles cleanup of the edge-to-edge connection data upon completion
219      * of the current event block.
220      */
221     private void handleBlock() {
222       if (block2edges == null) {
223         return;
224       }
225 
226       final EdgeList el = (EdgeList) block2edges.remove(new Integer(block));
227       if (el == null) {
228         return;
229       }
230 
231       armed = false;
232       handleRecursive(el);
233       armed = true;
234 
235       if (block2edges.isEmpty()) {
236         block2edges = null;
237       }
238     }
239 
240     private void handleRecursive( final EdgeList el ) {
241       final EdgeList cascade = new EdgeList();
242       for (EdgeCursor ec = el.edges(); ec.ok(); ec.next()) {
243         final Edge edge = ec.edge();
244         Node node;
245         node = edge.source();
246         if (EdgeConnectorManager.getEdgeConnection(node) != null) {
247           final Graph graph = node.getGraph();
248           if (graph != null && node.degree() == 0) {
249             graph.removeNode(node);
250           }
251         }
252         node = edge.target();
253         if (EdgeConnectorManager.getEdgeConnection(node) != null) {
254           final Graph graph = node.getGraph();
255           if (graph != null && node.degree() == 0) {
256             graph.removeNode(node);
257           }
258         }
259         final NodeList connectors = EdgeConnectorManager.getConnectorNodes(edge);
260         if (connectors != null) {
261           for (NodeCursor nc = connectors.nodes(); nc.ok(); nc.next()) {
262             node = nc.node();
263             final Graph graph = node.getGraph();
264             if (graph != null) {
265               for (EdgeCursor nec = node.edges(); nec.ok(); nec.next()) {
266                 cascade.add(nec.edge());
267               }
268               graph.removeNode(node);
269             }
270           }
271         }
272       }
273 
274       if (!cascade.isEmpty()) {
275         handleRecursive(cascade);
276       }
277     }
278   }
279 
280   /**
281    * Represents the end point of an edge that connects to another edge. Note that
282    * with this implementation a call to updateLocation enforces that the location
283    * of the node will be on the corresponding edge path. In this demo the call to
284    * updateLocation is performed by the Graph2DRenderer implementation EdgeConnectorGraph2DRenderer.
285    */
286   static class EdgeConnectorRealizer extends ShapeNodeRealizer {
287     public EdgeConnectorRealizer() {
288       setShapeType(ELLIPSE);
289       setSize(5,5);
290       setFillColor(Color.yellow);
291     }
292 
293     public EdgeConnectorRealizer(NodeRealizer nr) {
294       super(nr);
295     }
296     public NodeRealizer createCopy(NodeRealizer nr) {
297       return new EdgeConnectorRealizer(nr);
298     }
299 
300     public void updateLocation() {
301       Node node = getNode();
302       if(node != null) {
303         Edge edge = EdgeConnectorManager.getEdgeConnection(node);
304         if(edge != null) {
305           Graph2D graph = (Graph2D) node.getGraph();
306           double ratio = EdgeConnectorManager.getEdgeConnectionRatio(node);
307           try {
308             Point2D point = PointPathProjector.getPointForGlobalRatio(graph.getRealizer(edge).getPath(), ratio);
309             setCenter(point.getX(), point.getY());
310           }catch(IllegalStateException isex) {}
311         }
312       }
313     }
314 
315 //    public void calcUnionRect(Rectangle2D r) {
316 //      updateLocation();
317 //      super.calcUnionRect(r);
318 //    }
319 
320 //    public void paint(Graphics2D gfx) {
321 //      updateLocation();
322 //      super.paintNode(gfx);
323 //    }
324   }
325 
326   /**
327    * Extends MoveSelectionMode to also handle edge-to-edge connections.
328    */
329   static class EdgeConnectorMoveSelectionMode extends MoveSelectionMode {
330     protected NodeList getNodesToBeMoved() {
331       NodeList result = super.getNodesToBeMoved();
332       for(NodeCursor nc = result.nodes(); nc.ok(); nc.next()) {
333         Node n = nc.node();
334         for(EdgeCursor ec = n.edges(); ec.ok(); ec.next()) {
335           Edge edge = ec.edge();
336           NodeList connectors = EdgeConnectorManager.getConnectorNodes(edge);
337           result.splice(connectors);
338         }
339       }
340       BendList bends = getBendsToBeMoved();
341       for (BendCursor bc = bends.bends(); bc.ok(); bc.next()) {
342         Bend b = bc.bend();
343         NodeList connectors = EdgeConnectorManager.getConnectorNodes(b.getEdge());
344         result.splice(connectors);
345       }
346       return result;
347     }
348   }
349 
350   /**
351    * Extends CreateEdgeMode to also handle edge-to-edge connections.
352    */
353   static class CreateEdgeConnectorMode extends CreateEdgeMode {
354     private Node startNode;
355 
356     public void mouseShiftPressedLeft(double x, double y) {
357       if(isEditing()) {
358         super.mouseShiftPressedLeft(x,y);
359       }
360       else {
361         Graph2D graph = getGraph2D();
362         Edge edge = getHitInfo(x,y).getHitEdge();
363         if (edge != null) {
364           EdgeConnectorRealizer ecNR = new EdgeConnectorRealizer();
365           Point2D p = new Point2D.Double(x, y);
366           double[] result = PointPathProjector.calculateClosestPathPoint(graph.getRealizer(edge).getPath(), p);
367           ecNR.setCenter(result[0], result[1]);
368           //ecNR.setCenter(x,y);
369           startNode = getGraph2D().createNode(ecNR);
370           view.updateView();
371           super.mouseShiftPressedLeft(result[0], result[1]);
372           EdgeConnectorManager.addEdgeConnection(startNode, edge, result[5]);
373         }
374         else {
375           startNode = null;
376           super.mouseShiftPressedLeft(x, y);
377         }
378       }
379     }
380 
381     public void mouseShiftReleasedLeft(double x, double y) {
382       Graph2D graph = getGraph2D();
383       Edge edge = getHitInfo(x, y).getHitEdge();
384       if (edge != null) {
385         EdgeConnectorRealizer ecNR = new EdgeConnectorRealizer();
386         Point2D p = new Point2D.Double(x, y);
387         double[] result = PointPathProjector.calculateClosestPathPoint(graph.getRealizer(edge).getPath(), p);
388         ecNR.setCenter(result[0], result[1]);
389         Node endNode = getGraph2D().createNode(ecNR);
390         view.updateView();
391         super.mouseShiftReleasedLeft(result[0], result[1]);
392         EdgeConnectorManager.addEdgeConnection(endNode, edge, result[5]);
393       } else {
394         super.mouseShiftReleasedLeft(x, y);
395       }
396     }
397 
398     public HitInfo getHitInfo(double x, double y) {
399       return new HitInfo(view, x, y, false,
400                    HitInfo.PORT,
401                    HitInfo.BEND,
402                    HitInfo.ELABEL,
403                    HitInfo.EDGE,
404                    HitInfo.NODE,
405                    HitInfo.NLABEL);
406     }
407 
408     protected void cancelEdgeCreation() {
409       if(startNode != null) {
410         getGraph2D().removeNode(startNode);
411       }
412       super.cancelEdgeCreation();
413     }
414 
415     public void setEditing(boolean active) {
416       if (!active) {
417         startNode = null;
418       }
419       super.setEditing(active);
420     }
421   }
422 
423   static class EdgeConnectorEditMode extends EditMode {
424     public void mouseDraggedLeft(double x, double y) {
425       if(isModifierPressed(lastPressEvent)) {
426         double px = translateX(lastPressEvent.getX());
427         double py = translateY(lastPressEvent.getY());
428         Edge edge = getHitInfo(px,py).getHitEdge();
429         if(edge != null) {
430           setChild(getCreateEdgeMode(), lastPressEvent, lastDragEvent);
431           return;
432         }
433       }
434       super.mouseDraggedLeft(x, y);
435     }
436   }
437 
438   /**
439    * Special MovePortMode that will allow to move the port of an edge that connects to
440    * another edge to be moved along the edge path.
441    */
442   static class EdgeConnectorMovePortMode extends MovePortMode {
443 
444     protected YList getPortCandidates(Node v, Edge e, double gridSpacing) {
445       Edge connectedEdge = EdgeConnectorManager.getEdgeConnection(v);
446       if(connectedEdge != null) {
447         Graph2D graph = getGraph2D();
448         //v is a connector point
449         YList result = new YList();
450         YPoint yport = e.source() == v ? graph.getSourcePointAbs(e) : graph.getTargetPointAbs(e);
451         Point2D p = new Point2D.Double(yport.x, yport.y);
452         double[] pppResult = PointPathProjector.calculateClosestPathPoint(getGraph2D().getRealizer(connectedEdge).getPath(), p);
453         result.add(new YPoint(pppResult[0], pppResult[1]));
454         return result;
455       }
456       return super.getPortCandidates(v,e,gridSpacing);
457     }
458 
459     public void mouseReleasedLeft(double x, double y) {
460       Port p = this.port;
461       if(p != null) {
462         Edge e = p.getOwner().getEdge();
463         Node v = null;
464         if(p == p.getOwner().getTargetPort()) {
465           v = e.target();
466         }
467         else {
468           v = e.source();
469         }
470         Edge connectedEdge = EdgeConnectorManager.getEdgeConnection(v);
471         if(connectedEdge == null) {
472           super.mouseReleasedLeft(x,y);
473           return;
474         }
475         else {
476           double[] result = PointPathProjector.calculateClosestPathPoint(getGraph2D().getRealizer(connectedEdge).getPath(),  x, y);
477           double ratio = result[5];
478           EdgeConnectorManager.addEdgeConnection(v, connectedEdge, ratio);
479           super.mouseReleasedLeft(x,y);
480           getGraph2D().setCenter(v, result[0], result[1]);
481           p.setOffsets(0,0);
482         }
483         getGraph2D().updateViews();
484       }
485     }
486   }
487 
488   /**
489    * Helper class that provides diverse services related to working with points on a path.
490    */
491   static class PointPathProjector {
492     private PointPathProjector() {
493     }
494 
495     static double[] calculateClosestPathPoint(GeneralPath path, double px, double py) {
496       return calculateClosestPathPoint(path, new Point2D.Double(px,py));
497     }
498 
499     /**
500      * Calculates the point on the path which is closest to the given point.
501      * Ties are broken arbitrarily.
502      * @param path where to look for the closest point
503      * @param p to this point
504      * @return double[6]
505      * <ul>
506      *   <li>x coordinate of the closest point</li>
507      *   <li>y coordinate of the closest point</li>
508      *   <li>distance of the closest point to given point</li>
509      *   <li>index of the segment of the path including the closest point
510      *       (as a double starting with 0.0, segments are computed with a
511      *       path iterator with flatness 1.0)</li>
512      *   <li>ratio of closest point on the the including segment (between 0.0 and 1.0)</li>
513      *   <li>ratio of closest point on the entire path (between 0.0 and 1.0)</li>
514      * </ul>
515      */
516     static double[] calculateClosestPathPoint(GeneralPath path, Point2D p) {
517       double[] result = new double[6];
518       double px = p.getX();
519       double py = p.getY();
520       YPoint point = new YPoint(px, py);
521       double pathLength = 0;
522 
523       CustomPathIterator pi = new CustomPathIterator(path, 1.0);
524       double[] curSeg = new double[4];
525       double minDist;
526       if (pi.ok()) {
527         curSeg = pi.segment();
528         minDist = YPoint.distance(px, py, curSeg[0], curSeg[1]);
529         result[0] = curSeg[0];
530         result[1] = curSeg[1];
531         result[2] = minDist;
532         result[3] = 0.0;
533         result[4] = 0.0;
534         result[5] = 0.0;
535       } else {
536         // no points in GeneralPath: should not happen in this context
537         throw new IllegalStateException("path without any coordinates");
538       }
539 
540       int segmentIndex = 0;
541       double lastPathLength = 0.0;
542       do {
543         YPoint segmentStart = new YPoint(curSeg[0], curSeg[1]);
544         YPoint segmentEnd = new YPoint(curSeg[2], curSeg[3]);
545         YVector segmentDirection = new YVector(segmentEnd, segmentStart);
546         double segmentLength = segmentDirection.length();
547         pathLength += segmentLength;
548         segmentDirection.norm();
549 
550         AffineLine currentSegment = new AffineLine(segmentStart, segmentDirection);
551         AffineLine throughPoint = new AffineLine(point, YVector.orthoNormal(segmentDirection));
552         YPoint crossing = AffineLine.getCrossing(currentSegment, throughPoint);
553         YVector crossingVector = new YVector(crossing, segmentStart);
554 
555         YVector segmentVector = new YVector(segmentEnd, segmentStart);
556         double indexEnd = YVector.scalarProduct(segmentVector, segmentDirection);
557         double indexCrossing = YVector.scalarProduct(crossingVector, segmentDirection);
558 
559         double dist;
560         double segmentRatio;
561         YPoint nearestOnSegment;
562         if (indexCrossing <= 0.0) {
563           dist = YPoint.distance(point, segmentStart);
564           nearestOnSegment = segmentStart;
565           segmentRatio = 0.0;
566         } else if (indexCrossing >= indexEnd) {
567           dist = YPoint.distance(point, segmentEnd);
568           nearestOnSegment = segmentEnd;
569           segmentRatio = 1.0;
570         } else {
571           dist = YPoint.distance(point, crossing);
572           nearestOnSegment = crossing;
573           segmentRatio = indexCrossing / indexEnd;
574         }
575 
576         if (dist < minDist) {
577           minDist = dist;
578           result[0] = nearestOnSegment.getX();
579           result[1] = nearestOnSegment.getY();
580           result[2] = minDist;
581           result[3] = segmentIndex;
582           result[4] = segmentRatio;
583           result[5] = segmentLength * segmentRatio + lastPathLength;
584         }
585 
586         segmentIndex++;
587         lastPathLength = pathLength;
588         pi.next();
589       } while (pi.ok());
590 
591       if(pathLength > 0) {
592         result[5] = result[5] / pathLength;
593       } else {
594         result[5] = 0.0;
595       }
596       return result;
597     }
598 
599     static Point2D getPointForGlobalRatio(GeneralPath path, double globalRatio) {
600       if(globalRatio > 1.0 || globalRatio < 0.0) {
601         throw new IllegalArgumentException("globalRatio outside of [0,1]");
602       }
603       double totalPathLength = getPathLength(path);
604       double targetPathLength = totalPathLength * globalRatio;
605       CustomPathIterator pi = new CustomPathIterator(path, 1.0);
606       YPoint segmentStart = null, segmentEnd = null;
607       if (pi.isDone()) {
608         // no points in GeneralPath: should not happen in this context
609         throw new IllegalStateException("path without any coordinates");
610       } else {
611         segmentStart = pi.segmentStart();
612         segmentEnd = pi.segmentEnd();
613       }
614 
615       double currentPathLength = 0.0;
616       double lastPathLength = 0.0;
617       while (pi.ok()) {
618         YVector segmentDirection = new YVector(segmentEnd, segmentStart);
619         double segmentLength = segmentDirection.length();
620         currentPathLength += segmentLength;
621         if(currentPathLength / totalPathLength >= globalRatio) {
622           double remainingLength = targetPathLength - lastPathLength;
623           double localRatio = remainingLength / segmentLength;
624           segmentDirection.scale(localRatio);
625           YPoint targetPoint = YVector.add(segmentStart, segmentDirection);
626           return new Point2D.Double(targetPoint.getX(),targetPoint.getY());
627         }
628 
629         lastPathLength = currentPathLength;
630         pi.next();
631         segmentStart = pi.segmentStart();
632         segmentEnd = pi.segmentEnd();
633       }
634 
635       // we ran past the last point of the path (numeric problems?), return last point
636       return new Point2D.Double(segmentStart.getX(), segmentStart.getY());
637     }
638 
639     static Point2D getPointForLocalRatio(GeneralPath path, int segmentIndex, double segmentRatio) {
640       if (segmentRatio > 1.0 || segmentRatio < 0.0) {
641         throw new IllegalArgumentException("segmentRatio outside of [0,1]");
642       }
643       CustomPathIterator pi = new CustomPathIterator(path, 1.0);
644       if (pi.isDone()) {
645         // no points in GeneralPath: should not happen in this context
646         throw new IllegalStateException("path without any coordinates");
647       }
648       int currentIndex = 0;
649       while (pi.ok() && currentIndex < segmentIndex) {
650         pi.next();
651         currentIndex++;
652       }
653       if(currentIndex < segmentIndex)
654       {
655         throw new IllegalArgumentException("found no segment for given segmentIndex");
656       }
657 
658       YPoint segmentStart = pi.segmentStart();
659       YPoint segmentEnd = pi.segmentEnd();
660       YVector segmentDirection = new YVector(segmentEnd, segmentStart);
661       segmentDirection.scale(segmentRatio);
662       YPoint targetPoint = YVector.add(segmentStart, segmentDirection);
663       return new Point2D.Double(targetPoint.getX(), targetPoint.getY());
664     }
665 
666     private static double getPathLength(GeneralPath path) {
667       double length = 0.0;
668       for(CustomPathIterator pi = new CustomPathIterator(path, 1.0); pi.ok(); pi.next()) {
669         length += pi.segmentDirection().length();
670       }
671       return length;
672     }
673   }
674 
675   /**
676    * Helper class used by PointPathProjector.
677    */
678   static class CustomPathIterator {
679     private double[] cachedSegment;
680     private boolean moreToGet;
681     private PathIterator pathIterator;
682 
683     public CustomPathIterator(GeneralPath path, double flatness) {
684       // copy the path, thus the original may safely change during iteration
685       pathIterator = (new GeneralPath(path)).getPathIterator(Util.TRANSFORM, flatness);
686       cachedSegment = new double[4];
687       getFirstSegment();
688     }
689 
690     public boolean ok()
691     {
692       return moreToGet;
693     }
694 
695     public boolean isDone() {
696       return !moreToGet;
697     }
698 
699     public final double[] segment() {
700       if (moreToGet) {
701         return cachedSegment;
702       } else {
703         return null;
704       }
705     }
706 
707     public YPoint segmentStart() {
708       if(moreToGet) {
709         return new YPoint(cachedSegment[0], cachedSegment[1]);
710       } else {
711         return null;
712       }
713     }
714 
715     public YPoint segmentEnd() {
716       if(moreToGet) {
717         return new YPoint(cachedSegment[2], cachedSegment[3]);
718       } else {
719         return null;
720       }
721     }
722 
723     public YVector segmentDirection() {
724       if(moreToGet) {
725         return new YVector(segmentEnd(), segmentStart());
726       } else {
727         return null;
728       }
729     }
730 
731     public void next() {
732       if (!pathIterator.isDone()) {
733         float[] curSeg = new float[2];
734         cachedSegment[0] = cachedSegment[2];
735         cachedSegment[1] = cachedSegment[3];
736         pathIterator.currentSegment(curSeg);
737         cachedSegment[2] = curSeg[0];
738         cachedSegment[3] = curSeg[1];
739         pathIterator.next();
740       } else {
741         moreToGet = false;
742       }
743     }
744 
745     private void getFirstSegment() {
746       float[] curSeg = new float[2];
747       if (!pathIterator.isDone()) {
748         pathIterator.currentSegment(curSeg);
749         cachedSegment[0] = curSeg[0];
750         cachedSegment[1] = curSeg[1];
751         pathIterator.next();
752         moreToGet = true;
753       } else {
754         moreToGet = false;
755       }
756       if (!pathIterator.isDone()) {
757         pathIterator.currentSegment(curSeg);
758         cachedSegment[2] = curSeg[0];
759         cachedSegment[3] = curSeg[1];
760         pathIterator.next();
761         moreToGet = true;
762       } else {
763         moreToGet = false;
764       }
765     }
766   }
767 
768   public static void main(String[] args) {
769     initLnF();
770     EdgeConnectorDemo demo = new EdgeConnectorDemo();
771     demo.start(demo.getClass().getName());
772   }
773 }
774