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