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.advanced;
29  
30  import java.awt.Graphics2D;
31  import java.awt.EventQueue;
32  import java.awt.geom.GeneralPath;
33  import java.awt.geom.PathIterator;
34  import java.awt.geom.Point2D;
35  import java.util.HashMap;
36  import java.util.Iterator;
37  import java.util.Locale;
38  import java.util.Map;
39  import java.util.WeakHashMap;
40  
41  import javax.swing.ActionMap;
42  import javax.swing.InputMap;
43  import javax.swing.JComponent;
44  
45  import org.w3c.dom.Element;
46  
47  import demo.view.DemoBase;
48  import demo.view.DemoDefaults;
49  
50  import y.base.DataMap;
51  import y.base.Edge;
52  import y.base.EdgeCursor;
53  import y.base.EdgeList;
54  import y.base.Graph;
55  import y.base.GraphEvent;
56  import y.base.GraphListener;
57  import y.base.Node;
58  import y.base.NodeCursor;
59  import y.base.NodeList;
60  import y.base.YList;
61  import y.geom.Geom;
62  import y.geom.YDimension;
63  import y.geom.YPoint;
64  import y.geom.YVector;
65  import y.io.GraphMLIOHandler;
66  import y.io.graphml.KeyScope;
67  import y.io.graphml.KeyType;
68  import y.io.graphml.input.GraphMLParseContext;
69  import y.io.graphml.input.GraphMLParseException;
70  import y.io.graphml.input.NameBasedDeserializer;
71  import y.io.graphml.input.ParseEventListenerAdapter;
72  import y.io.graphml.output.AbstractOutputHandler;
73  import y.io.graphml.output.GraphElementIdProvider;
74  import y.io.graphml.output.GraphMLWriteContext;
75  import y.io.graphml.output.GraphMLWriteException;
76  import y.io.graphml.output.XmlWriter;
77  import y.util.DataAcceptorAdapter;
78  import y.util.GraphCopier;
79  import y.util.Maps;
80  import y.util.Tuple;
81  import y.view.Bend;
82  import y.view.BendCursor;
83  import y.view.BendList;
84  import y.view.CreateEdgeMode;
85  import y.view.DefaultGraph2DRenderer;
86  import y.view.EdgeRealizer;
87  import y.view.EditMode;
88  import y.view.GenericNodeRealizer;
89  import y.view.Graph2D;
90  import y.view.Graph2DClipboard;
91  import y.view.Graph2DTraversal;
92  import y.view.Graph2DViewActions;
93  import y.view.HitInfo;
94  import y.view.MovePortMode;
95  import y.view.MoveSelectionMode;
96  import y.view.NodeRealizer;
97  import y.view.Port;
98  import y.view.ShapeNodePainter;
99  
100 /**
101  * Class that shows how to mimic node-to-edge and edge-to-edge connections. In this demo an edge that connects
102  * to a node or to another edge is modeled as a normal edge that has a special node as its end point. That special
103  * node is located on the path of the edge. When moving the edge path the special node will also be moved. Thus,
104  * it looks and feels like a proper edge connection to an edge.
105  * <p>
106  * Usage: to create an edge that starts at another edge, shift-press on the edge to initiate the
107  * edge creation gesture, then drag the mouse. To create an edge that ends at another edge,
108  * shift-release the mouse on the edge.
109  * </p>
110  */
111 public class EdgeConnectorDemo extends DemoBase {
112 
113   /**
114    * Create a GenericNodeRealizer configuration for nodes that represent edge connectors
115    */
116   static {
117     Map configurationMap = GenericNodeRealizer.getFactory().createDefaultConfigurationMap();
118     ShapeNodePainter painter = new ShapeNodePainter();
119     painter.setShapeType(ShapeNodePainter.ELLIPSE);
120     configurationMap.put(y.view.GenericNodeRealizer.Painter.class, painter);
121 
122     // Size constraint to prevent resizing of edge connectors
123     GenericNodeRealizer.GenericSizeConstraintProvider scp = new GenericNodeRealizer.GenericSizeConstraintProvider() {
124       public YDimension getMaximumSize(NodeRealizer context) {
125         return new YDimension(5,5);
126       }
127 
128       public YDimension getMinimumSize(NodeRealizer context) {
129         return new YDimension(5,5);
130       }
131     };
132     configurationMap.put(GenericNodeRealizer.GenericSizeConstraintProvider.class, scp);
133     GenericNodeRealizer.getFactory().addConfiguration("EdgeConnector", configurationMap);
134   }
135 
136   protected void initialize() {
137     super.initialize();
138     view.setAntialiasedPainting(true);
139     EdgeConnectorGraph2DRenderer r = new EdgeConnectorGraph2DRenderer();
140     r.setDrawEdgesFirst(true);
141     view.setGraph2DRenderer(r);
142     view.getGraph2D().addGraphListener(new EdgeConnectorListener());
143     loadGraph("resource/EdgeConnectorDemo.graphml");
144   }
145 
146   protected void registerViewModes() {
147     EditMode editMode = new EdgeConnectorEditMode();
148     editMode.setCreateEdgeMode(new CreateEdgeConnectorMode());
149     editMode.setMoveSelectionMode(new EdgeConnectorMoveSelectionMode());
150     editMode.setMovePortMode(new EdgeConnectorMovePortMode());
151     view.addViewMode(editMode);
152   }
153 
154   /**
155    * Special Graph2DRenderer that updates the edge connector locations before graph elements
156    * are rendered to the view.
157    */
158   static class EdgeConnectorGraph2DRenderer extends DefaultGraph2DRenderer {
159     public void paint(final Graphics2D gfx, final Graph2D graph) {
160       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
161         Node n = nc.node();
162         if(EdgeConnectorManager.isEdgeConnector(n)) {
163           updateEdgeConnectorLocation(n);
164         }
165       }
166       super.paint(gfx, graph);
167     }
168 
169     public void updateEdgeConnectorLocation(Node node) {
170       if(node != null) {
171         Edge edge = EdgeConnectorManager.getEdgeConnection(node);
172         if(edge != null) {
173           Graph2D graph = (Graph2D) node.getGraph();
174           double ratio = EdgeConnectorManager.getEdgeConnectionRatio(node);
175           Point2D point = PointPathProjector.getPointForGlobalRatio(graph.getRealizer(edge), ratio);
176           NodeRealizer nr = graph.getRealizer(node);
177           nr.setCenter(point.getX(), point.getY());
178         }
179       }
180     }
181   }
182 
183   /**
184    * Create a GraphMLIOHandler that will serialize and deserialize data that is associated with edge connector nodes.
185    */
186   protected GraphMLIOHandler createGraphMLIOHandler() {
187     GraphMLIOHandler ioHandler = super.createGraphMLIOHandler();
188 
189     ioHandler.getGraphMLHandler().addOutputHandlerProvider(new AbstractOutputHandler("edgeConnectingData", KeyScope.NODE, KeyType.COMPLEX) {
190       protected void writeValueCore(GraphMLWriteContext context, Object data)
191           throws GraphMLWriteException {
192         if(data != null) {
193           Tuple tuple = (Tuple) data;
194           Edge edge = (Edge) tuple.o1;
195           double ratio = ((Double)tuple.o2).doubleValue();
196           XmlWriter writer = context.getWriter();
197           GraphElementIdProvider idProvider = (GraphElementIdProvider) context.lookup(GraphElementIdProvider.class);
198           String edgeId = idProvider.getEdgeId(edge, context);
199           writer.writeStartElement("connectorData", "demo");
200           writer.writeAttribute("edgeId", edgeId);
201           writer.writeAttribute("ratio", ratio);
202           writer.writeEndElement();
203         }
204       }
205 
206       protected Object getValue(GraphMLWriteContext context, Object key)
207           throws GraphMLWriteException {
208         return EdgeConnectorManager.map.get(key);
209       }
210     });
211 
212     ioHandler.getGraphMLHandler().addOutputHandlerProvider(new AbstractOutputHandler("edgeId", KeyScope.EDGE, KeyType.STRING) {
213       protected void writeValueCore(GraphMLWriteContext context, Object data)
214           throws GraphMLWriteException {
215         if(data != null) {
216           XmlWriter writer = context.getWriter();
217           writer.writeText(data.toString());
218         }
219       }
220 
221       protected Object getValue(GraphMLWriteContext context, Object key)
222           throws GraphMLWriteException {
223         GraphElementIdProvider idProvider = (GraphElementIdProvider) context.lookup(GraphElementIdProvider.class);
224         return idProvider.getEdgeId((Edge) key, context);
225       }
226     });
227 
228     final DataMap edgeIdMap = Maps.createHashedDataMap();
229     ioHandler.getGraphMLHandler().addInputDataAcceptor("edgeId",
230         new DataAcceptorAdapter() {
231           public void set(Object dataHolder, Object value) {
232             edgeIdMap.set(value, dataHolder);
233           }
234         },
235         KeyScope.EDGE, KeyType.STRING);
236 
237 
238     final DataMap tempConnectorMap = Maps.createHashedDataMap();
239 
240     ioHandler.getGraphMLHandler().addInputDataAcceptor("edgeConnectingData", tempConnectorMap, KeyScope.NODE, new NameBasedDeserializer() {
241       public Object deserializeNode(org.w3c.dom.Node xmlNode,
242           GraphMLParseContext context) throws GraphMLParseException {
243         Element xmlElem = (Element) xmlNode;
244         String edgeId = xmlElem.getAttribute("edgeId");
245         String doubleStr = xmlElem.getAttribute("ratio");
246         return new Tuple(edgeId, doubleStr);
247       }
248 
249       public String getNodeName(GraphMLParseContext context) {
250         return "connectorData";
251       }
252 
253       public String getNamespaceURI(GraphMLParseContext context) {
254         return "demo";
255       }
256     });
257 
258     ioHandler.getGraphMLHandler().addParseEventListener(new ParseEventListenerAdapter() {
259       public void onGraphMLParsed(y.io.graphml.input.ParseEvent event) {
260         Graph2D graph = (Graph2D) event.getContext().getGraph();
261         for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
262           Node n = nc.node();
263           Tuple tuple = (Tuple) tempConnectorMap.get(n);
264           if(tuple != null) {
265             Edge edge = (Edge) edgeIdMap.get(tuple.o1);
266             Double ratio = Double.valueOf(tuple.o2.toString());
267             EdgeConnectorManager.map.put(n, new Tuple(edge, ratio));
268           }
269         }
270       }
271     });
272     return ioHandler;
273   }
274 
275   /**
276    * Overwritten to decorate the clipboard's copy factory with an {@link EdgeConnectorGraphCopyFactory} that also
277    * handles copying the edge connector information.
278    */
279   protected Graph2DClipboard getClipboard() {
280     final Graph2DClipboard clipboard = super.getClipboard();
281     clipboard.setCopyFactory(new EdgeConnectorGraphCopyFactory(clipboard.getCopyFactory()));
282     return clipboard;
283   }
284 
285   protected void registerViewActions() {
286     //register keyboard actions
287     super.registerViewActions();
288     ActionMap amap = view.getCanvasComponent().getActionMap();
289     InputMap imap = view.getCanvasComponent().getInputMap();
290     if (!isDeletionEnabled()) {
291       amap.remove(Graph2DViewActions.DELETE_SELECTION);
292     }
293     view.getCanvasComponent().setActionMap(amap);
294     view.getCanvasComponent().setInputMap(JComponent.WHEN_FOCUSED, imap);
295   }
296 
297   /**
298    * Manages edge-to-edge dependency information.
299    */
300   static class EdgeConnectorManager {
301     static final Map map = new WeakHashMap();
302 
303     private EdgeConnectorManager() {
304     }
305 
306     static boolean isEdgeConnector(Node n) {
307       return map.containsKey(n);
308     }
309 
310     static void addEdgeConnection(Node connector, Edge edge, double pathRatio) {
311       map.put(connector, Tuple.create(edge, new Double(pathRatio)));
312     }
313 
314     static Edge getEdgeConnection(Node connector) {
315       Tuple tuple = (Tuple) map.get(connector);
316       if (tuple != null) {
317         return (Edge) tuple.o1;
318       }
319       return null;
320     }
321 
322     static double getEdgeConnectionRatio(Node connector) {
323       Tuple tuple = (Tuple) map.get(connector);
324       if (tuple != null) {
325         return ((Double) tuple.o2).doubleValue();
326       }
327       return 0.0;  //should throw an exception
328     }
329 
330     static NodeList getConnectorNodes(Edge edge) {
331       NodeList result = new NodeList();
332       for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
333         Map.Entry entry = (Map.Entry) iter.next();
334         Tuple value = (Tuple) entry.getValue();
335         if (value.o1 == edge) {
336           result.add(entry.getKey());
337         }
338       }
339       return result;
340     }
341 
342     static NodeRealizer createEdgeConnectorRealizer() {
343       GenericNodeRealizer gnr = new GenericNodeRealizer("EdgeConnector");
344       gnr.setSize(5,5);
345       gnr.setFillColor(DemoDefaults.DEFAULT_CONTRAST_COLOR);
346       return gnr;
347     }
348 
349   }
350 
351   /**
352    * Graph listener that automatically removes edges that connect to edges that
353    * are to be removed.
354    * This implementation assumes that all edge removal operations are triggered
355    * through user interaction, i.e. that <em>all</em> edge removal events are
356    * bracketed in <code>PRE</code> and <code>POST</code> events.
357    */
358   static class EdgeConnectorListener implements GraphListener {
359     /** The current event block */
360     private int block;
361     /** Stores edges by event block */
362     private Map block2edges;
363     /** Stores the active/inactive state of this listener */
364     private boolean armed;
365 
366     EdgeConnectorListener() {
367       armed = true;
368     }
369 
370     public void onGraphEvent(final GraphEvent e) {
371       if (!armed) {
372         return;
373       }
374 
375       switch (e.getType()) {
376         case GraphEvent.PRE_EVENT:
377           ++block;
378           break;
379         case GraphEvent.POST_EVENT:
380           handleBlock();
381           --block;
382           break;
383         case GraphEvent.POST_EDGE_REMOVAL:
384           storeForHandleBlock((Edge) e.getData());
385           break;
386       }
387     }
388 
389     /**
390      * Stores the specified edge for later processing upon completion of the
391      * current event block.
392      */
393     private void storeForHandleBlock( final Edge e ) {
394       if (block2edges == null) {
395         block2edges = new HashMap();
396       }
397       final Integer key = new Integer(block);
398       EdgeList edges = (EdgeList) block2edges.get(key);
399       if (edges == null) {
400         edges = new EdgeList();
401         block2edges.put(key, edges);
402       }
403       edges.add(e);
404     }
405 
406     /**
407      * Handles cleanup of the edge-to-edge connection data upon completion
408      * of the current event block.
409      */
410     private void handleBlock() {
411       if (block2edges == null) {
412         return;
413       }
414 
415       final EdgeList el = (EdgeList) block2edges.remove(new Integer(block));
416       if (el == null) {
417         return;
418       }
419 
420       armed = false;
421       handleRecursive(el);
422       armed = true;
423 
424       if (block2edges.isEmpty()) {
425         block2edges = null;
426       }
427     }
428 
429     private void handleRecursive( final EdgeList el ) {
430       final EdgeList cascade = new EdgeList();
431       for (EdgeCursor ec = el.edges(); ec.ok(); ec.next()) {
432         final Edge edge = ec.edge();
433         Node node;
434         node = edge.source();
435         if (EdgeConnectorManager.getEdgeConnection(node) != null) {
436           final Graph graph = node.getGraph();
437           if (graph != null && node.degree() == 0) {
438             graph.removeNode(node);
439           }
440         }
441         node = edge.target();
442         if (EdgeConnectorManager.getEdgeConnection(node) != null) {
443           final Graph graph = node.getGraph();
444           if (graph != null && node.degree() == 0) {
445             graph.removeNode(node);
446           }
447         }
448         final NodeList connectors = EdgeConnectorManager.getConnectorNodes(edge);
449         if (connectors != null) {
450           for (NodeCursor nc = connectors.nodes(); nc.ok(); nc.next()) {
451             node = nc.node();
452             final Graph graph = node.getGraph();
453             if (graph != null) {
454               for (EdgeCursor nec = node.edges(); nec.ok(); nec.next()) {
455                 cascade.add(nec.edge());
456               }
457               graph.removeNode(node);
458             }
459           }
460         }
461       }
462 
463       if (!cascade.isEmpty()) {
464         handleRecursive(cascade);
465       }
466     }
467   }
468 //
469 //  /**
470 //   * Represents the end point of an edge that connects to another edge. Note that
471 //   * with this implementation a call to updateLocation enforces that the location
472 //   * of the node will be on the corresponding edge path. In this demo the call to
473 //   * updateLocation is performed by the Graph2DRenderer implementation EdgeConnectorGraph2DRenderer.
474 //   */
475 //  static class EdgeConnectorRealizer extends ShapeNodeRealizer {
476 //    public EdgeConnectorRealizer() {
477 //      setShapeType(ELLIPSE);
478 //      setSize(5,5);
479 //      setFillColor(Color.yellow);
480 //    }
481 //
482 //    public EdgeConnectorRealizer(NodeRealizer nr) {
483 //      super(nr);
484 //    }
485 //    public NodeRealizer createCopy(NodeRealizer nr) {
486 //      return new EdgeConnectorRealizer(nr);
487 //    }
488 //
489 //
490 ////    public void calcUnionRect(Rectangle2D r) {
491 ////      updateLocation();
492 ////      super.calcUnionRect(r);
493 ////    }
494 //
495 ////    public void paint(Graphics2D gfx) {
496 ////      updateLocation();
497 ////      super.paintNode(gfx);
498 ////    }
499 //  }
500 
501   /**
502    * Extends MoveSelectionMode to also handle edge-to-edge connections.
503    */
504   static class EdgeConnectorMoveSelectionMode extends MoveSelectionMode {
505     protected NodeList getNodesToBeMoved() {
506       NodeList result = super.getNodesToBeMoved();
507       for(NodeCursor nc = result.nodes(); nc.ok(); nc.next()) {
508         Node n = nc.node();
509         for(EdgeCursor ec = n.edges(); ec.ok(); ec.next()) {
510           Edge edge = ec.edge();
511           NodeList connectors = EdgeConnectorManager.getConnectorNodes(edge);
512           result.splice(connectors);
513         }
514       }
515       BendList bends = getBendsToBeMoved();
516       for (BendCursor bc = bends.bends(); bc.ok(); bc.next()) {
517         Bend b = bc.bend();
518         NodeList connectors = EdgeConnectorManager.getConnectorNodes(b.getEdge());
519         result.splice(connectors);
520       }
521       return result;
522     }
523   }
524 
525   /**
526    * Extends CreateEdgeMode to also handle edge-to-edge connections.
527    */
528   static class CreateEdgeConnectorMode extends CreateEdgeMode {
529     private Node startNode;
530 
531     public void mousePressedLeft(double x, double y) {
532       // fire event to mark start of edge creation for undo/redo
533       final Node hitNode = getHitInfo(x, y).getHitNode();
534       if (hitNode != null) {
535         getGraph2D().firePreEvent();
536       }
537       super.mousePressedLeft(x, y);
538     }
539 
540     public void mouseShiftPressedLeft(double x, double y) {
541       // fire event to mark start of edge creation for undo/redo
542       final Node hitNode = getHitInfo(x, y).getHitNode();
543       if (hitNode != null) {
544         getGraph2D().firePreEvent();
545       }
546       if(isEditing()) {
547         super.mouseShiftPressedLeft(x,y);
548       }
549       else {
550         Graph2D graph = getGraph2D();
551         Edge edge = getHitInfo(x,y).getHitEdge();
552         if (edge != null) {
553           // fire event to mark start of edge creation for undo/redo
554           getGraph2D().firePreEvent();
555           NodeRealizer ecNR = EdgeConnectorManager.createEdgeConnectorRealizer();
556           Point2D p = new Point2D.Double(x, y);
557           double[] result = PointPathProjector.calculateClosestPathPoint(graph.getRealizer(edge).getPath(), p);
558           ecNR.setCenter(result[0], result[1]);
559           //ecNR.setCenter(x,y);
560           startNode = getGraph2D().createNode(ecNR);
561           view.updateView();
562           super.mouseShiftPressedLeft(result[0], result[1]);
563           EdgeConnectorManager.addEdgeConnection(startNode, edge, result[5]);
564         }
565         else {
566           startNode = null;
567           super.mouseShiftPressedLeft(x, y);
568         }
569       }
570     }
571 
572     public void mouseReleasedLeft(double x, double y) {
573       // fire event to mark start of edge creation for undo/redo
574       super.mouseReleasedLeft(x, y);
575       final Node hitNode = getHitInfo(x, y).getHitNode();
576       if (hitNode != null) {
577         getGraph2D().firePostEvent();
578       }
579     }
580 
581     public void mouseShiftReleasedLeft(double x, double y) {
582       Graph2D graph = getGraph2D();
583       Edge edge = getHitInfo(x, y).getHitEdge();
584       if (edge != null) {
585         NodeRealizer ecNR = EdgeConnectorManager.createEdgeConnectorRealizer();
586         Point2D p = new Point2D.Double(x, y);
587         double[] result = PointPathProjector.calculateClosestPathPoint(graph.getRealizer(edge).getPath(), p);
588         ecNR.setCenter(result[0], result[1]);
589         Node endNode = getGraph2D().createNode(ecNR);
590         view.updateView();
591         super.mouseShiftReleasedLeft(result[0], result[1]);
592         EdgeConnectorManager.addEdgeConnection(endNode, edge, result[5]);
593         // fire event to mark start of edge creation for undo/redo
594         getGraph2D().firePostEvent();
595       } else {
596         super.mouseShiftReleasedLeft(x, y);
597       }
598       // fire event to mark start of edge creation for undo/redo
599       final Node hitNode = getHitInfo(x, y).getHitNode();
600       if (hitNode != null && !EdgeConnectorManager.isEdgeConnector(hitNode)) {
601         getGraph2D().firePostEvent();
602       }
603     }
604 
605     public HitInfo getHitInfo(double x, double y) {
606       final HitInfo info = view.getHitInfoFactory()
607               .createHitInfo(x, y, Graph2DTraversal.ALL, false);
608       setLastHitInfo(info);
609       return info;
610     }
611 
612     protected void cancelEdgeCreation() {
613       if(startNode != null) {
614         final Node tmp = startNode;
615         startNode = null;
616         getGraph2D().removeNode(tmp);
617         // fire event to mark start of edge creation for undo/redo
618         getGraph2D().firePostEvent();
619       }
620       super.cancelEdgeCreation();
621     }
622 
623     public void setEditing(boolean active) {
624       if (!active) {
625         startNode = null;
626       }
627       super.setEditing(active);
628     }
629   }
630 
631   static class EdgeConnectorEditMode extends EditMode {
632     public void mouseDraggedLeft(double x, double y) {
633       if(isModifierPressed(lastPressEvent)) {
634         double px = translateX(lastPressEvent.getX());
635         double py = translateY(lastPressEvent.getY());
636         Edge edge = getHitInfo(px,py).getHitEdge();
637         if(edge != null) {
638           setChild(getCreateEdgeMode(), lastPressEvent, lastDragEvent);
639           return;
640         }
641       }
642       super.mouseDraggedLeft(x, y);
643     }
644   }
645 
646   /**
647    * Special MovePortMode that will allow to move the port of an edge that connects to
648    * another edge to be moved along the edge path.
649    */
650   static class EdgeConnectorMovePortMode extends MovePortMode {
651 
652     protected YList getPortCandidates(Node v, Edge e, double gridSpacing) {
653       Edge connectedEdge = EdgeConnectorManager.getEdgeConnection(v);
654       if(connectedEdge != null) {
655         Graph2D graph = getGraph2D();
656         //v is a connector point
657         YList result = new YList();
658         YPoint yport = e.source() == v ? graph.getSourcePointAbs(e) : graph.getTargetPointAbs(e);
659         Point2D p = new Point2D.Double(yport.x, yport.y);
660         double[] pppResult = PointPathProjector.calculateClosestPathPoint(getGraph2D().getRealizer(connectedEdge).getPath(), p);
661         result.add(new YPoint(pppResult[0], pppResult[1]));
662         return result;
663       }
664       return super.getPortCandidates(v,e,gridSpacing);
665     }
666 
667     public void mouseReleasedLeft(double x, double y) {
668       Port p = this.port;
669       if(p != null) {
670         Edge e = p.getOwner().getEdge();
671         Node v = null;
672         if(p == p.getOwner().getTargetPort()) {
673           v = e.target();
674         }
675         else {
676           v = e.source();
677         }
678         Edge connectedEdge = EdgeConnectorManager.getEdgeConnection(v);
679         if(connectedEdge == null) {
680           super.mouseReleasedLeft(x,y);
681           return;
682         }
683         else {
684           double[] result = PointPathProjector.calculateClosestPathPoint(getGraph2D().getRealizer(connectedEdge).getPath(),  x, y);
685           double ratio = result[5];
686           EdgeConnectorManager.addEdgeConnection(v, connectedEdge, ratio);
687           super.mouseReleasedLeft(x,y);
688           getGraph2D().setCenter(v, result[0], result[1]);
689           p.setOffsets(0,0);
690         }
691         getGraph2D().updateViews();
692       }
693     }
694   }
695 
696   /**
697    * Helper class that provides diverse services related to working with points on a path.
698    */
699   public static class PointPathProjector {
700 
701     private PointPathProjector() {
702     }
703 
704     public static double[] calculateClosestPathPoint(GeneralPath path, double px, double py) {
705       return calculateClosestPathPoint(path, new Point2D.Double(px,py));
706     }
707 
708     /**
709      * Calculates the point on the path which is closest to the given point.
710      * Ties are broken arbitrarily.
711      * @param path where to look for the closest point
712      * @param p to this point
713      * @return double[6]
714      * <ul>
715      *   <li>x coordinate of the closest point</li>
716      *   <li>y coordinate of the closest point</li>
717      *   <li>distance of the closest point to given point</li>
718      *   <li>index of the segment of the path including the closest point
719      *       (as a double starting with 0.0, segments are computed with a
720      *       path iterator with flatness 1.0)</li>
721      *   <li>ratio of closest point on the the including segment (between 0.0 and 1.0)</li>
722      *   <li>ratio of closest point on the entire path (between 0.0 and 1.0)</li>
723      * </ul>
724      */
725     public static double[] calculateClosestPathPoint(GeneralPath path, Point2D p) {
726       double[] result = new double[6];
727       double px = p.getX();
728       double py = p.getY();
729       YPoint point = new YPoint(px, py);
730       double pathLength = 0;
731 
732       CustomPathIterator pi = new CustomPathIterator(path, 1.0);
733       double[] curSeg = new double[4];
734       double minDist;
735       if (pi.ok()) {
736         curSeg = pi.segment();
737         minDist = YPoint.distance(px, py, curSeg[0], curSeg[1]);
738         result[0] = curSeg[0];
739         result[1] = curSeg[1];
740         result[2] = minDist;
741         result[3] = 0.0;
742         result[4] = 0.0;
743         result[5] = 0.0;
744       } else {
745         // no points in GeneralPath: should not happen in this context
746         throw new IllegalStateException("path without any coordinates");
747       }
748 
749       int segmentIndex = 0;
750       double lastPathLength = 0.0;
751       do {
752         YPoint segmentStart = new YPoint(curSeg[0], curSeg[1]);
753         YPoint segmentEnd = new YPoint(curSeg[2], curSeg[3]);
754         YVector segmentDirection = new YVector(segmentEnd, segmentStart);
755         double segmentLength = segmentDirection.length();
756         pathLength += segmentLength;
757         segmentDirection.norm();
758 
759         YPoint crossing = Geom.calcIntersection(segmentStart, segmentDirection, point, YVector.orthoNormal(segmentDirection));
760         YVector crossingVector = new YVector(crossing, segmentStart);
761 
762         YVector segmentVector = new YVector(segmentEnd, segmentStart);
763         double indexEnd = YVector.scalarProduct(segmentVector, segmentDirection);
764         double indexCrossing = YVector.scalarProduct(crossingVector, segmentDirection);
765 
766         double dist;
767         double segmentRatio;
768         YPoint nearestOnSegment;
769         if (indexCrossing <= 0.0) {
770           dist = YPoint.distance(point, segmentStart);
771           nearestOnSegment = segmentStart;
772           segmentRatio = 0.0;
773         } else if (indexCrossing >= indexEnd) {
774           dist = YPoint.distance(point, segmentEnd);
775           nearestOnSegment = segmentEnd;
776           segmentRatio = 1.0;
777         } else {
778           dist = YPoint.distance(point, crossing);
779           nearestOnSegment = crossing;
780           segmentRatio = indexCrossing / indexEnd;
781         }
782 
783         if (dist < minDist) {
784           minDist = dist;
785           result[0] = nearestOnSegment.getX();
786           result[1] = nearestOnSegment.getY();
787           result[2] = minDist;
788           result[3] = segmentIndex;
789           result[4] = segmentRatio;
790           result[5] = segmentLength * segmentRatio + lastPathLength;
791         }
792 
793         segmentIndex++;
794         lastPathLength = pathLength;
795         pi.next();
796       } while (pi.ok());
797 
798       if(pathLength > 0) {
799         result[5] = result[5] / pathLength;
800       } else {
801         result[5] = 0.0;
802       }
803       return result;
804     }
805 
806     static Point2D getPointForGlobalRatio(EdgeRealizer er, double globalRatio) {
807       GeneralPath path = er.getPath();
808       if(globalRatio > 1.0 || globalRatio < 0.0) {
809         throw new IllegalArgumentException("globalRatio outside of [0,1]");
810       }
811       double totalPathLength = getPathLength(path);
812       double targetPathLength = totalPathLength * globalRatio;
813       CustomPathIterator pi = new CustomPathIterator(path, 1.0);
814       YPoint segmentStart = null, segmentEnd = null;
815       if (pi.isDone()) {
816         // no points in GeneralPath: source and target node overlap
817         // => set the connector point in the middle between their centers
818         return getPointFromEndpoints(er);
819       } else {
820         segmentStart = pi.segmentStart();
821         segmentEnd = pi.segmentEnd();
822       }
823 
824       double currentPathLength = 0.0;
825       double lastPathLength = 0.0;
826       while (pi.ok()) {
827         YVector segmentDirection = new YVector(segmentEnd, segmentStart);
828         double segmentLength = segmentDirection.length();
829         currentPathLength += segmentLength;
830         if(currentPathLength / totalPathLength >= globalRatio) {
831           double remainingLength = targetPathLength - lastPathLength;
832           double localRatio = remainingLength / segmentLength;
833           segmentDirection.scale(localRatio);
834           YPoint targetPoint = YVector.add(segmentStart, segmentDirection);
835           return new Point2D.Double(targetPoint.getX(),targetPoint.getY());
836         }
837 
838         lastPathLength = currentPathLength;
839         pi.next();
840         segmentStart = pi.segmentStart();
841         segmentEnd = pi.segmentEnd();
842       }
843 
844       // we ran past the last point of the path (numeric problems?), return last point
845       return new Point2D.Double(segmentStart.getX(), segmentStart.getY());
846     }
847 
848     static Point2D getPointForLocalRatio(EdgeRealizer er, int segmentIndex, double segmentRatio) {
849       GeneralPath path = er.getPath();
850       if (segmentRatio > 1.0 || segmentRatio < 0.0) {
851         throw new IllegalArgumentException("segmentRatio outside of [0,1]");
852       }
853       CustomPathIterator pi = new CustomPathIterator(path, 1.0);
854       if (pi.isDone()) {
855         // no points in GeneralPath: source and target node overlap
856         // => set the connector point in the middle between their centers
857         return getPointFromEndpoints(er);
858       }
859       int currentIndex = 0;
860       while (pi.ok() && currentIndex < segmentIndex) {
861         pi.next();
862         currentIndex++;
863       }
864       if(currentIndex < segmentIndex)
865       {
866         throw new IllegalArgumentException("found no segment for given segmentIndex");
867       }
868 
869       YPoint segmentStart = pi.segmentStart();
870       YPoint segmentEnd = pi.segmentEnd();
871       YVector segmentDirection = new YVector(segmentEnd, segmentStart);
872       segmentDirection.scale(segmentRatio);
873       YPoint targetPoint = YVector.add(segmentStart, segmentDirection);
874       return new Point2D.Double(targetPoint.getX(), targetPoint.getY());
875     }
876 
877     private static Point2D getPointFromEndpoints(EdgeRealizer er) {
878       final NodeRealizer sourceRealizer = er.getSourceRealizer();
879       double sourceX = sourceRealizer.getCenterX();
880       double sourceY = sourceRealizer.getCenterY();
881       final NodeRealizer targetRealizer = er.getTargetRealizer();
882       double targetX = targetRealizer.getCenterX();
883       double targetY = targetRealizer.getCenterY();
884       return new Point2D.Double((sourceX + targetX) * 0.5, (sourceY + targetY) * 0.5);
885     }
886 
887     private static double getPathLength(GeneralPath path) {
888       double length = 0.0;
889       for(CustomPathIterator pi = new CustomPathIterator(path, 1.0); pi.ok(); pi.next()) {
890         length += pi.segmentDirection().length();
891       }
892       return length;
893     }
894   }
895 
896   /**
897    * Helper class used by PointPathProjector.
898    */
899   static class CustomPathIterator {
900     private double[] cachedSegment;
901     private boolean moreToGet;
902     private PathIterator pathIterator;
903 
904     public CustomPathIterator(GeneralPath path, double flatness) {
905       // copy the path, thus the original may safely change during iteration
906       pathIterator = (new GeneralPath(path)).getPathIterator(null, flatness);
907       cachedSegment = new double[4];
908       getFirstSegment();
909     }
910 
911     public boolean ok()
912     {
913       return moreToGet;
914     }
915 
916     public boolean isDone() {
917       return !moreToGet;
918     }
919 
920     public final double[] segment() {
921       if (moreToGet) {
922         return cachedSegment;
923       } else {
924         return null;
925       }
926     }
927 
928     public YPoint segmentStart() {
929       if(moreToGet) {
930         return new YPoint(cachedSegment[0], cachedSegment[1]);
931       } else {
932         return null;
933       }
934     }
935 
936     public YPoint segmentEnd() {
937       if(moreToGet) {
938         return new YPoint(cachedSegment[2], cachedSegment[3]);
939       } else {
940         return null;
941       }
942     }
943 
944     public YVector segmentDirection() {
945       if(moreToGet) {
946         return new YVector(segmentEnd(), segmentStart());
947       } else {
948         return null;
949       }
950     }
951 
952     public void next() {
953       if (!pathIterator.isDone()) {
954         float[] curSeg = new float[2];
955         cachedSegment[0] = cachedSegment[2];
956         cachedSegment[1] = cachedSegment[3];
957         pathIterator.currentSegment(curSeg);
958         cachedSegment[2] = curSeg[0];
959         cachedSegment[3] = curSeg[1];
960         pathIterator.next();
961       } else {
962         moreToGet = false;
963       }
964     }
965 
966     private void getFirstSegment() {
967       float[] curSeg = new float[2];
968       if (!pathIterator.isDone()) {
969         pathIterator.currentSegment(curSeg);
970         cachedSegment[0] = curSeg[0];
971         cachedSegment[1] = curSeg[1];
972         pathIterator.next();
973         moreToGet = true;
974       } else {
975         moreToGet = false;
976       }
977       if (!pathIterator.isDone()) {
978         pathIterator.currentSegment(curSeg);
979         cachedSegment[2] = curSeg[0];
980         cachedSegment[3] = curSeg[1];
981         pathIterator.next();
982         moreToGet = true;
983       } else {
984         moreToGet = false;
985       }
986     }
987   }
988 
989   /**
990    * This {@link GraphCopier.CopyFactory} handles edge connectors for cut/copy/paste.
991    */
992   private class EdgeConnectorGraphCopyFactory implements GraphCopier.CopyFactory {
993     private final GraphCopier.CopyFactory copyFactory;
994     private final HashMap node2connector;
995 
996     private EdgeConnectorGraphCopyFactory(GraphCopier.CopyFactory copyFactory) {
997       this.copyFactory = copyFactory;
998       node2connector = new HashMap();
999     }
1000
1001    public Node copyNode(Graph targetGraph, Node originalNode) {
1002      return copyFactory.copyNode(targetGraph, originalNode);
1003    }
1004
1005    public Edge copyEdge(Graph targetGraph, Node newSource, Node newTarget, Edge originalEdge) {
1006      return copyFactory.copyEdge(targetGraph, newSource, newTarget, originalEdge);
1007    }
1008
1009    public Graph createGraph() {
1010      return copyFactory.createGraph();
1011    }
1012
1013    public void preCopyGraphData(Graph sourceGraph, Graph targetGraph) {
1014      copyFactory.preCopyGraphData(sourceGraph, targetGraph);
1015    }
1016
1017    /**
1018     * After copying the (sub-) graph, also the edge connector information needs to be stored/updated. That way,
1019     * copies of edge connector nodes still behave like connectors.
1020     */
1021    public void postCopyGraphData(Graph sourceGraph, Graph targetGraph, Map nodeMap, Map edgeMap) {
1022      copyFactory.postCopyGraphData(sourceGraph, targetGraph, nodeMap, edgeMap);
1023
1024      // check if the source graph is the graph in the current view to see if it is a cut/copy or paste action
1025      if (sourceGraph == view.getGraph2D()) {
1026        // cut/copy
1027        // store the connector information from the source nodes for the nodes in the copied subgraph
1028        node2connector.clear();
1029        for (NodeCursor nc = sourceGraph.nodes(); nc.ok(); nc.next()) {
1030          final Node sourceNode = nc.node();
1031          if (EdgeConnectorManager.isEdgeConnector(sourceNode)) {
1032            final Node targetNode = (Node) nodeMap.get(sourceNode);
1033            if (targetNode != null) {
1034              final Edge sourceEdge = EdgeConnectorManager.getEdgeConnection(sourceNode);
1035              final Edge targetEdge = (Edge) edgeMap.get(sourceEdge);
1036              final double ratio = EdgeConnectorManager.getEdgeConnectionRatio(sourceNode);
1037              node2connector.put(targetNode, Tuple.create(targetEdge, new Double(ratio)));
1038            }
1039          }
1040        }
1041
1042        // make sure only edge connectors on existing edges and with incoming and outgoing edges are copied
1043        for (NodeCursor nc = targetGraph.nodes(); nc.ok(); nc.next()) {
1044          final Node targetNode = nc.node();
1045          if (node2connector.containsKey(targetNode)
1046              && (targetNode.degree() == 0 || ((Tuple) node2connector.get(targetNode)).o1 == null)) {
1047            targetGraph.removeNode(targetNode);
1048          }
1049        }
1050      } else {
1051        // paste
1052        // apply the stored connector information of the copied subgraph to the target nodes in the graph of the view
1053        for (NodeCursor nc = sourceGraph.nodes(); nc.ok(); nc.next()) {
1054          final Node sourceNode = nc.node();
1055          if (node2connector.containsKey(sourceNode)) {
1056            final Node targetNode = (Node) nodeMap.get(sourceNode);
1057            final Tuple connector = (Tuple) node2connector.get(sourceNode);
1058            final Edge sourceEdge = (Edge) connector.o1;
1059            final Edge targetEdge = (Edge) edgeMap.get(sourceEdge);
1060            EdgeConnectorManager.addEdgeConnection(targetNode, targetEdge, ((Double) connector.o2).doubleValue());
1061          }
1062        }
1063      }
1064    }
1065  }
1066
1067  public static void main(String[] args) {
1068    EventQueue.invokeLater(new Runnable() {
1069      public void run() {
1070        Locale.setDefault(Locale.ENGLISH);
1071        initLnF();
1072        (new EdgeConnectorDemo()).start();
1073      }
1074    });
1075  }
1076}
1077