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