]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram/src/org/simantics/diagram/participant/RouteGraphConnectTool.java
4cccea938270b6b8ac0b50baf6ac91d2b190528a
[simantics/platform.git] / bundles / org.simantics.diagram / src / org / simantics / diagram / participant / RouteGraphConnectTool.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2016 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *     Semantum Oy - Fixed bug #6364
12  *******************************************************************************/
13 package org.simantics.diagram.participant;
14
15 import java.awt.AlphaComposite;
16 import java.awt.Composite;
17 import java.awt.Shape;
18 import java.awt.geom.AffineTransform;
19 import java.awt.geom.Point2D;
20 import java.awt.geom.Rectangle2D;
21 import java.util.ArrayDeque;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Deque;
25 import java.util.HashSet;
26 import java.util.Iterator;
27 import java.util.Set;
28
29 import org.simantics.Simantics;
30 import org.simantics.db.Resource;
31 import org.simantics.db.WriteGraph;
32 import org.simantics.db.common.request.WriteRequest;
33 import org.simantics.db.exception.DatabaseException;
34 import org.simantics.diagram.connection.RouteGraph;
35 import org.simantics.diagram.connection.RouteGraphConnectionClass;
36 import org.simantics.diagram.connection.RouteLine;
37 import org.simantics.diagram.connection.RoutePoint;
38 import org.simantics.diagram.connection.RouteTerminal;
39 import org.simantics.diagram.connection.delta.RouteGraphDelta;
40 import org.simantics.diagram.connection.rendering.IRouteGraphRenderer;
41 import org.simantics.diagram.connection.rendering.arrows.ArrowLineEndStyle;
42 import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle;
43 import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle;
44 import org.simantics.diagram.synchronization.ISynchronizationContext;
45 import org.simantics.diagram.synchronization.SynchronizationHints;
46 import org.simantics.diagram.synchronization.graph.RouteGraphConnection;
47 import org.simantics.g2d.canvas.ICanvasContext;
48 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
49 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
50 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
51 import org.simantics.g2d.connection.ConnectionEntity;
52 import org.simantics.g2d.connection.IConnectionAdvisor;
53 import org.simantics.g2d.diagram.DiagramHints;
54 import org.simantics.g2d.diagram.DiagramUtils;
55 import org.simantics.g2d.diagram.IDiagram;
56 import org.simantics.g2d.diagram.handler.PickRequest;
57 import org.simantics.g2d.diagram.handler.Topology.Connection;
58 import org.simantics.g2d.diagram.handler.Topology.Terminal;
59 import org.simantics.g2d.diagram.participant.ElementPainter;
60 import org.simantics.g2d.diagram.participant.TerminalPainter;
61 import org.simantics.g2d.diagram.participant.TerminalPainter.TerminalHoverStrategy;
62 import org.simantics.g2d.diagram.participant.pointertool.AbstractMode;
63 import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;
64 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil;
65 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalInfo;
66 import org.simantics.g2d.element.ElementClass;
67 import org.simantics.g2d.element.ElementClasses;
68 import org.simantics.g2d.element.ElementHints;
69 import org.simantics.g2d.element.ElementUtils;
70 import org.simantics.g2d.element.IElement;
71 import org.simantics.g2d.element.IElementClassProvider;
72 import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;
73 import org.simantics.g2d.element.handler.SceneGraph;
74 import org.simantics.g2d.element.handler.TerminalTopology;
75 import org.simantics.g2d.element.handler.impl.BranchPointTerminal;
76 import org.simantics.g2d.element.impl.Element;
77 import org.simantics.g2d.elementclass.FlagClass;
78 import org.simantics.g2d.elementclass.FlagHandler;
79 import org.simantics.g2d.participant.TransformUtil;
80 import org.simantics.g2d.utils.geom.DirectionSet;
81 import org.simantics.scenegraph.g2d.G2DParentNode;
82 import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
83 import org.simantics.scenegraph.g2d.events.KeyEvent;
84 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
85 import org.simantics.scenegraph.g2d.events.MouseEvent;
86 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent;
87 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
88 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
89 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
90 import org.simantics.scenegraph.g2d.events.command.Commands;
91 import org.simantics.scenegraph.g2d.nodes.BranchPointNode;
92 import org.simantics.scenegraph.g2d.nodes.ConnectionNode;
93 import org.simantics.scenegraph.g2d.nodes.connection.RouteGraphNode;
94 import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
95 import org.simantics.scenegraph.utils.GeometryUtils;
96 import org.simantics.structural2.modelingRules.ConnectionJudgement;
97 import org.simantics.utils.datastructures.Pair;
98 import org.simantics.utils.datastructures.Triple;
99 import org.simantics.utils.logging.TimeLogger;
100 import org.simantics.utils.ui.ErrorLogger;
101 import org.simantics.utils.ui.ExceptionUtils;
102
103 import gnu.trove.map.hash.THashMap;
104
105 /**
106  * A basic tool for making connection on diagrams.
107  * 
108  * This version defines the starting, ending and route points of a connection.
109  * The routing itself is left up to the diagram router employed by
110  * {@link DiagramUtils#validateAndFix(IDiagram, ICanvasContext)}.
111  * 
112  * Manual:
113  * 
114  * This tool is added to the diagram when a connection sequence is initiated by
115  * another participant. PointerInteractor is one such participant which adds the
116  * tool when a terminal or non-terminal-occupied canvas space is ALT+clicked
117  * (see {@link PointerInteractor#checkInitiateConnectTool(MouseEvent, Point2D)}
118  * ). The connection will be finished when another allowed terminal is clicked
119  * upon or empty canvas space is ALT+clicked. Route points for the connection
120  * can be created by clicking around on non-terminal-occupied canvas space while
121  * connecting.
122  * 
123  * <p>
124  * Connections can be started from and ended in flags by pressing ALT while
125  * left-clicking.
126  * 
127  * @author Tuukka Lehtonen
128  */
129 public class RouteGraphConnectTool extends AbstractMode {
130
131     private static final String      END_TERMINAL_DATA     = "END";
132
133     public static final int          PAINT_PRIORITY        = ElementPainter.ELEMENT_PAINT_PRIORITY + 5;
134
135     @Dependency
136     protected TransformUtil          util;
137
138     @Dependency
139     protected ElementPainter         diagramPainter;
140
141     @Dependency
142     protected PointerInteractor      pi;
143
144     /**
145      * Starting point designation.
146      * 
147      * The value is received by the constructor.
148      */
149     protected RouteGraphTarget       startingPoint;
150
151     protected TerminalInfo           startTerminal         = new TerminalInfo();
152
153     /**
154      * <code>true</code> if this tool should create connection continuation
155      * flags, <code>false</code> otherwise.
156      */
157     protected boolean                createFlags;
158
159     /**
160      * 
161      */
162     protected IElementClassProvider  elementClassProvider;
163
164     /**
165      * 
166      */
167     protected Deque<ControlPoint>    controlPoints         = new ArrayDeque<ControlPoint>();
168
169     /**
170      * Element terminal of connection end element. <code>null</code> if
171      * connection cannot be ended where it is currently being attempted to end.
172      */
173     protected TerminalInfo           endTerminal;
174
175     /**
176      * The latest connectability judgment from the active
177      * {@link IConnectionAdvisor} should the connection happen between
178      * {@link #startTerminal} and {@link #endTerminal}.
179      */
180     protected ConnectionJudgement    connectionJudgment;
181
182     /**
183      * A temporary variable for use with
184      * {@link TerminalTopology#getTerminals(IElement, Collection)}.
185      */
186     protected Collection<Terminal>   terminals             = new ArrayList<Terminal>();
187
188     /**
189      * Previous mouse canvas position recorded by
190      * {@link #processMouseMove(MouseMovedEvent)}.
191      */
192     protected Point2D                lastMouseCanvasPos    = new Point2D.Double();
193
194     /**
195      * Set to true once {@link #processMouseMove(MouseMovedEvent)} has been
196      * invoked at least once. This is used to tell whether to allow creation of
197      * branch points or finising the connection in thin air. It will not be
198      * allowed if the mouse has not moved at all since starting the connection.
199      */
200     protected boolean                mouseHasMoved         = false;
201
202     protected TerminalHoverStrategy  originalStrategy      = null;
203
204     protected TerminalHoverStrategy  terminalHoverStrategy = new TerminalHoverStrategy() {
205         @Override
206         public boolean highlightEnabled() {
207             return !isEndingInFlag();
208         }
209
210         @Override
211         public boolean highlight(TerminalInfo ti) {
212             return canConnect(ti.e, ti.t) != null;
213         }
214     };
215
216     protected final static Composite ALPHA_COMPOSITE       = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f);
217
218     /**
219      * Root scene graph node for all visualization performed by this tool.
220      */
221     protected ConnectionNode         ghostNode;
222
223     protected RouteGraphNode           rgNode;
224
225     protected  RouteGraph               routeGraph;
226     protected  RouteTerminal            endRouteTerminal;
227     private ILineEndStyle            endTerminalStyle;
228
229     /**
230      * Indicates whether the connection is about to be ended into a new
231      * flag/branchpoint or not.
232      */
233     protected TerminalInfo           endFlag;
234
235     protected G2DParentNode          endFlagNode;
236
237     private RouteLine                attachedToRouteLine;
238
239     protected RouteGraph               beforeRouteGraph;
240
241     private IRouteGraphRenderer      beforeRenderer;
242
243     private boolean                  beforeEditable;
244
245     protected RouteGraphDelta          routeGraphDelta;
246
247     private Set<Pair<IElement, Terminal>> alreadyConnected;
248     
249     /**
250      * @param startElement
251      * @param routeGraphConnection
252      * @param mouseId
253      * @param startCanvasPos
254      */
255     public RouteGraphConnectTool(RouteGraphTarget startingPoint, int mouseId) {
256         super(mouseId);
257
258         Point2D intersection = startingPoint.getIntersectionPosition();
259
260         this.startingPoint = startingPoint;
261         this.lastMouseCanvasPos.setLocation(intersection);
262
263         BranchPointTerminal t = BranchPointTerminal.existingTerminal(
264                 AffineTransform.getTranslateInstance(intersection.getX(), intersection.getY()),
265                 DirectionSet.ANY,
266                 BranchPointNode.SHAPE);
267
268         Point2D p = startingPoint.getIntersectionPosition();
269         AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY());
270
271         startTerminal.e = startingPoint.getElement();
272         startTerminal.t = t;
273         startTerminal.posElem = at;
274         startTerminal.posDia = at;
275         startTerminal.shape = t.getShape();
276
277         controlPoints.add( newControlPoint( startingPoint.getIntersectionPosition() ) );
278         controlPoints.add( newControlPoint( startingPoint.getCanvasPosition() ) );
279
280         alreadyConnected = new HashSet<Pair<IElement,Terminal>>();
281         IElement connection = startingPoint.getElement();
282         ConnectionEntity ce = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY);
283         Collection<Connection> tcs = ce.getTerminalConnections(null);
284         for (Connection tc : tcs)
285             alreadyConnected.add(Pair.make(tc.node, tc.terminal));
286     }
287
288     @Override
289     public void addedToContext(ICanvasContext ctx) {
290         super.addedToContext(ctx);
291
292         // Force terminals to always be highlighted without pressing certain
293         // keys or key combinations.
294         originalStrategy = getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
295         setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
296     }
297
298     @Override
299     protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) {
300         if (newDiagram != null) {
301             // Get IElementClassProvider
302             ISynchronizationContext ctx = newDiagram.getHint(SynchronizationHints.CONTEXT);
303             if (ctx != null) {
304                 this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER);
305             }
306
307             // See if flags should be created or not.
308             this.createFlags = Boolean.TRUE.equals(newDiagram.getHint(DiagramHints.KEY_USE_CONNECTION_FLAGS));
309         }
310     }
311
312     @Override
313     public void removedFromContext(ICanvasContext ctx) {
314         if (getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY) == terminalHoverStrategy) {
315             if (originalStrategy != null)
316                 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, originalStrategy);
317             else
318                 removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
319         }
320
321         super.removedFromContext(ctx);
322     }
323
324     int straightDirections(RouteLine line) {
325         return line.isHorizontal()
326                 ? (RouteTerminal.DIR_DOWN | RouteTerminal.DIR_UP)
327                 : (RouteTerminal.DIR_LEFT | RouteTerminal.DIR_RIGHT);
328     }
329
330     private static RouteTerminal addTerminal(Object data, RouteGraph rg, double x, double y, Rectangle2D bounds, int allowedDirections, ILineEndStyle style) {
331         RouteTerminal rt;
332         if (bounds != null)
333             rt = rg.addTerminal(x, y, bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY(), allowedDirections, style);
334         else
335             rt = rg.addTerminal(x, y, x, y, x, y, allowedDirections, style);
336         rt.setData( data );
337         return rt;
338     }
339
340     private RouteTerminal setEndTerminal(double x, double y, Rectangle2D bounds, int allowedDirections) {
341
342         // First add then remove to prevent deletion of route lines in case there are 2 only terminals
343         
344         RouteTerminal toRemove = endRouteTerminal;
345         
346         endRouteTerminal = addTerminal( END_TERMINAL_DATA, routeGraph, x, y, bounds, allowedDirections, endTerminalStyle );
347         routeGraph.link( attachedToRouteLine, endRouteTerminal );
348         
349         if (toRemove != null)
350             routeGraph.remove(toRemove);
351         
352         return endRouteTerminal;
353         
354     }
355
356     protected void setEndTerminal(TerminalInfo ti) {
357         Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double());
358         GeometryUtils.expandRectangle(bounds, 2);
359         int dir = RouteGraphConnectionClass.shortestDirectionOutOfBounds(
360                 ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds);
361         setEndTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds, dir);
362     }
363
364     @SGInit
365     public void initSG(G2DParentNode parent) {
366         ghostNode = parent.addNode("branched connection", ConnectionNode.class);
367         //ghostNode.setComposite(AlphaComposite.SrcOver.derive(0.5f));
368         ghostNode.setZIndex(PAINT_PRIORITY);
369
370         rgNode = ghostNode.addNode("branch", RouteGraphNode.class);
371
372         double ex = startingPoint.getCanvasPosition().getX();
373         double ey = startingPoint.getCanvasPosition().getY();
374
375         beforeRouteGraph = startingPoint.getNode().getRouteGraph();
376         beforeEditable = startingPoint.getNode().isEditable();
377
378         RouteGraphNode beforeRgNode = startingPoint.getNode();
379         beforeRenderer = beforeRgNode.getRenderer();
380
381         rgNode.setRenderer(beforeRenderer);
382         rgNode.setEditable(false);
383
384         beforeRgNode.setEditable(beforeEditable);
385         beforeRgNode.setRenderer(null);
386         
387         initRG(ex, ey);
388         
389     }
390     
391     public void initRG(double ex, double ey) {
392         
393         THashMap<Object, Object> map = new THashMap<Object, Object>();
394         routeGraph = beforeRouteGraph.copy(map);
395
396         endTerminalStyle = PlainLineEndStyle.INSTANCE;
397         for (RouteTerminal t : routeGraph.getTerminals()) {
398             if (t.getRenderStyle() instanceof ArrowLineEndStyle) {
399                 endTerminalStyle = t.getRenderStyle();
400                 break;
401             }
402         }
403
404         attachedToRouteLine = (RouteLine) map.get(startingPoint.getLine());
405         routeGraph.makePersistent(attachedToRouteLine);
406         for (RouteLine line : routeGraph.getAllLines()) {
407             if (!line.isTransient() && line.isHorizontal() == attachedToRouteLine.isHorizontal()
408                     && line.getPosition() == attachedToRouteLine.getPosition()) {
409                 attachedToRouteLine = line;
410                 break;
411             }
412         }
413         routeGraphDelta = new RouteGraphDelta(beforeRouteGraph, routeGraph);
414
415 //        beforeRouteGraph.print();
416 //        routeGraph.print();
417 //        routeGraphDelta.print();
418
419         setEndTerminal(ex, ey, null, 0xf);
420
421         rgNode.setRouteGraph(routeGraph);
422         
423     }
424     
425     @SGCleanup
426     public void cleanupSG() {
427         RouteGraphNode beforeRgNode = startingPoint.getNode();
428         beforeRgNode.setRouteGraph(beforeRouteGraph);
429         beforeRgNode.setRenderer(beforeRenderer);
430         beforeRgNode.setEditable(beforeEditable);
431
432         ghostNode.remove();
433         ghostNode = null;
434
435         setDirty();
436     }
437
438     @EventHandler(priority = 200)
439     public boolean handleCommandEvents(CommandEvent ce) {
440         if (ce.command.equals(Commands.CANCEL)) {
441             setDirty();
442             remove();
443             return true;
444         } else if (ce.command.equals(Commands.ROTATE_ELEMENT_CCW) || ce.command.equals(Commands.ROTATE_ELEMENT_CW)) {
445             // TODO: rotate flag?
446         }
447         return false;
448     }
449
450     @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
451     public boolean handleKeyEvents(KeyEvent ke) {
452         if (ke instanceof KeyPressedEvent) {
453             // Back-space, cancel prev bend
454             if (ke.keyCode == java.awt.event.KeyEvent.VK_BACK_SPACE)
455                 return cancelPreviousBend();
456         }
457
458         if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) {
459             if (createFlags) {
460                 endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(ke instanceof KeyPressedEvent));
461                 return true;
462             }
463         }
464
465         return false;
466     }
467
468     @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
469     public boolean handleEvent(MouseEvent me) {
470         // Only handle events for the connection-initiating mouse
471         if (me.mouseId != mouseId)
472             return false;
473
474         if (me instanceof MouseMovedEvent)
475             return processMouseMove((MouseMovedEvent) me);
476
477         if (me instanceof MouseButtonPressedEvent)
478             return processMouseButtonPress((MouseButtonPressedEvent) me);
479
480         return false;
481     }
482
483     protected boolean processMouseMove(MouseMovedEvent me) {
484         mouseHasMoved = true;
485
486         Point2D mouseControlPos = me.controlPosition;
487         Point2D mouseCanvasPos = util.controlToCanvas(mouseControlPos, new Point2D.Double());
488
489         ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
490         if (snapAdvisor != null)
491             snapAdvisor.snap(mouseCanvasPos);
492
493         // Record last snapped canvas position of mouse.
494         this.lastMouseCanvasPos.setLocation(mouseCanvasPos);
495
496         if (isEndingInFlag()) {
497             endFlagNode.setTransform(AffineTransform.getTranslateInstance(mouseCanvasPos.getX(), mouseCanvasPos.getY()));
498         }
499
500         TerminalInfo ti = pi.pickTerminal(me.controlPosition);
501         if (ti != null) {
502             Object canConnect = canConnect(ti.e, ti.t);
503             if (canConnect != null) {
504                 connectionJudgment = (ConnectionJudgement) canConnect;
505
506                 if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) {
507                     controlPoints.getLast()
508                     .setPosition(ti.posDia)
509                     .setAttachedToTerminal(ti);
510
511                     endTerminal = ti;
512
513                     connect(ti);
514                     
515                 }
516
517                 // Make sure that we are ending with a flag if ALT is pressed
518                 // and no end terminal is defined.
519                 endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me));
520
521                 updateSG(new Point2D.Double(ti.posDia.getTranslateX(), ti.posDia.getTranslateY()));
522                 return false;
523             }
524         }
525
526         connectionJudgment = null;
527         if (isEndTerminalDefined()) {
528             // CASE: Mouse was previously on top of a valid terminal to end
529             // the connection. Now the mouse has been moved where there is
530             // no longer a terminal to connect to.
531             //
532             // => Disconnect the last edge segment from the previous
533             // terminal, mark endElement/endTerminal non-existent
534             // and connect the disconnected edge to a new branch point.
535
536             disconnect(mouseCanvasPos);
537             
538             controlPoints.getLast()
539             .setPosition(mouseCanvasPos)
540             .setAttachedToTerminal(null);
541
542             endTerminal = null;
543         } else {
544             // CASE: Mouse was not previously on top of a valid ending
545             // element terminal.
546             //
547             // => Move and re-orient last branch point.
548
549             controlPoints.getLast()
550             .setPosition(mouseCanvasPos);
551         }
552
553         // Make sure that we are ending with a flag if ALT is pressed and no end
554         // terminal is defined.
555         endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me));
556
557         updateSG(lastMouseCanvasPos);
558
559         return false;
560     }
561
562     protected void connect(TerminalInfo ti) {
563         setEndTerminal(ti);
564     }
565     
566     protected void disconnect(Point2D mouseCanvasPos) {
567         setEndTerminal(mouseCanvasPos.getX(), mouseCanvasPos.getY(), null, 0xf);
568     }
569     
570     protected boolean processMouseButtonPress(MouseButtonPressedEvent e) {
571         MouseButtonEvent me = e;
572
573         // Do nothing before the mouse has moved at least a little.
574         // This prevents the user from ending the connection right where
575         // it started.
576         if (!mouseHasMoved)
577             return true;
578
579         if (me.button == MouseEvent.LEFT_BUTTON) {
580             Point2D mouseControlPos = me.controlPosition;
581             Point2D mouseCanvasPos = util.getInverseTransform().transform(mouseControlPos, new Point2D.Double());
582
583             ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
584             if (snapAdvisor != null)
585                 snapAdvisor.snap(mouseCanvasPos);
586
587             // Clicked on an allowed end terminal. End connection & end mode.
588             if (isEndTerminalDefined()) {
589                 createConnection();
590                 remove();
591                 return true;
592             } else {
593                 // Finish connection in thin air only if the
594                 // connection was started from a valid terminal.
595                 if ((me.stateMask & MouseEvent.ALT_MASK) != 0 && startTerminal != null) {
596                     connectionJudgment = (ConnectionJudgement) canConnect(null, null);
597                     if (connectionJudgment == null)
598                         return true;
599                     createConnection();
600                     remove();
601                     return true;
602                 } else if (routePointsAllowed()
603                         && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) {
604                     // Add new connection control point.
605                     controlPoints.add(newControlPoint(mouseCanvasPos));
606                     updateSG(mouseCanvasPos);
607                 }
608             }
609
610             // Eat the event to prevent other participants from doing
611             // incompatible things while in this connection mode.
612             return true;
613         } else if (me.button == MouseEvent.RIGHT_BUTTON) {
614             return cancelPreviousBend();
615         }
616
617         return false;
618     }
619
620     protected boolean cancelPreviousBend() {
621         if (!routePointsAllowed())
622             return false;
623
624         // Just to make this code more comprehensible, prevent an editing
625         // case that requires ugly code to work.
626         if (isEndingInFlag())
627             return true;
628
629         // If there are no real route points, cancel whole connection.
630         if (controlPoints.size() <= 2) {
631             setDirty();
632             remove();
633             return true;
634         }
635
636         // Cancel last bend
637         controlPoints.removeLast();
638         controlPoints.getLast().setPosition(lastMouseCanvasPos);
639
640         updateSG(lastMouseCanvasPos);
641         return true;
642     }
643
644     protected void updateSG(Point2D mousePos) {
645         routeGraph.setLocation(endRouteTerminal, mousePos.getX(), mousePos.getY());
646         //routeGraph.print(System.err);
647         setDirty();
648     }
649
650     protected boolean shouldEndWithFlag(MouseEvent me) {
651         return shouldEndWithFlag((me.stateMask & MouseEvent.ALT_MASK) != 0);
652     }
653
654     protected boolean shouldEndWithFlag(boolean altPressed) {
655         return altPressed && !isEndTerminalDefined() && createFlags;
656     }
657
658     protected boolean isEndTerminalDefined() {
659         return endTerminal != null;
660     }
661
662     protected boolean isFlagTerminal(TerminalInfo ti) {
663         return ti.e.getElementClass().containsClass(FlagHandler.class);
664     }
665
666
667     protected boolean isEndingInFlag() {
668         return endFlag != null;
669     }
670
671     protected void endWithoutTerminal(Point2D mousePos, boolean altDown) {
672         // Just go with branch points if flags are not allowed.
673         if (!createFlags)
674             return;
675
676         boolean endTerminalDefined = isEndTerminalDefined();
677
678         if (altDown) {
679             if (!isEndingInFlag()) {
680                 endFlag = createFlag(EdgeEnd.End);
681                 endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos);
682                 controlPoints.getLast()
683                 .setAttachedToTerminal(endFlag);
684
685                 // TerminalPainter must refresh
686                 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
687
688                 updateSG(mousePos);
689             }
690         } else {
691             if (isEndingInFlag()) {
692                 // Currently ending with flag but ALT is no longer down
693                 // so that flag must be removed.
694                 endFlag = null;
695                 endFlagNode.remove();
696                 endFlagNode = null;
697
698                 ControlPoint cp = controlPoints.getLast();
699                 cp.setAttachedToTerminal(endTerminal);
700
701                 if (endTerminalDefined) {
702                     cp.setPosition(endTerminal.posDia);
703                 } else {
704                     cp.setPosition(mousePos);
705                 }
706
707                 // Force Terminalpainter refresh
708                 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
709
710                 updateSG(mousePos);
711             }
712         }
713     }
714
715     private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, Point2D pos) {
716         return showElement(parent, nodeId, element, AffineTransform.getTranslateInstance(pos.getX(), pos.getY()));
717     }
718
719     private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, AffineTransform tr) {
720         G2DParentNode elementParent = parent.getOrCreateNode(nodeId, G2DParentNode.class);
721         elementParent.setTransform(tr);
722         elementParent.removeNodes();
723         for (SceneGraph sg : element.getElementClass().getItemsByClass(SceneGraph.class))
724             sg.init(element, elementParent);
725         return elementParent;
726     }
727
728     /**
729      * @param canvasPos
730      * @return
731      */
732     protected ControlPoint newControlPoint(Point2D canvasPos) {
733         return new ControlPoint(canvasPos);
734     }
735
736     protected Triple<RouteGraph,RouteGraph,RouteGraphDelta> prepareRouteGraphDelta() {
737         // Prevent route graph connection synchronization from crashing on the
738         // transient route terminal that doesn't exist yet. It is created after
739         // persisting the route line, which is the only purpose of this route
740         // graph synchronization.
741         routeGraph.remove(endRouteTerminal);
742         return Triple.make(beforeRouteGraph, routeGraph, routeGraphDelta);
743     }
744
745     protected void createConnection() {
746         TimeLogger.resetTimeAndLog(getClass(), "createConnection");
747         final ConnectionJudgement judgment = this.connectionJudgment;
748         if (judgment == null) {
749             ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity", null);
750             return;
751         }
752
753         final ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
754         final Triple<RouteGraph,RouteGraph,RouteGraphDelta> rgs = prepareRouteGraphDelta();
755
756         Simantics.getSession().asyncRequest(new WriteRequest() {
757             @Override
758             public void perform(WriteGraph graph) throws DatabaseException {
759                 graph.markUndoPoint();
760                 Resource connection = ElementUtils.getObject(startTerminal.e);
761                 if (!rgs.third.isEmpty()) {
762                     new RouteGraphConnection(graph, connection).synchronize(graph, rgs.first, rgs.second, rgs.third);
763                 }
764                 Resource attachToLine = RouteGraphConnection.deserialize(graph, attachedToRouteLine.getData());
765                 builder.attachToRouteGraph(graph, judgment, connection, attachToLine, controlPoints, endTerminal, FlagClass.Type.Out);
766             }
767         }, e -> {
768             if (e != null)
769                 ExceptionUtils.logAndShowError(e);
770         });
771     }
772
773     protected boolean routePointsAllowed() {
774         return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS));
775     }
776
777     protected Object canConnect(IElement endElement, Terminal endTerminal) {
778         IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);
779         return canConnect(advisor, endElement, endTerminal);
780     }
781
782     protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) {
783         if (advisor == null)
784             return ConnectionJudgement.CANBEMADELEGAL;
785         if (alreadyConnected.contains(Pair.make(endElement, endTerminal)))
786             return null;
787         return advisor.canBeConnected(null, startTerminal.e, startTerminal.t, endElement, endTerminal);
788     }
789
790     protected static FlagClass.Type endToFlagType(EdgeEnd end) {
791         switch (end) {
792             case Begin:
793                 return FlagClass.Type.In;
794             case End:
795                 return FlagClass.Type.Out;
796             default:
797                 throw new IllegalArgumentException("unrecognized edge end: " + end);
798         }
799     }
800
801     protected TerminalInfo createFlag(EdgeEnd connectionEnd) {
802         ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG);
803         IElement e = Element.spawnNew(flagClass);
804
805         e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd));
806         e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal);
807
808         TerminalInfo ti = new TerminalInfo();
809         ti.e = e;
810         ti.t = ElementUtils.getSingleTerminal(e);
811         ti.posElem = TerminalUtil.getTerminalPosOnElement(e, ti.t);
812         ti.posDia = TerminalUtil.getTerminalPosOnDiagram(e, ti.t);
813
814         return ti;
815     }
816
817     // ------------------------------------------------------------------------
818
819     static RouteGraphTarget pickRouteGraphConnection(IDiagram diagram, Shape pickShape, double pickDistance) {
820         ArrayList<IElement> elements = new ArrayList<IElement>();
821         PickRequest req = new PickRequest(pickShape);
822         DiagramUtils.pick(diagram, req, elements);
823         for (Iterator<IElement> it = elements.iterator(); it.hasNext();) {
824             IElement e = it.next();
825             RouteGraphNode rgn = e.getHint(RouteGraphConnectionClass.KEY_RG_NODE);
826             if (rgn == null || rgn.getRouteGraph() == null)
827                 it.remove();
828         }
829         if (elements.isEmpty())
830             return null;
831
832         Rectangle2D pickRect = pickShape.getBounds2D();
833         final double x = pickRect.getCenterX();
834         final double y = pickRect.getCenterY();
835
836         return pickNearestRouteGraphConnection(elements, x, y, pickDistance);
837     }
838
839     private static RouteGraphTarget pickNearestRouteGraphConnection(ArrayList<IElement> elements, double x, double y, double pd) {
840         // Find the nearest distance at which we get hits.
841         double hi = pd + 1;
842         double lo = hi * .01;
843         double limit = 0.5;
844         while (true) {
845             double delta = (hi - lo);
846             if (delta <= limit)
847                 break;
848
849             pd = (lo + hi) * .5;
850
851             boolean hit = false;
852             for (IElement connection : elements) {
853                 RouteGraphNode rgn = connection.getHint(RouteGraphConnectionClass.KEY_RG_NODE);
854                 RouteLine line = rgn.getRouteGraph().pickLine(x, y, pd);
855                 if (line != null) {
856                     hit = true;
857                     break;
858                 }
859             }
860
861             if (hit)
862                 hi = pd;
863             else
864                 lo = pd;
865         }
866
867         // Now that the nearest hitting distance is found, find the nearest intersection.
868         RouteGraphTarget nearestTarget = null;
869         double nearest = Double.MAX_VALUE;
870         for (IElement connection : elements) {
871             RouteGraphNode rgn = connection.getHint(RouteGraphConnectionClass.KEY_RG_NODE);
872             RouteLine line = rgn.getRouteGraph().pickLine(x, y, pd);
873             if (line == null)
874                 continue;
875
876             Point2D intersection = intersectionPoint(x, y, line);
877             if (intersection == null)
878                 continue;
879
880             double dx = intersection.getX() - x;
881             double dy = intersection.getY() - y;
882             double dist = Math.sqrt(dx*dx + dy*dy);
883             if (dist < nearest) {
884                 nearest = dist;
885                 nearestTarget = new RouteGraphTarget(connection, rgn, line, new Point2D.Double(x, y), intersection);
886             }
887         }
888
889         return nearestTarget;
890     }
891
892     static Point2D intersectionPoint(double x, double y, RouteLine line) {
893         Collection<RoutePoint> points = line.getPoints();
894         if (points.size() < 2)
895             return null;
896         RoutePoint s = line.getBegin();
897         RoutePoint e = line.getEnd();
898         return GeometryUtils.intersectionToLine(s.getX(), s.getY(), e.getX(), e.getY(), x, y);
899     }
900
901 }