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