]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram/src/org/simantics/diagram/participant/ConnectTool2.java
ConnectionTool fixes for overlapping terminals
[simantics/platform.git] / bundles / org.simantics.diagram / src / org / simantics / diagram / participant / ConnectTool2.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 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  *******************************************************************************/
12 package org.simantics.diagram.participant;
13
14 import java.awt.AlphaComposite;
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Composite;
18 import java.awt.geom.AffineTransform;
19 import java.awt.geom.Path2D;
20 import java.awt.geom.Point2D;
21 import java.awt.geom.Rectangle2D;
22 import java.util.ArrayDeque;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.Deque;
28 import java.util.Iterator;
29 import java.util.List;
30
31 import org.simantics.Simantics;
32 import org.simantics.db.ReadGraph;
33 import org.simantics.db.Resource;
34 import org.simantics.db.WriteGraph;
35 import org.simantics.db.common.request.UniqueRead;
36 import org.simantics.db.common.request.WriteRequest;
37 import org.simantics.db.common.utils.NameUtils;
38 import org.simantics.db.exception.DatabaseException;
39 import org.simantics.diagram.connection.RouteGraph;
40 import org.simantics.diagram.connection.RouteGraphConnectionClass;
41 import org.simantics.diagram.connection.RouteLine;
42 import org.simantics.diagram.connection.RouteTerminal;
43 import org.simantics.diagram.connection.delta.RouteGraphDelta;
44 import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle;
45 import org.simantics.diagram.content.ResourceTerminal;
46 import org.simantics.diagram.stubs.DiagramResource;
47 import org.simantics.diagram.synchronization.ISynchronizationContext;
48 import org.simantics.diagram.synchronization.SynchronizationHints;
49 import org.simantics.diagram.synchronization.graph.RouteGraphConnection;
50 import org.simantics.g2d.canvas.ICanvasContext;
51 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
52 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
53 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
54 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
55 import org.simantics.g2d.connection.IConnectionAdvisor;
56 import org.simantics.g2d.diagram.DiagramHints;
57 import org.simantics.g2d.diagram.DiagramUtils;
58 import org.simantics.g2d.diagram.IDiagram;
59 import org.simantics.g2d.diagram.handler.PickContext;
60 import org.simantics.g2d.diagram.handler.Topology.Terminal;
61 import org.simantics.g2d.diagram.participant.ElementPainter;
62 import org.simantics.g2d.diagram.participant.TerminalPainter;
63 import org.simantics.g2d.diagram.participant.TerminalPainter.TerminalHoverStrategy;
64 import org.simantics.g2d.diagram.participant.pointertool.AbstractMode;
65 import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;
66 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil;
67 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalInfo;
68 import org.simantics.g2d.element.ElementClass;
69 import org.simantics.g2d.element.ElementClasses;
70 import org.simantics.g2d.element.ElementUtils;
71 import org.simantics.g2d.element.IElement;
72 import org.simantics.g2d.element.IElementClassProvider;
73 import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;
74 import org.simantics.g2d.element.handler.SceneGraph;
75 import org.simantics.g2d.element.handler.TerminalTopology;
76 import org.simantics.g2d.element.handler.impl.BranchPointTerminal;
77 import org.simantics.g2d.element.impl.Element;
78 import org.simantics.g2d.elementclass.BranchPoint;
79 import org.simantics.g2d.elementclass.BranchPoint.Direction;
80 import org.simantics.g2d.elementclass.FlagClass;
81 import org.simantics.g2d.elementclass.FlagHandler;
82 import org.simantics.g2d.participant.RenderingQualityInteractor;
83 import org.simantics.g2d.participant.TransformUtil;
84 import org.simantics.g2d.utils.geom.DirectionSet;
85 import org.simantics.modeling.ModelingResources;
86 import org.simantics.scenegraph.g2d.G2DParentNode;
87 import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
88 import org.simantics.scenegraph.g2d.events.KeyEvent;
89 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
90 import org.simantics.scenegraph.g2d.events.MouseEvent;
91 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent;
92 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
93 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
94 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
95 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
96 import org.simantics.scenegraph.g2d.events.command.Commands;
97 import org.simantics.scenegraph.g2d.nodes.BranchPointNode;
98 import org.simantics.scenegraph.g2d.nodes.ShapeNode;
99 import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
100 import org.simantics.scenegraph.utils.GeometryUtils;
101 import org.simantics.scenegraph.utils.Quality;
102 import org.simantics.structural2.modelingRules.ConnectionJudgement;
103 import org.simantics.utils.datastructures.Pair;
104 import org.simantics.utils.logging.TimeLogger;
105 import org.simantics.utils.ui.ErrorLogger;
106 import org.simantics.utils.ui.ExceptionUtils;
107
108 import gnu.trove.map.hash.THashMap;
109
110 /**
111  * A basic tool for making connection on diagrams.
112  * 
113  * This version defines the starting, ending and route points of a connection.
114  * The routing itself is left up to the diagram router employed by
115  * {@link DiagramUtils#validateAndFix(IDiagram, ICanvasContext)}.
116  * 
117  * Manual:
118  * 
119  * This tool is added to the diagram when a connection sequence is initiated by
120  * another participant. PointerInteractor is one such participant which adds the
121  * tool when a terminal or non-terminal-occupied canvas space is ALT+clicked
122  * (see {@link PointerInteractor#checkInitiateConnectTool(MouseEvent, Point2D)}
123  * ). The connection will be finished when another allowed terminal is clicked
124  * upon or empty canvas space is ALT+clicked. Route points for the connection
125  * can be created by clicking around on non-terminal-occupied canvas space while
126  * connecting.
127  * 
128  * <p>
129  * Connections can be started from and ended in flags by pressing ALT while
130  * left-clicking.
131  * 
132  * @author Tuukka Lehtonen
133  */
134 public class ConnectTool2 extends AbstractMode {
135
136     public static final int          PAINT_PRIORITY        = ElementPainter.ELEMENT_PAINT_PRIORITY + 5;
137
138     @Reference
139     protected RenderingQualityInteractor quality;
140
141     @Dependency
142     protected TransformUtil          util;
143
144     @Dependency
145     protected ElementPainter         diagramPainter;
146
147     @Dependency
148     protected PointerInteractor      pi;
149
150     @Dependency
151     protected PickContext            pickContext;
152
153     /**
154      * Start element terminal of the connection. <code>null</code> if connection
155      * was started from a flag or a branch point.
156      * 
157      * The value is received by the constructor.
158      */
159     protected List<TerminalInfo>     startTerminals;
160
161     /**
162      * Refers to any of the possible overlapping start terminals. The value is
163      * taken from the first index of {@link #startTerminals} assuming that the
164      * first one is the nearest. It is <code>null</code> if
165      * {@link #startTerminals} is empty.
166      */
167     protected TerminalInfo           startTerminal;
168
169     protected TerminalInfo           startFlag;
170
171     /**
172      * Starting position of the connection, received as an external argument.
173      */
174     protected final Point2D          startPos;
175
176     /**
177      * <code>true</code> if this tool should create connection continuation
178      * flags, <code>false</code> otherwise.
179      */
180     protected boolean                createFlags;
181
182     /**
183      * 
184      */
185     protected IElementClassProvider  elementClassProvider;
186
187     /**
188      * 
189      */
190     protected Deque<ControlPoint>    controlPoints         = new ArrayDeque<ControlPoint>();
191
192     /**
193      * Contains <code>null</code> when a connection is started from a new flag
194      * or one of the terminals in {@link #startTerminals} when a connection is
195      * being created starting from a terminal or possibly a set of terminals.
196      * 
197      * <p>
198      * Note that this is different from {@link #startTerminal} which simply
199      * represents the first element of {@link #startTerminals}.
200      * 
201      * <p>
202      * Only when this value and {@link #endTerminal} is properly set will a
203      * connection be created between two element terminals.
204      */
205     protected TerminalInfo           selectedStartTerminal;
206
207     /**
208      * Element terminal of connection end element. <code>null</code> if
209      * connection cannot be ended where it is currently being attempted to end.
210      */
211     protected TerminalInfo           endTerminal;
212
213     /**
214      * The latest connectability judgment from the active
215      * {@link IConnectionAdvisor} should the connection happen between
216      * {@link #selectedStartTerminal} and {@link #endTerminal}.
217      */
218     protected ConnectionJudgement    connectionJudgment;
219
220     /**
221      * The latest connectability judgment from the active
222      * {@link IConnectionAdvisor} should the connection happen between
223      * {@link #selectedStartTerminal} and {@link #lastRouteGraphTarget}.
224      */
225     protected ConnectionJudgement    attachToConnectionJudgement;
226
227     /**
228      * If non-null during connection drawing this field tells the direction
229      * forced for the current branch point by the user through the UI commands
230      * {@link Commands#ROTATE_ELEMENT_CCW} and
231      * {@link Commands#ROTATE_ELEMENT_CW}.
232      */
233     private Direction                forcedBranchPointDirection;
234
235     /**
236      * A temporary variable for use with
237      * {@link TerminalTopology#getTerminals(IElement, Collection)}.
238      */
239     protected Collection<Terminal>   terminals             = new ArrayList<Terminal>();
240
241     /**
242      * Previous mouse canvas position recorded by
243      * {@link #processMouseMove(MouseMovedEvent)}.
244      */
245     protected Point2D                lastMouseCanvasPos    = new Point2D.Double();
246
247     /**
248      * Set to true once {@link #processMouseMove(MouseMovedEvent)} has been
249      * invoked at least once. This is used to tell whether to allow creation of
250      * branch points or finising the connection in thin air. It will not be
251      * allowed if the mouse has not moved at all since starting the connection.
252      */
253     protected boolean                mouseHasMoved         = false;
254
255     protected TerminalHoverStrategy  originalStrategy      = null;
256
257     protected TerminalHoverStrategy  terminalHoverStrategy = new TerminalHoverStrategy() {
258         @Override
259         public boolean highlightEnabled() {
260             return !isEndingInFlag();
261         }
262
263         @Override
264         public boolean highlight(TerminalInfo ti) {
265             boolean reflexive = isStartTerminal(ti.e, ti.t);
266             if (reflexive && !allowReflexiveConnections())
267                 return false;
268
269             return canConnect(ti.e, ti.t) != null;
270         }
271     };
272
273     protected final static Composite ALPHA_COMPOSITE       = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f);
274
275     /**
276      * Root scene graph node for all visualization performed by this tool.
277      */
278     protected G2DParentNode          ghostNode;
279
280     /**
281      * Indicates whether the connection is about to be ended into a new
282      * flag/branchpoint or not.
283      */
284     protected TerminalInfo           endFlag;
285
286     protected G2DParentNode          endFlagNode;
287
288     private RouteGraphTarget         lastRouteGraphTarget;
289
290     /**
291      * @param startTerminal
292      * @param mouseId
293      * @param startCanvasPos
294      */
295     public ConnectTool2(TerminalInfo startTerminal, int mouseId, Point2D startCanvasPos) {
296         this(startTerminal == null ? Collections.<TerminalInfo> emptyList()
297                 : Collections.singletonList(startTerminal),
298                 mouseId,
299                 startCanvasPos);
300     }
301
302     /**
303      * @param startTerminals
304      * @param mouseId
305      * @param startCanvasPos
306      */
307     public ConnectTool2(List<TerminalInfo> startTerminals, int mouseId, Point2D startCanvasPos) {
308         super(mouseId);
309
310         if (startCanvasPos == null)
311             throw new NullPointerException("null start position");
312         if (startTerminals == null)
313             throw new NullPointerException("null start terminals");
314
315         this.startPos = startCanvasPos;
316         this.lastMouseCanvasPos.setLocation(startPos);
317
318         this.startTerminals = startTerminals;
319         this.startTerminal = startTerminals.isEmpty() ? null : startTerminals.get(0);
320     }
321
322     @Override
323     public void addedToContext(ICanvasContext ctx) {
324         super.addedToContext(ctx);
325
326         if (quality != null)
327             quality.setStaticQuality(Quality.LOW);
328
329         // Force terminals to always be highlighted without pressing certain
330         // keys or key combinations.
331         originalStrategy = getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
332         setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
333     }
334
335     @Override
336     protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) {
337         if (newDiagram != null) {
338             // Get IElementClassProvider
339             ISynchronizationContext ctx = newDiagram.getHint(SynchronizationHints.CONTEXT);
340             if (ctx != null) {
341                 this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER);
342             }
343
344             // See if flags should be created or not.
345             this.createFlags = Boolean.TRUE.equals(newDiagram.getHint(DiagramHints.KEY_USE_CONNECTION_FLAGS));
346             startConnection();
347         }
348     }
349
350     @Override
351     public void removedFromContext(ICanvasContext ctx) {
352         if (getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY) == terminalHoverStrategy) {
353             if (originalStrategy != null)
354                 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, originalStrategy);
355             else
356                 removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
357         }
358
359         if (quality != null)
360             quality.setStaticQuality(null);
361
362         super.removedFromContext(ctx);
363     }
364
365     protected void startConnection() {
366         Point2D startPos = (Point2D) this.startPos.clone();
367         ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
368         if (snapAdvisor != null)
369             snapAdvisor.snap(startPos);
370
371         // Resolve the first element and terminal of the connection.
372         ControlPoint start = new ControlPoint(startPos);
373
374         if (startTerminal != null) {
375             assert ElementUtils.peekDiagram(startTerminal.e) == diagram;
376             Point2D terminalPos = new Point2D.Double(startTerminal.posDia.getTranslateX(),
377                     startTerminal.posDia.getTranslateY());
378             start.setPosition(terminalPos).setAttachedToTerminal(startTerminal);
379         } else {
380             // Create TerminalInfo describing the flag to be created.
381             if (createFlags) {
382                 // This prevents connection creation from creating a branch
383                 // point in place of this flag.
384                 startFlag = createFlag(EdgeEnd.Begin);
385                 start.setAttachedToTerminal(startFlag);
386                 showElement(ghostNode, "startFlag", startFlag.e, startPos);
387             }
388         }
389         controlPoints.add(start);
390         controlPoints.add(new ControlPoint(startPos));
391
392         // Make sure that we are ending with a flag if ALT is pressed.
393         // This makes the tool always start with a flag which can be quite
394         // cumbersome and is therefore disabled. The current version will not
395         // end the connection if the mouse has not moved at all.
396         //if (keyUtil.isKeyPressed(java.awt.event.KeyEvent.VK_ALT)) {
397         //    endWithoutTerminal(lastMouseCanvasPos, true);
398         //}
399     }
400
401     @SGInit
402     public void initSG(G2DParentNode parent) {
403         ghostNode = parent.addNode(G2DParentNode.class);
404         ghostNode.setZIndex(PAINT_PRIORITY);
405
406         ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class);
407         pathNode.setColor(new Color(160, 0, 0));
408         pathNode.setStroke(new BasicStroke(0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10,
409                 new float[] { 0.5f, 0.2f }, 0));
410         pathNode.setScaleStroke(false);
411         pathNode.setZIndex(0);
412
413         G2DParentNode points = ghostNode.getOrCreateNode("points", G2DParentNode.class);
414         points.setZIndex(1);
415
416         updateSG();
417     }
418
419     private RouteTerminal addControlPoint(RouteGraph routeGraph, ControlPoint cp) {
420         TerminalInfo ti = cp.getAttachedTerminal();
421         if(ti != null && ti != startFlag && ti != endFlag) {
422             Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double());
423             GeometryUtils.expandRectangle(bounds, 2);
424             int allowedDirections = RouteGraphConnectionClass.shortestDirectionOutOfBounds(
425                     ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds);
426             return routeGraph.addTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(),
427                     bounds, allowedDirections, PlainLineEndStyle.INSTANCE);
428         }
429         else {
430             double x = cp.getPosition().getX();
431             double y = cp.getPosition().getY();
432             int allowedDirections = 0xf;
433             switch(cp.getDirection()) {
434             case Horizontal: allowedDirections = 5; break;
435             case Vertical: allowedDirections = 10; break;
436             case Any: allowedDirections = 15; break;
437             }
438             return routeGraph.addTerminal(x, y, x, y, x, y, allowedDirections);
439         }
440     }
441     
442     protected void updateSG() {
443         if (controlPoints.size() != 2)
444             return;
445
446         ControlPoint begin = controlPoints.getFirst();
447         ControlPoint end = controlPoints.getLast();
448
449         RouteGraph routeGraph = new RouteGraph();
450         RouteTerminal a = addControlPoint(routeGraph, begin);
451         RouteTerminal b = addControlPoint(routeGraph, end);
452         routeGraph.link(a, b);
453
454         Path2D path = routeGraph.getPath2D();
455
456         // Create scene graph to visualize the connection.
457         ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class);
458         pathNode.setShape(path);
459
460         setDirty();
461     }
462
463     private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, Point2D pos) {
464         return showElement(parent, nodeId, element, AffineTransform.getTranslateInstance(pos.getX(), pos.getY()));
465     }
466
467     private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, AffineTransform tr) {
468         G2DParentNode elementParent = parent.getOrCreateNode(nodeId, G2DParentNode.class);
469         elementParent.setTransform(tr);
470         elementParent.removeNodes();
471         for (SceneGraph sg : element.getElementClass().getItemsByClass(SceneGraph.class))
472             sg.init(element, elementParent);
473         return elementParent;
474     }
475
476     @SGCleanup
477     public void cleanupSG() {
478         ghostNode.remove();
479         ghostNode = null;
480     }
481
482     @EventHandler(priority = 200)
483     public boolean handleCommandEvents(CommandEvent ce) {
484         if (ce.command.equals(Commands.CANCEL)) {
485             setDirty();
486             remove();
487             return true;
488         } else if (ce.command.equals(Commands.ROTATE_ELEMENT_CCW) || ce.command.equals(Commands.ROTATE_ELEMENT_CW)) {
489             return rotateLastBranchPoint(ce.command.equals(Commands.ROTATE_ELEMENT_CW));
490         }
491         return false;
492     }
493
494     @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
495     public boolean handleKeyEvents(KeyEvent ke) {
496         if (ke instanceof KeyPressedEvent) {
497             // Back-space, cancel prev bend
498             if (ke.keyCode == java.awt.event.KeyEvent.VK_BACK_SPACE)
499                 return cancelPreviousBend();
500         }
501
502         if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) {
503             if (createFlags) {
504                 endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(ke instanceof KeyPressedEvent));
505                 return true;
506             }
507         }
508
509         return false;
510     }
511
512     @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
513     public boolean handleEvent(MouseEvent me) {
514         // Only handle events for the connection-initiating mouse
515         if (me.mouseId != mouseId)
516             return false;
517
518         if (me instanceof MouseMovedEvent)
519             return processMouseMove((MouseMovedEvent) me);
520
521         if (me instanceof MouseButtonPressedEvent)
522             return processMouseButtonPress((MouseButtonPressedEvent) me);
523
524         // #7653: Support creating connections between terminals without lifting mouse button in between.
525         if (me instanceof MouseButtonReleasedEvent)
526             return processMouseButtonRelease((MouseButtonReleasedEvent) me);
527
528         return false;
529     }
530
531     protected boolean processMouseMove(MouseMovedEvent me) {
532         mouseHasMoved = true;
533
534         Point2D mouseControlPos = me.controlPosition;
535         Point2D mouseCanvasPos = util.controlToCanvas(mouseControlPos, new Point2D.Double());
536
537         ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
538         if (snapAdvisor != null)
539             snapAdvisor.snap(mouseCanvasPos);
540
541         // Record last snapped canvas position of mouse.
542         this.lastMouseCanvasPos.setLocation(mouseCanvasPos);
543
544         if (isEndingInFlag()) {
545             endFlagNode.setTransform(AffineTransform.getTranslateInstance(mouseCanvasPos.getX(), mouseCanvasPos.getY()));
546         }
547
548         List<TerminalInfo> tis = pi.pickTerminals(me.controlPosition);
549         tis = TerminalUtil.findNearestOverlappingTerminals(tis);
550         if (!tis.isEmpty() && !containsStartTerminal(tis)) {
551             //System.out.println("end terminals (" + tis.size() + "):\n" + EString.implode(tis));
552             for (TerminalInfo ti : tis) {
553                 Pair<ConnectionJudgement, TerminalInfo> canConnect = canConnect(ti.e, ti.t);
554                 if (canConnect != null) {
555                     connectionJudgment = canConnect.first;
556
557                     if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) {
558                         if (canConnect.second != null) {
559                             controlPoints.getFirst()
560                             .setPosition(canConnect.second.posDia)
561                             .setAttachedToTerminal(canConnect.second);
562                         }
563                         controlPoints.getLast()
564                         .setPosition(ti.posDia)
565                         .setAttachedToTerminal(ti);
566
567                         selectedStartTerminal = canConnect.second;
568                         endTerminal = ti;
569                     }
570
571                     // Make sure that we are ending with a flag if ALT is pressed
572                     // and no end terminal is defined.
573                     if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
574                         updateSG();
575                     return false;
576                 }
577             }
578         } else {
579             RouteGraphTarget cp = RouteGraphConnectTool.pickRouteGraphConnection(
580                     getContext(),
581                     diagram,
582                     pi.getCanvasPickShape(me.controlPosition),
583                     pi.getPickDistance());
584             if (cp != null) {
585                 // Remove branch point highlight from previously picked route graph.
586                 if (lastRouteGraphTarget != null && cp.getNode() != lastRouteGraphTarget.getNode())
587                     cp.getNode().showBranchPoint(null);
588                 lastRouteGraphTarget = cp;
589
590                 // Validate connection before visualizing connectability
591                 Point2D isectPos = cp.getIntersectionPosition();
592                 TerminalInfo ti = TerminalInfo.create(
593                         isectPos,
594                         cp.getElement(),
595                         BranchPointTerminal.existingTerminal(
596                                 isectPos,
597                                 DirectionSet.ANY,
598                                 BranchPointNode.SHAPE),
599                         BranchPointNode.SHAPE);
600                 Pair<ConnectionJudgement, TerminalInfo> canConnect = canConnect(ti.e, ti.t);
601                 if (canConnect != null) {
602                     attachToConnectionJudgement = canConnect.first;
603                     controlPoints.getLast().setPosition(ti.posDia).setAttachedToTerminal(ti);
604                     endTerminal = ti;
605                     startTerminal = canConnect.second;
606                     cp.getNode().showBranchPoint(isectPos);
607                     if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
608                         updateSG();
609                     return false;
610                 }
611             } else {
612                 if (lastRouteGraphTarget != null) {
613                     lastRouteGraphTarget.getNode().showBranchPoint(null);
614                     lastRouteGraphTarget = null;
615                 }
616             }
617         }
618
619         connectionJudgment = null;
620         attachToConnectionJudgement = null;
621         if (isEndTerminalDefined()) {
622             // CASE: Mouse was previously on top of a valid terminal to end
623             // the connection. Now the mouse has been moved where there is
624             // no longer a terminal to connect to.
625             //
626             // => Disconnect the last edge segment from the previous
627             // terminal, mark endElement/endTerminal non-existent
628             // and connect the disconnected edge to a new branch point.
629
630             controlPoints.getLast()
631             .setPosition(mouseCanvasPos)
632             .setDirection(calculateCurrentBranchPointDirection())
633             .setAttachedToTerminal(null);
634
635             endTerminal = null;
636         } else {
637             // CASE: Mouse was not previously on top of a valid ending
638             // element terminal.
639             //
640             // => Move and re-orient last branch point.
641
642             controlPoints.getLast()
643             .setPosition(mouseCanvasPos)
644             .setDirection(calculateCurrentBranchPointDirection());
645         }
646
647         // Make sure that we are ending with a flag if ALT is pressed and no end
648         // terminal is defined.
649         if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
650             updateSG();
651
652         return false;
653     }
654
655     protected boolean processMouseButtonPress(MouseButtonPressedEvent e) {
656         MouseButtonEvent me = e;
657
658         // Do nothing before the mouse has moved at least a little.
659         // This prevents the user from ending the connection right where
660         // it started.
661         if (!mouseHasMoved)
662             return true;
663
664         if (me.button == MouseEvent.LEFT_BUTTON) {
665             Point2D mouseControlPos = me.controlPosition;
666             Point2D mouseCanvasPos = util.getInverseTransform().transform(mouseControlPos, new Point2D.Double());
667
668             ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
669             if (snapAdvisor != null)
670                 snapAdvisor.snap(mouseCanvasPos);
671
672             if (tryEndConnection()) {
673                 return true;
674             } else {
675                 // Finish connection in thin air only if the
676                 // connection was started from a valid terminal.
677                 if (me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) && !startTerminals.isEmpty()) {
678                     Pair<ConnectionJudgement, TerminalInfo> pair = canConnect(null, null);
679                     if (pair != null) {
680                         connectionJudgment = (ConnectionJudgement) pair.first;
681                         selectedStartTerminal = pair.second;
682 //                        endFlag = createFlag(EdgeEnd.End);
683 //                        controlPoints.getLast().setAttachedToTerminal(endFlag);
684                         createConnection();
685                         setDirty();
686                         remove();
687                     } else {
688                         // Inform the user why connection couldn't be created.
689                         String tmsg = terminalsToString(startTerminals);
690                         ErrorLogger.defaultLogWarning("Can't resolve connection type for new connection when starting from one of the following terminals:\n" + tmsg, null);
691                     }
692                     return true;
693                 } else if (routePointsAllowed()
694                         && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) {
695                     // Add new connection control point.
696                     controlPoints.add(newControlPointWithCalculatedDirection(mouseCanvasPos));
697                     resetForcedBranchPointDirection();
698                     updateSG();
699                 }
700             }
701
702             // Eat the event to prevent other participants from doing
703             // incompatible things while in this connection mode.
704             return true;
705         } else if (me.button == MouseEvent.RIGHT_BUTTON) {
706             return cancelPreviousBend();
707         }
708
709         return false;
710     }
711
712     private int mouseLeftReleaseCount = 0;
713
714     protected boolean processMouseButtonRelease(MouseButtonReleasedEvent me) {
715         if (me.button == MouseEvent.LEFT_BUTTON
716                 && ++mouseLeftReleaseCount == 1) {
717             return tryEndConnection();
718         }
719         return false;
720     }
721
722     /**
723      * @return <code>true</code> if connection was successfully ended
724      */
725     private boolean tryEndConnection() {
726         if (isEndTerminalDefined() && connectionJudgment != null) {
727             createConnection();
728             remove();
729             return true;
730         } else if (lastRouteGraphTarget != null && attachToConnectionJudgement != null) {
731             lastRouteGraphTarget.getNode().showBranchPoint(null);
732             attachToConnection();
733             remove();
734             return true;
735         }
736         return false;
737     }
738
739     private void attachToConnection() {
740         ConnectionJudgement judgment = this.attachToConnectionJudgement;
741         if (judgment == null) {
742             ErrorLogger.defaultLogError("Cannot attach to connection, no judgment available on connection validity", null);
743             return;
744         }
745
746         ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
747         RouteGraph before = lastRouteGraphTarget.getNode().getRouteGraph();
748         THashMap<Object, Object> copyMap = new THashMap<>();
749         RouteGraph after = before.copy(copyMap);
750
751         RouteLine attachTo = (RouteLine) copyMap.get(lastRouteGraphTarget.getLine());
752         after.makePersistent(attachTo);
753         for (RouteLine line : after.getAllLines()) {
754             if (!line.isTransient() && line.isHorizontal() == attachTo.isHorizontal()
755                     && line.getPosition() == attachTo.getPosition()) {
756                 attachTo = line;
757                 break;
758             }
759         }
760         RouteLine attachToLine = attachTo;
761         RouteGraphDelta delta = new RouteGraphDelta(before, after);
762
763         Simantics.getSession().asyncRequest(new WriteRequest() {
764             @Override
765             public void perform(WriteGraph graph) throws DatabaseException {
766                 graph.markUndoPoint();
767                 Resource connection = ElementUtils.getObject(endTerminal.e);
768                 if (!delta.isEmpty()) {
769                     new RouteGraphConnection(graph, connection).synchronize(graph, before, after, delta);
770                 }
771                 Resource line = RouteGraphConnection.deserialize(graph, attachToLine.getData());
772                 Deque<ControlPoint> cps = new ArrayDeque<>();
773                 for (Iterator<ControlPoint> iterator = controlPoints.descendingIterator(); iterator.hasNext();)
774                     cps.add(iterator.next());
775                 builder.attachToRouteGraph(graph, judgment, connection, line, cps, startTerminal, FlagClass.Type.In);
776             }
777         }, parameter -> {
778             if (parameter != null)
779                 ExceptionUtils.logAndShowError(parameter);
780         });
781     }
782
783     protected boolean cancelPreviousBend() {
784         if (!routePointsAllowed())
785             return false;
786
787         // Just to make this code more comprehensible, prevent an editing
788         // case that requires ugly code to work.
789         if (isEndingInFlag())
790             return true;
791
792         // If there are no real route points, cancel whole connection.
793         if (controlPoints.size() <= 2) {
794             setDirty();
795             remove();
796             return true;
797         }
798
799         // Cancel last bend
800         controlPoints.removeLast();
801         controlPoints.getLast().setPosition(lastMouseCanvasPos);
802         resetForcedBranchPointDirection();
803
804         updateSG();
805         return true;
806     }
807
808     /**
809      * Rotates the last branch point in the created connection in either
810      * clockwise or counter-clockwise direction as a response to a user
811      * interaction.
812      * 
813      * <p>
814      * At the same time it use {@link #forcedBranchPointDirection} to mark the
815      * current last branch point to be forcefully oriented according to the
816      * users wishes instead of calculating a default value for the orientation
817      * from the routed connection path. See
818      * {@link #calculateCurrentBranchPointDirection()} for more information on
819      * this.
820      * 
821      * <p>
822      * The logic of this method goes as follows:
823      * <ul>
824      * <li>Calculate the current branch point direction</li>
825      * <li>If the branch point direction is currently user selected (
826      * {@link #forcedBranchPointDirection}</li>
827      * <li></li>
828      * <li></li>
829      * </ul>
830      * 
831      * @param clockwise
832      * @return <code>true</code> if the rotation was successful
833      */
834     protected boolean rotateLastBranchPoint(boolean clockwise) {
835         Direction oldDir = calculateCurrentBranchPointDirection();
836
837         if (forcedBranchPointDirection == null) {
838             forcedBranchPointDirection = oldDir.toggleDetermined();
839         } else {
840             forcedBranchPointDirection = clockwise ? oldDir.cycleNext() : oldDir.cyclePrevious();
841         }
842
843         controlPoints.getLast().setDirection(forcedBranchPointDirection);
844
845         updateSG();
846
847         return true;
848     }
849
850     /**
851      * Set preferred direction for a branch/route point element.
852      * 
853      * @param branchPoint the element to set the direction for
854      * @param direction the direction to set
855      * @return
856      */
857     protected void setDirection(IElement branchPoint, Direction direction) {
858         branchPoint.getElementClass().getSingleItem(BranchPoint.class).setDirectionPreference(branchPoint, direction);
859     }
860
861     protected Direction forcedBranchPointDirection() {
862         return forcedBranchPointDirection;
863     }
864
865     protected void resetForcedBranchPointDirection() {
866         forcedBranchPointDirection = null;
867     }
868
869     protected void forceBranchPointDirection(Direction direction) {
870         forcedBranchPointDirection = direction;
871     }
872
873     /**
874      * @return
875      */
876     protected Direction calculateCurrentBranchPointDirection() {
877         // If this is not the first branch point, toggle direction compared to
878         // last.
879         if (forcedBranchPointDirection != null)
880             return forcedBranchPointDirection;
881
882         if (controlPoints.size() > 2) {
883             // This is not the first edge segment, toggle route point
884             // directions.
885             Iterator<ControlPoint> it = controlPoints.descendingIterator();
886             it.next();
887             ControlPoint secondLastCp = it.next();
888
889             Direction dir = secondLastCp.getDirection();
890             switch (dir) {
891                 case Horizontal:
892                     return Direction.Vertical;
893                 case Vertical:
894                     return Direction.Horizontal;
895                 case Any:
896             }
897         }
898
899         // If this is the first branch point, calculate based on edge segment
900         // angle.
901         if (controlPoints.size() > 1) {
902             Iterator<ControlPoint> it = controlPoints.descendingIterator();
903             ControlPoint last = it.next();
904             ControlPoint secondLast = it.next();
905
906             double angle = Math.atan2(Math.abs(last.getPosition().getY() - secondLast.getPosition().getY()),
907                     Math.abs(last.getPosition().getX() - secondLast.getPosition().getX()));
908
909             if (angle >= 0 && angle < Math.PI / 4) {
910                 return Direction.Horizontal;
911             } else if (angle > Math.PI / 4 && angle <= Math.PI / 2) {
912                 return Direction.Vertical;
913             }
914         }
915
916         return Direction.Any;
917     }
918
919     protected boolean isEndingInFlag() {
920         return endFlag != null;
921     }
922
923     /**
924      * @param mousePos
925      * @param altDown
926      * @return <code>true</code> if updateSG was executed, <code>false</code>
927      *         otherwise
928      */
929     protected boolean endWithoutTerminal(Point2D mousePos, boolean altDown) {
930         // Just go with branch points if flags are not allowed.
931         if (!createFlags)
932             return false;
933
934         boolean endTerminalDefined = isEndTerminalDefined();
935
936         if (altDown) {
937             if (!isEndingInFlag()) {
938                 endFlag = createFlag(EdgeEnd.End);
939                 endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos);
940                 controlPoints.getLast()
941                 .setDirection(calculateCurrentBranchPointDirection())
942                 .setAttachedToTerminal(endFlag);
943
944                 // TerminalPainter must refresh
945                 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
946
947                 updateSG();
948                 return true;
949             }
950         } else {
951             if (isEndingInFlag()) {
952                 // Currently ending with flag but ALT is no longer down
953                 // so that flag must be removed.
954                 endFlag = null;
955                 endFlagNode.remove();
956                 endFlagNode = null;
957
958                 ControlPoint cp = controlPoints.getLast();
959                 cp.setDirection(calculateCurrentBranchPointDirection())
960                 .setAttachedToTerminal(endTerminal);
961
962                 if (endTerminalDefined) {
963                     cp.setPosition(endTerminal.posDia);
964                 } else {
965                     cp.setPosition(mousePos);
966                 }
967
968                 // Force TerminalPainter refresh
969                 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
970
971                 updateSG();
972                 return true;
973             }
974         }
975         return false;
976     }
977
978     protected void createConnection() {
979         createConnection(
980                 this.selectedStartTerminal,
981                 this.endTerminal,
982                 this.connectionJudgment,
983                 this.controlPoints);
984     }
985
986     protected void createConnection(
987             final TerminalInfo startTerminal,
988             final TerminalInfo endTerminal,
989             final ConnectionJudgement judgement,
990             final Deque<ControlPoint> controlPoints)
991     {
992         TimeLogger.resetTimeAndLog(getClass(), "createConnection");
993         if (judgement == null) {
994             // Inform the user why connection couldn't be created.
995             String tmsg = terminalsToString(Arrays.asList(startTerminal, endTerminal));
996             ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity when connecting the terminals:\n" + tmsg, null);
997             return;
998         }
999
1000         final ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
1001
1002         Simantics.getSession().asyncRequest(new WriteRequest() {
1003             @Override
1004             public void perform(WriteGraph graph) throws DatabaseException {
1005                 builder.create(graph, judgement, controlPoints, startTerminal, endTerminal);
1006             }
1007         }, parameter -> {
1008             if (parameter != null)
1009                 ExceptionUtils.logAndShowError(parameter);
1010         });
1011     }
1012
1013     /**
1014      * @param canvasPos
1015      * @return
1016      */
1017     protected ControlPoint newControlPointWithCalculatedDirection(Point2D canvasPos) {
1018         return new ControlPoint(canvasPos, calculateCurrentBranchPointDirection());
1019     }
1020
1021     /**
1022      * @param e
1023      * @param t
1024      * @return <code>true</code> if the specified element terminal matches any
1025      *         TerminalInfo in {@link #startTerminals}
1026      */
1027     protected boolean isStartTerminal(IElement e, Terminal t) {
1028         if (startTerminal == null)
1029             return false;
1030         for (TerminalInfo st : startTerminals) {
1031             if (st.e == e && st.t == t) {
1032                 return true;
1033             }
1034         }
1035         return false;
1036     }
1037
1038     /**
1039      * @param e
1040      * @param t
1041      * @return <code>true</code> if the specified element terminal matches any
1042      *         TerminalInfo in {@link #startTerminals}
1043      */
1044     protected boolean containsStartTerminal(List<TerminalInfo> tis) {
1045         if (startTerminal == null)
1046             return false;
1047         for (TerminalInfo st : startTerminals) {
1048             for (TerminalInfo et : tis) {
1049                 if (st.e == et.e && st.t == et.t) {
1050                     return true;
1051                 }
1052             }
1053         }
1054         return false;
1055     }
1056
1057     protected static FlagClass.Type endToFlagType(EdgeEnd end) {
1058         switch (end) {
1059             case Begin:
1060                 return FlagClass.Type.In;
1061             case End:
1062                 return FlagClass.Type.Out;
1063             default:
1064                 throw new IllegalArgumentException("unrecognized edge end: " + end);
1065         }
1066     }
1067
1068     protected TerminalInfo createFlag(EdgeEnd connectionEnd) {
1069         ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG);
1070         IElement e = Element.spawnNew(flagClass);
1071
1072         e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd));
1073         e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal);
1074
1075         TerminalInfo ti = new TerminalInfo();
1076         ti.e = e;
1077         ti.t = ElementUtils.getSingleTerminal(e);
1078         ti.posElem = TerminalUtil.getTerminalPosOnElement(e, ti.t);
1079         ti.posDia = TerminalUtil.getTerminalPosOnDiagram(e, ti.t);
1080
1081         return ti;
1082     }
1083
1084     protected boolean shouldEndWithFlag(MouseEvent me) {
1085         return shouldEndWithFlag( me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) );
1086     }
1087
1088     protected boolean shouldEndWithFlag(boolean altPressed) {
1089         return altPressed && !isEndTerminalDefined() && createFlags && startFlag == null;
1090     }
1091
1092     protected boolean isEndTerminalDefined() {
1093         return endTerminal != null;
1094     }
1095
1096     protected boolean isFlagTerminal(TerminalInfo ti) {
1097         return ti.e.getElementClass().containsClass(FlagHandler.class);
1098     }
1099
1100     protected boolean allowReflexiveConnections() {
1101         return false;
1102     }
1103
1104     protected boolean routePointsAllowed() {
1105         return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS));
1106     }
1107
1108     /**
1109      * @param endElement
1110      * @param endTerminal
1111      * @return
1112      */
1113     @SuppressWarnings("unchecked")
1114     protected final Pair<ConnectionJudgement, TerminalInfo> canConnect(IElement endElement, Terminal endTerminal) {
1115         IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);
1116         Object judgement = canConnect(advisor, endElement, endTerminal);
1117         if (judgement == null)
1118             return null;
1119         if (judgement instanceof Pair<?, ?>)
1120             return (Pair<ConnectionJudgement, TerminalInfo>) judgement;
1121         return Pair.<ConnectionJudgement, TerminalInfo>make((ConnectionJudgement) judgement, startTerminal);
1122     }
1123
1124     protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) {
1125         if (advisor == null)
1126             return Pair.make(ConnectionJudgement.CANBEMADELEGAL, startTerminal);
1127         if (startTerminals.isEmpty()) {
1128             ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, null, null, endElement, endTerminal);
1129             return obj != null ? Pair.<ConnectionJudgement, TerminalInfo>make(obj, null) : null;
1130         }
1131         for (TerminalInfo st : startTerminals) {
1132             ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, st.e, st.t, endElement, endTerminal);
1133             if (obj != null) {
1134                 return Pair.make(obj, st);
1135             }
1136         }
1137         return null;
1138     }
1139
1140     /**
1141      * For generating debugging information of what was attempted by the user
1142      * when a connection couldn't be created.
1143      * 
1144      * @param ts
1145      * @return
1146      */
1147     private String terminalsToString(final Iterable<TerminalInfo> ts) {
1148         try {
1149             return Simantics.sync(new UniqueRead<String>() {
1150                 @Override
1151                 public String perform(ReadGraph graph) throws DatabaseException {
1152                     DiagramResource DIA = DiagramResource.getInstance(graph);
1153                     ModelingResources MOD = ModelingResources.getInstance(graph);
1154                     StringBuilder sb = new StringBuilder();
1155                     boolean first = true;
1156                     for (TerminalInfo ti : ts) {
1157                         if (!first)
1158                             sb.append("\n");
1159                         first = false;
1160                         sb.append("element ");
1161                         Object o = ElementUtils.getObject(ti.e);
1162                         if (o instanceof Resource) {
1163                             Resource er = (Resource) o;
1164                             Resource cer = graph.getPossibleObject(er, MOD.ElementToComponent);
1165                             Resource r = cer != null ? cer : er;
1166                             sb.append(NameUtils.getSafeName(graph, r)).append(" : ");
1167                             for (Resource type : graph.getPrincipalTypes(r)) {
1168                                 sb.append(NameUtils.getSafeName(graph, type, true));
1169                             }
1170                         } else {
1171                             sb.append(ti.e.toString());
1172                         }
1173                         sb.append(", terminal ");
1174                         if (ti.t instanceof ResourceTerminal) {
1175                             Resource tr = ((ResourceTerminal) ti.t).getResource();
1176                             Resource cp = graph.getPossibleObject(tr, DIA.HasConnectionPoint);
1177                             Resource r = cp != null ? cp : tr;
1178                             sb.append(NameUtils.getSafeName(graph, r, true));
1179                         } else {
1180                             sb.append(ti.t.toString());
1181                         }
1182                     }
1183                     return sb.toString();
1184                 }
1185             });
1186         } catch (DatabaseException e) {
1187             return e.getMessage();
1188         }
1189     }
1190
1191 }