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
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
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
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
104 import gnu.trove.map.hash.THashMap;
\r
107 * A basic tool for making connection on diagrams.
\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
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
125 * Connections can be started from and ended in flags by pressing ALT while
\r
128 * @author Tuukka Lehtonen
\r
130 public class RouteGraphConnectTool extends AbstractMode {
\r
132 private static final String END_TERMINAL_DATA = "END";
\r
134 public static final int PAINT_PRIORITY = ElementPainter.ELEMENT_PAINT_PRIORITY + 5;
\r
137 protected TransformUtil util;
\r
140 protected ElementPainter diagramPainter;
\r
143 protected PointerInteractor pi;
\r
146 * Starting point designation.
\r
148 * The value is received by the constructor.
\r
150 protected RouteGraphTarget startingPoint;
\r
152 protected TerminalInfo startTerminal = new TerminalInfo();
\r
155 * <code>true</code> if this tool should create connection continuation
\r
156 * flags, <code>false</code> otherwise.
\r
158 protected boolean createFlags;
\r
163 protected IElementClassProvider elementClassProvider;
\r
168 protected Deque<ControlPoint> controlPoints = new ArrayDeque<ControlPoint>();
\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
174 protected TerminalInfo endTerminal;
\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
181 protected ConnectionJudgement connectionJudgment;
\r
184 * A temporary variable for use with
\r
185 * {@link TerminalTopology#getTerminals(IElement, Collection)}.
\r
187 protected Collection<Terminal> terminals = new ArrayList<Terminal>();
\r
190 * Previous mouse canvas position recorded by
\r
191 * {@link #processMouseMove(MouseMovedEvent)}.
\r
193 protected Point2D lastMouseCanvasPos = new Point2D.Double();
\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
201 protected boolean mouseHasMoved = false;
\r
203 protected TerminalHoverStrategy originalStrategy = null;
\r
205 protected TerminalHoverStrategy terminalHoverStrategy = new TerminalHoverStrategy() {
\r
207 public boolean highlightEnabled() {
\r
208 return !isEndingInFlag();
\r
212 public boolean highlight(TerminalInfo ti) {
\r
213 return canConnect(ti.e, ti.t) != null;
\r
217 protected final static Composite ALPHA_COMPOSITE = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f);
\r
220 * Root scene graph node for all visualization performed by this tool.
\r
222 protected ConnectionNode ghostNode;
\r
224 protected RouteGraphNode rgNode;
\r
226 protected RouteGraph routeGraph;
\r
227 protected RouteTerminal endRouteTerminal;
\r
228 private ILineEndStyle endTerminalStyle;
\r
231 * Indicates whether the connection is about to be ended into a new
\r
232 * flag/branchpoint or not.
\r
234 protected TerminalInfo endFlag;
\r
236 protected G2DParentNode endFlagNode;
\r
238 private RouteLine attachedToRouteLine;
\r
240 protected RouteGraph beforeRouteGraph;
\r
242 private IRouteGraphRenderer beforeRenderer;
\r
244 private boolean beforeEditable;
\r
246 protected RouteGraphDelta routeGraphDelta;
\r
248 private Set<Pair<IElement, Terminal>> alreadyConnected;
\r
251 * @param startElement
\r
252 * @param routeGraphConnection
\r
254 * @param startCanvasPos
\r
256 public RouteGraphConnectTool(RouteGraphTarget startingPoint, int mouseId) {
\r
259 Point2D intersection = startingPoint.getIntersectionPosition();
\r
261 this.startingPoint = startingPoint;
\r
262 this.lastMouseCanvasPos.setLocation(intersection);
\r
264 BranchPointTerminal t = BranchPointTerminal.existingTerminal(
\r
265 AffineTransform.getTranslateInstance(intersection.getX(), intersection.getY()),
\r
267 BranchPointNode.SHAPE);
\r
269 Point2D p = startingPoint.getIntersectionPosition();
\r
270 AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY());
\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
278 controlPoints.add( newControlPoint( startingPoint.getIntersectionPosition() ) );
\r
279 controlPoints.add( newControlPoint( startingPoint.getCanvasPosition() ) );
\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
290 public void addedToContext(ICanvasContext ctx) {
\r
291 super.addedToContext(ctx);
\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
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
305 this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER);
\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
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
319 removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
\r
322 super.removedFromContext(ctx);
\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
331 private static RouteTerminal addTerminal(Object data, RouteGraph rg, double x, double y, Rectangle2D bounds, int allowedDirections, ILineEndStyle style) {
\r
333 if (bounds != null)
\r
334 rt = rg.addTerminal(x, y, bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY(), allowedDirections, style);
\r
336 rt = rg.addTerminal(x, y, x, y, x, y, allowedDirections, style);
\r
337 rt.setData( data );
\r
341 private RouteTerminal setEndTerminal(double x, double y, Rectangle2D bounds, int allowedDirections) {
\r
343 // First add then remove to prevent deletion of route lines in case there are 2 only terminals
\r
345 RouteTerminal toRemove = endRouteTerminal;
\r
347 endRouteTerminal = addTerminal( END_TERMINAL_DATA, routeGraph, x, y, bounds, allowedDirections, endTerminalStyle );
\r
348 routeGraph.link( attachedToRouteLine, endRouteTerminal );
\r
350 if (toRemove != null)
\r
351 routeGraph.remove(toRemove);
\r
353 return endRouteTerminal;
\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
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
371 rgNode = ghostNode.addNode("branch", RouteGraphNode.class);
\r
373 double ex = startingPoint.getCanvasPosition().getX();
\r
374 double ey = startingPoint.getCanvasPosition().getY();
\r
376 beforeRouteGraph = startingPoint.getNode().getRouteGraph();
\r
377 beforeEditable = startingPoint.getNode().isEditable();
\r
379 RouteGraphNode beforeRgNode = startingPoint.getNode();
\r
380 beforeRenderer = beforeRgNode.getRenderer();
\r
382 rgNode.setRenderer(beforeRenderer);
\r
383 rgNode.setEditable(false);
\r
385 beforeRgNode.setEditable(beforeEditable);
\r
386 beforeRgNode.setRenderer(null);
\r
392 public void initRG(double ex, double ey) {
\r
394 THashMap<Object, Object> map = new THashMap<Object, Object>();
\r
395 routeGraph = beforeRouteGraph.copy(map);
\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
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
414 routeGraphDelta = new RouteGraphDelta(beforeRouteGraph, routeGraph);
\r
416 // beforeRouteGraph.print();
\r
417 // routeGraph.print();
\r
418 // routeGraphDelta.print();
\r
420 setEndTerminal(ex, ey, null, 0xf);
\r
422 rgNode.setRouteGraph(routeGraph);
\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
433 ghostNode.remove();
\r
439 @EventHandler(priority = 200)
\r
440 public boolean handleCommandEvents(CommandEvent ce) {
\r
441 if (ce.command.equals(Commands.CANCEL)) {
\r
445 } else if (ce.command.equals(Commands.ROTATE_ELEMENT_CCW) || ce.command.equals(Commands.ROTATE_ELEMENT_CW)) {
\r
446 // TODO: rotate flag?
\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
459 if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) {
\r
461 endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(ke instanceof KeyPressedEvent));
\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
475 if (me instanceof MouseMovedEvent)
\r
476 return processMouseMove((MouseMovedEvent) me);
\r
478 if (me instanceof MouseButtonPressedEvent)
\r
479 return processMouseButtonPress((MouseButtonPressedEvent) me);
\r
484 protected boolean processMouseMove(MouseMovedEvent me) {
\r
485 mouseHasMoved = true;
\r
487 Point2D mouseControlPos = me.controlPosition;
\r
488 Point2D mouseCanvasPos = util.controlToCanvas(mouseControlPos, new Point2D.Double());
\r
490 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
\r
491 if (snapAdvisor != null)
\r
492 snapAdvisor.snap(mouseCanvasPos);
\r
494 // Record last snapped canvas position of mouse.
\r
495 this.lastMouseCanvasPos.setLocation(mouseCanvasPos);
\r
497 if (isEndingInFlag()) {
\r
498 endFlagNode.setTransform(AffineTransform.getTranslateInstance(mouseCanvasPos.getX(), mouseCanvasPos.getY()));
\r
501 TerminalInfo ti = pi.pickTerminal(me.controlPosition);
\r
503 Object canConnect = canConnect(ti.e, ti.t);
\r
504 if (canConnect != null) {
\r
505 connectionJudgment = (ConnectionJudgement) canConnect;
\r
507 if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) {
\r
508 controlPoints.getLast()
\r
509 .setPosition(ti.posDia)
\r
510 .setAttachedToTerminal(ti);
\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
522 updateSG(new Point2D.Double(ti.posDia.getTranslateX(), ti.posDia.getTranslateY()));
\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
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
537 disconnect(mouseCanvasPos);
\r
539 controlPoints.getLast()
\r
540 .setPosition(mouseCanvasPos)
\r
541 .setAttachedToTerminal(null);
\r
543 endTerminal = null;
\r
545 // CASE: Mouse was not previously on top of a valid ending
\r
546 // element terminal.
\r
548 // => Move and re-orient last branch point.
\r
550 controlPoints.getLast()
\r
551 .setPosition(mouseCanvasPos);
\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
558 updateSG(lastMouseCanvasPos);
\r
563 protected void connect(TerminalInfo ti) {
\r
564 setEndTerminal(ti);
\r
567 protected void disconnect(Point2D mouseCanvasPos) {
\r
568 setEndTerminal(mouseCanvasPos.getX(), mouseCanvasPos.getY(), null, 0xf);
\r
571 protected boolean processMouseButtonPress(MouseButtonPressedEvent e) {
\r
572 MouseButtonEvent me = e;
\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
577 if (!mouseHasMoved)
\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
584 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
\r
585 if (snapAdvisor != null)
\r
586 snapAdvisor.snap(mouseCanvasPos);
\r
588 // Clicked on an allowed end terminal. End connection & end mode.
\r
589 if (isEndTerminalDefined()) {
\r
590 createConnection();
\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
600 createConnection();
\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
611 // Eat the event to prevent other participants from doing
\r
612 // incompatible things while in this connection mode.
\r
614 } else if (me.button == MouseEvent.RIGHT_BUTTON) {
\r
615 return cancelPreviousBend();
\r
621 protected boolean cancelPreviousBend() {
\r
622 if (!routePointsAllowed())
\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
630 // If there are no real route points, cancel whole connection.
\r
631 if (controlPoints.size() <= 2) {
\r
637 // Cancel last bend
\r
638 controlPoints.removeLast();
\r
639 controlPoints.getLast().setPosition(lastMouseCanvasPos);
\r
641 updateSG(lastMouseCanvasPos);
\r
645 protected void updateSG(Point2D mousePos) {
\r
646 routeGraph.setLocation(endRouteTerminal, mousePos.getX(), mousePos.getY());
\r
647 //routeGraph.print(System.err);
\r
651 protected boolean shouldEndWithFlag(MouseEvent me) {
\r
652 return shouldEndWithFlag((me.stateMask & MouseEvent.ALT_MASK) != 0);
\r
655 protected boolean shouldEndWithFlag(boolean altPressed) {
\r
656 return altPressed && !isEndTerminalDefined() && createFlags;
\r
659 protected boolean isEndTerminalDefined() {
\r
660 return endTerminal != null;
\r
663 protected boolean isFlagTerminal(TerminalInfo ti) {
\r
664 return ti.e.getElementClass().containsClass(FlagHandler.class);
\r
668 protected boolean isEndingInFlag() {
\r
669 return endFlag != null;
\r
672 protected void endWithoutTerminal(Point2D mousePos, boolean altDown) {
\r
673 // Just go with branch points if flags are not allowed.
\r
677 boolean endTerminalDefined = isEndTerminalDefined();
\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
686 // TerminalPainter must refresh
\r
687 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
\r
689 updateSG(mousePos);
\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
696 endFlagNode.remove();
\r
697 endFlagNode = null;
\r
699 ControlPoint cp = controlPoints.getLast();
\r
700 cp.setAttachedToTerminal(endTerminal);
\r
702 if (endTerminalDefined) {
\r
703 cp.setPosition(endTerminal.posDia);
\r
705 cp.setPosition(mousePos);
\r
708 // Force Terminalpainter refresh
\r
709 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
\r
711 updateSG(mousePos);
\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
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
733 protected ControlPoint newControlPoint(Point2D canvasPos) {
\r
734 return new ControlPoint(canvasPos);
\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
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
754 final ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
\r
755 final Triple<RouteGraph,RouteGraph,RouteGraphDelta> rgs = prepareRouteGraphDelta();
\r
757 Simantics.getSession().asyncRequest(new WriteRequest() {
\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
765 Resource attachToLine = RouteGraphConnection.deserialize(graph, attachedToRouteLine.getData());
\r
766 builder.attachToRouteGraph(graph, judgment, connection, attachToLine, controlPoints, endTerminal, FlagClass.Type.Out);
\r
768 }, new Callback<DatabaseException>() {
\r
770 public void run(DatabaseException parameter) {
\r
771 if (parameter != null)
\r
772 ExceptionUtils.logAndShowError(parameter);
\r
777 protected boolean routePointsAllowed() {
\r
778 return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS));
\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
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
791 return advisor.canBeConnected(null, startTerminal.e, startTerminal.t, endElement, endTerminal);
\r
794 protected static FlagClass.Type endToFlagType(EdgeEnd end) {
\r
797 return FlagClass.Type.In;
\r
799 return FlagClass.Type.Out;
\r
801 throw new IllegalArgumentException("unrecognized edge end: " + end);
\r
805 protected TerminalInfo createFlag(EdgeEnd connectionEnd) {
\r
806 ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG);
\r
807 IElement e = Element.spawnNew(flagClass);
\r
809 e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd));
\r
810 e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal);
\r
812 TerminalInfo ti = new TerminalInfo();
\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
821 // ------------------------------------------------------------------------
\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
833 if (elements.isEmpty())
\r
836 Rectangle2D pickRect = pickShape.getBounds2D();
\r
837 final double x = pickRect.getCenterX();
\r
838 final double y = pickRect.getCenterY();
\r
840 return pickNearestRouteGraphConnection(elements, x, y, pickDistance);
\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
849 double delta = (hi - lo);
\r
850 if (delta <= limit)
\r
853 pd = (lo + hi) * .5;
\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
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
880 Point2D intersection = intersectionPoint(x, y, line);
\r
881 if (intersection == null)
\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
889 nearestTarget = new RouteGraphTarget(connection, rgn, line, new Point2D.Double(x, y), intersection);
\r
893 return nearestTarget;
\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
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