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