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