/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.diagram.participant; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.Iterator; import java.util.List; import org.simantics.Simantics; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.WriteGraph; import org.simantics.db.common.request.UniqueRead; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.common.utils.NameUtils; import org.simantics.db.exception.DatabaseException; import org.simantics.diagram.connection.RouteGraph; import org.simantics.diagram.connection.RouteGraphConnectionClass; import org.simantics.diagram.connection.RouteLine; import org.simantics.diagram.connection.RouteTerminal; import org.simantics.diagram.connection.delta.RouteGraphDelta; import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle; import org.simantics.diagram.content.ResourceTerminal; import org.simantics.diagram.stubs.DiagramResource; import org.simantics.diagram.synchronization.ISynchronizationContext; import org.simantics.diagram.synchronization.SynchronizationHints; import org.simantics.diagram.synchronization.graph.RouteGraphConnection; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency; import org.simantics.g2d.canvas.impl.DependencyReflection.Reference; import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup; import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit; import org.simantics.g2d.connection.IConnectionAdvisor; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.DiagramUtils; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.handler.PickContext; import org.simantics.g2d.diagram.handler.Topology.Terminal; import org.simantics.g2d.diagram.participant.ElementPainter; import org.simantics.g2d.diagram.participant.TerminalPainter; import org.simantics.g2d.diagram.participant.TerminalPainter.TerminalHoverStrategy; import org.simantics.g2d.diagram.participant.pointertool.AbstractMode; import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor; import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil; import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalInfo; import org.simantics.g2d.element.ElementClass; import org.simantics.g2d.element.ElementClasses; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.IElementClassProvider; import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd; import org.simantics.g2d.element.handler.SceneGraph; import org.simantics.g2d.element.handler.TerminalTopology; import org.simantics.g2d.element.handler.impl.BranchPointTerminal; import org.simantics.g2d.element.impl.Element; import org.simantics.g2d.elementclass.BranchPoint; import org.simantics.g2d.elementclass.BranchPoint.Direction; import org.simantics.g2d.elementclass.FlagClass; import org.simantics.g2d.elementclass.FlagHandler; import org.simantics.g2d.participant.RenderingQualityInteractor; import org.simantics.g2d.participant.TransformUtil; import org.simantics.g2d.utils.geom.DirectionSet; import org.simantics.modeling.ModelingResources; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler; import org.simantics.scenegraph.g2d.events.KeyEvent; import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent; import org.simantics.scenegraph.g2d.events.command.CommandEvent; import org.simantics.scenegraph.g2d.events.command.Commands; import org.simantics.scenegraph.g2d.nodes.BranchPointNode; import org.simantics.scenegraph.g2d.nodes.ShapeNode; import org.simantics.scenegraph.g2d.snap.ISnapAdvisor; import org.simantics.scenegraph.utils.GeometryUtils; import org.simantics.scenegraph.utils.Quality; import org.simantics.structural2.modelingRules.ConnectionJudgement; import org.simantics.utils.datastructures.Callback; import org.simantics.utils.datastructures.Pair; import org.simantics.utils.logging.TimeLogger; import org.simantics.utils.ui.ErrorLogger; import org.simantics.utils.ui.ExceptionUtils; import gnu.trove.map.hash.THashMap; /** * A basic tool for making connection on diagrams. * * This version defines the starting, ending and route points of a connection. * The routing itself is left up to the diagram router employed by * {@link DiagramUtils#validateAndFix(IDiagram, ICanvasContext)}. * * Manual: * * This tool is added to the diagram when a connection sequence is initiated by * another participant. PointerInteractor is one such participant which adds the * tool when a terminal or non-terminal-occupied canvas space is ALT+clicked * (see {@link PointerInteractor#checkInitiateConnectTool(MouseEvent, Point2D)} * ). The connection will be finished when another allowed terminal is clicked * upon or empty canvas space is ALT+clicked. Route points for the connection * can be created by clicking around on non-terminal-occupied canvas space while * connecting. * *

* Connections can be started from and ended in flags by pressing ALT while * left-clicking. * * @author Tuukka Lehtonen */ public class ConnectTool2 extends AbstractMode { public static final int PAINT_PRIORITY = ElementPainter.ELEMENT_PAINT_PRIORITY + 5; @Reference protected RenderingQualityInteractor quality; @Dependency protected TransformUtil util; @Dependency protected ElementPainter diagramPainter; @Dependency protected PointerInteractor pi; @Dependency protected PickContext pickContext; /** * Start element terminal of the connection. null if connection * was started from a flag or a branch point. * * The value is received by the constructor. */ protected List startTerminals; /** * Refers to any of the possible overlapping start terminals. The value is * taken from the first index of {@link #startTerminals} assuming that the * first one is the nearest. It is null if * {@link #startTerminals} is empty. */ protected TerminalInfo startTerminal; protected TerminalInfo startFlag; /** * Starting position of the connection, received as an external argument. */ protected final Point2D startPos; /** * true if this tool should create connection continuation * flags, false otherwise. */ protected boolean createFlags; /** * */ protected IElementClassProvider elementClassProvider; /** * */ protected Deque controlPoints = new ArrayDeque(); /** * Contains null when a connection is started from a new flag * or one of the terminals in {@link #startTerminals} when a connection is * being created starting from a terminal or possibly a set of terminals. * *

* Note that this is different from {@link #startTerminal} which simply * represents the first element of {@link #startTerminals}. * *

* Only when this value and {@link #endTerminal} is properly set will a * connection be created between two element terminals. */ protected TerminalInfo selectedStartTerminal; /** * Element terminal of connection end element. null if * connection cannot be ended where it is currently being attempted to end. */ protected TerminalInfo endTerminal; /** * The latest connectability judgment from the active * {@link IConnectionAdvisor} should the connection happen between * {@link #selectedStartTerminal} and {@link #endTerminal}. */ protected ConnectionJudgement connectionJudgment; /** * If non-null during connection drawing this field tells the direction * forced for the current branch point by the user through the UI commands * {@link Commands#ROTATE_ELEMENT_CCW} and * {@link Commands#ROTATE_ELEMENT_CW}. */ private Direction forcedBranchPointDirection; /** * A temporary variable for use with * {@link TerminalTopology#getTerminals(IElement, Collection)}. */ protected Collection terminals = new ArrayList(); /** * Previous mouse canvas position recorded by * {@link #processMouseMove(MouseMovedEvent)}. */ protected Point2D lastMouseCanvasPos = new Point2D.Double(); /** * Set to true once {@link #processMouseMove(MouseMovedEvent)} has been * invoked at least once. This is used to tell whether to allow creation of * branch points or finising the connection in thin air. It will not be * allowed if the mouse has not moved at all since starting the connection. */ protected boolean mouseHasMoved = false; protected TerminalHoverStrategy originalStrategy = null; protected TerminalHoverStrategy terminalHoverStrategy = new TerminalHoverStrategy() { @Override public boolean highlightEnabled() { return !isEndingInFlag(); } @Override public boolean highlight(TerminalInfo ti) { boolean reflexive = isStartTerminal(ti.e, ti.t); if (reflexive && !allowReflexiveConnections()) return false; return canConnect(ti.e, ti.t) != null; } }; protected final static Composite ALPHA_COMPOSITE = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f); /** * Root scene graph node for all visualization performed by this tool. */ protected G2DParentNode ghostNode; /** * Indicates whether the connection is about to be ended into a new * flag/branchpoint or not. */ protected TerminalInfo endFlag; protected G2DParentNode endFlagNode; private RouteGraphTarget lastRouteGraphTarget; /** * @param startTerminal * @param mouseId * @param startCanvasPos */ public ConnectTool2(TerminalInfo startTerminal, int mouseId, Point2D startCanvasPos) { this(startTerminal == null ? Collections. emptyList() : Collections.singletonList(startTerminal), mouseId, startCanvasPos); } /** * @param startTerminals * @param mouseId * @param startCanvasPos */ public ConnectTool2(List startTerminals, int mouseId, Point2D startCanvasPos) { super(mouseId); if (startCanvasPos == null) throw new NullPointerException("null start position"); if (startTerminals == null) throw new NullPointerException("null start terminals"); this.startPos = startCanvasPos; this.lastMouseCanvasPos.setLocation(startPos); this.startTerminals = startTerminals; this.startTerminal = startTerminals.isEmpty() ? null : startTerminals.get(0); } @Override public void addedToContext(ICanvasContext ctx) { super.addedToContext(ctx); if (quality != null) quality.setStaticQuality(Quality.LOW); // Force terminals to always be highlighted without pressing certain // keys or key combinations. originalStrategy = getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY); setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); } @Override protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) { if (newDiagram != null) { // Get IElementClassProvider ISynchronizationContext ctx = newDiagram.getHint(SynchronizationHints.CONTEXT); if (ctx != null) { this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER); } // See if flags should be created or not. this.createFlags = Boolean.TRUE.equals(newDiagram.getHint(DiagramHints.KEY_USE_CONNECTION_FLAGS)); startConnection(); } } @Override public void removedFromContext(ICanvasContext ctx) { if (getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY) == terminalHoverStrategy) { if (originalStrategy != null) setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, originalStrategy); else removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY); } if (quality != null) quality.setStaticQuality(null); super.removedFromContext(ctx); } protected void startConnection() { Point2D startPos = (Point2D) this.startPos.clone(); ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR); if (snapAdvisor != null) snapAdvisor.snap(startPos); // Resolve the first element and terminal of the connection. ControlPoint start = new ControlPoint(startPos); if (startTerminal != null) { assert ElementUtils.peekDiagram(startTerminal.e) == diagram; Point2D terminalPos = new Point2D.Double(startTerminal.posDia.getTranslateX(), startTerminal.posDia.getTranslateY()); start.setPosition(terminalPos).setAttachedToTerminal(startTerminal); } else { // Create TerminalInfo describing the flag to be created. if (createFlags) { // This prevents connection creation from creating a branch // point in place of this flag. startFlag = createFlag(EdgeEnd.Begin); start.setAttachedToTerminal(startFlag); showElement(ghostNode, "startFlag", startFlag.e, startPos); } } controlPoints.add(start); controlPoints.add(new ControlPoint(startPos)); // Make sure that we are ending with a flag if ALT is pressed. // This makes the tool always start with a flag which can be quite // cumbersome and is therefore disabled. The current version will not // end the connection if the mouse has not moved at all. //if (keyUtil.isKeyPressed(java.awt.event.KeyEvent.VK_ALT)) { // endWithoutTerminal(lastMouseCanvasPos, true); //} } @SGInit public void initSG(G2DParentNode parent) { ghostNode = parent.addNode(G2DParentNode.class); ghostNode.setZIndex(PAINT_PRIORITY); ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class); pathNode.setColor(new Color(160, 0, 0)); pathNode.setStroke(new BasicStroke(0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, new float[] { 0.5f, 0.2f }, 0)); pathNode.setScaleStroke(false); pathNode.setZIndex(0); G2DParentNode points = ghostNode.getOrCreateNode("points", G2DParentNode.class); points.setZIndex(1); updateSG(); } static class Segment { public final ControlPoint begin; public final ControlPoint end; public Path2D path; public Segment(ControlPoint begin, ControlPoint end) { this.begin = begin; this.end = end; } @Override public String toString() { return "Segment[begin=" + begin + ", end=" + end + ", path=" + path + "]"; } } private RouteTerminal addControlPoint(RouteGraph routeGraph, ControlPoint cp) { TerminalInfo ti = cp.getAttachedTerminal(); if(ti != null && ti != startFlag && ti != endFlag) { Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double()); GeometryUtils.expandRectangle(bounds, 2); int allowedDirections = RouteGraphConnectionClass.shortestDirectionOutOfBounds( ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds); return routeGraph.addTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds, allowedDirections, PlainLineEndStyle.INSTANCE); } else { double x = cp.getPosition().getX(); double y = cp.getPosition().getY(); int allowedDirections = 0xf; switch(cp.getDirection()) { case Horizontal: allowedDirections = 5; break; case Vertical: allowedDirections = 10; break; case Any: allowedDirections = 15; break; } return routeGraph.addTerminal(x, y, x, y, x, y, allowedDirections); } } protected void updateSG() { if (controlPoints.size() != 2) return; ControlPoint begin = controlPoints.getFirst(); ControlPoint end = controlPoints.getLast(); RouteGraph routeGraph = new RouteGraph(); RouteTerminal a = addControlPoint(routeGraph, begin); RouteTerminal b = addControlPoint(routeGraph, end); routeGraph.link(a, b); Path2D path = routeGraph.getPath2D(); // Create scene graph to visualize the connection. ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class); pathNode.setShape(path); setDirty(); } private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, Point2D pos) { return showElement(parent, nodeId, element, AffineTransform.getTranslateInstance(pos.getX(), pos.getY())); } private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, AffineTransform tr) { G2DParentNode elementParent = parent.getOrCreateNode(nodeId, G2DParentNode.class); elementParent.setTransform(tr); elementParent.removeNodes(); for (SceneGraph sg : element.getElementClass().getItemsByClass(SceneGraph.class)) sg.init(element, elementParent); return elementParent; } private List toSegments(Deque points) { if (points.isEmpty()) return Collections.emptyList(); List segments = new ArrayList(); Iterator it = points.iterator(); ControlPoint prev = it.next(); while (it.hasNext()) { ControlPoint next = it.next(); segments.add(new Segment(prev, next)); prev = next; } return segments; } @SGCleanup public void cleanupSG() { ghostNode.remove(); ghostNode = null; } @EventHandler(priority = 200) public boolean handleCommandEvents(CommandEvent ce) { if (ce.command.equals(Commands.CANCEL)) { setDirty(); remove(); return true; } else if (ce.command.equals(Commands.ROTATE_ELEMENT_CCW) || ce.command.equals(Commands.ROTATE_ELEMENT_CW)) { return rotateLastBranchPoint(ce.command.equals(Commands.ROTATE_ELEMENT_CW)); } return false; } @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20) public boolean handleKeyEvents(KeyEvent ke) { if (ke instanceof KeyPressedEvent) { // Back-space, cancel prev bend if (ke.keyCode == java.awt.event.KeyEvent.VK_BACK_SPACE) return cancelPreviousBend(); } if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) { if (createFlags) { endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(ke instanceof KeyPressedEvent)); return true; } } return false; } @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20) public boolean handleEvent(MouseEvent me) { // Only handle events for the connection-initiating mouse if (me.mouseId != mouseId) return false; if (me instanceof MouseMovedEvent) return processMouseMove((MouseMovedEvent) me); if (me instanceof MouseButtonPressedEvent) return processMouseButtonPress((MouseButtonPressedEvent) me); return false; } protected boolean processMouseMove(MouseMovedEvent me) { mouseHasMoved = true; Point2D mouseControlPos = me.controlPosition; Point2D mouseCanvasPos = util.controlToCanvas(mouseControlPos, new Point2D.Double()); ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR); if (snapAdvisor != null) snapAdvisor.snap(mouseCanvasPos); // Record last snapped canvas position of mouse. this.lastMouseCanvasPos.setLocation(mouseCanvasPos); if (isEndingInFlag()) { endFlagNode.setTransform(AffineTransform.getTranslateInstance(mouseCanvasPos.getX(), mouseCanvasPos.getY())); } List tis = pi.pickTerminals(me.controlPosition); tis = TerminalUtil.findNearestOverlappingTerminals(tis); if (!tis.isEmpty() && !containsStartTerminal(tis)) { //System.out.println("end terminals (" + tis.size() + "):\n" + EString.implode(tis)); for (TerminalInfo ti : tis) { Pair canConnect = canConnect(ti.e, ti.t); if (canConnect != null) { connectionJudgment = canConnect.first; if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) { if (canConnect.second != null) { controlPoints.getFirst() .setPosition(canConnect.second.posDia) .setAttachedToTerminal(canConnect.second); } controlPoints.getLast() .setPosition(ti.posDia) .setAttachedToTerminal(ti); selectedStartTerminal = canConnect.second; endTerminal = ti; } // Make sure that we are ending with a flag if ALT is pressed // and no end terminal is defined. if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me))) updateSG(); return false; } } } else { RouteGraphTarget cp = RouteGraphConnectTool.pickRouteGraphConnection( diagram, pi.getCanvasPickShape(me.controlPosition), pi.getPickDistance()); if (cp != null) { // Remove branch point highlight from previously picked route graph. if (lastRouteGraphTarget != null && cp.getNode() != lastRouteGraphTarget.getNode()) cp.getNode().showBranchPoint(null); lastRouteGraphTarget = cp; // Validate connection before visualizing connectability Point2D isectPos = cp.getIntersectionPosition(); TerminalInfo ti = TerminalInfo.create( isectPos, cp.getElement(), BranchPointTerminal.existingTerminal( isectPos, DirectionSet.ANY, BranchPointNode.SHAPE), BranchPointNode.SHAPE); Pair canConnect = canConnect(ti.e, ti.t); if (canConnect != null) { connectionJudgment = canConnect.first; controlPoints.getLast().setPosition(ti.posDia).setAttachedToTerminal(ti); endTerminal = ti; cp.getNode().showBranchPoint(isectPos); if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me))) updateSG(); return false; } } else { if (lastRouteGraphTarget != null) { lastRouteGraphTarget.getNode().showBranchPoint(null); lastRouteGraphTarget = null; } } } connectionJudgment = null; if (isEndTerminalDefined()) { // CASE: Mouse was previously on top of a valid terminal to end // the connection. Now the mouse has been moved where there is // no longer a terminal to connect to. // // => Disconnect the last edge segment from the previous // terminal, mark endElement/endTerminal non-existent // and connect the disconnected edge to a new branch point. controlPoints.getLast() .setPosition(mouseCanvasPos) .setDirection(calculateCurrentBranchPointDirection()) .setAttachedToTerminal(null); endTerminal = null; } else { // CASE: Mouse was not previously on top of a valid ending // element terminal. // // => Move and re-orient last branch point. controlPoints.getLast() .setPosition(mouseCanvasPos) .setDirection(calculateCurrentBranchPointDirection()); } // Make sure that we are ending with a flag if ALT is pressed and no end // terminal is defined. if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me))) updateSG(); return false; } protected boolean processMouseButtonPress(MouseButtonPressedEvent e) { MouseButtonEvent me = e; // Do nothing before the mouse has moved at least a little. // This prevents the user from ending the connection right where // it started. if (!mouseHasMoved) return true; if (me.button == MouseEvent.LEFT_BUTTON) { Point2D mouseControlPos = me.controlPosition; Point2D mouseCanvasPos = util.getInverseTransform().transform(mouseControlPos, new Point2D.Double()); ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR); if (snapAdvisor != null) snapAdvisor.snap(mouseCanvasPos); if (lastRouteGraphTarget != null) { lastRouteGraphTarget.getNode().showBranchPoint(null); attachToConnection(); remove(); return true; } else if (isEndTerminalDefined()) { // Clicked on an allowed end terminal. End connection & end mode. createConnection(); remove(); return true; } else { // Finish connection in thin air only if the // connection was started from a valid terminal. if (me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) && !startTerminals.isEmpty()) { Pair pair = canConnect(null, null); if (pair != null) { connectionJudgment = (ConnectionJudgement) pair.first; selectedStartTerminal = pair.second; // endFlag = createFlag(EdgeEnd.End); // controlPoints.getLast().setAttachedToTerminal(endFlag); createConnection(); setDirty(); remove(); } else { // Inform the user why connection couldn't be created. String tmsg = terminalsToString(startTerminals); ErrorLogger.defaultLogWarning("Can't resolve connection type for new connection when starting from one of the following terminals:\n" + tmsg, null); } return true; } else if (routePointsAllowed() && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) { // Add new connection control point. controlPoints.add(newControlPointWithCalculatedDirection(mouseCanvasPos)); resetForcedBranchPointDirection(); updateSG(); } } // Eat the event to prevent other participants from doing // incompatible things while in this connection mode. return true; } else if (me.button == MouseEvent.RIGHT_BUTTON) { return cancelPreviousBend(); } return false; } private void attachToConnection() { ConnectionJudgement judgment = this.connectionJudgment; if (judgment == null) { ErrorLogger.defaultLogError("Cannot attach to connection, no judgment available on connection validity", null); return; } ConnectionBuilder builder = new ConnectionBuilder(this.diagram); RouteGraph before = lastRouteGraphTarget.getNode().getRouteGraph(); THashMap copyMap = new THashMap<>(); RouteGraph after = before.copy(copyMap); RouteLine attachTo = (RouteLine) copyMap.get(lastRouteGraphTarget.getLine()); after.makePersistent(attachTo); for (RouteLine line : after.getAllLines()) { if (!line.isTransient() && line.isHorizontal() == attachTo.isHorizontal() && line.getPosition() == attachTo.getPosition()) { attachTo = line; break; } } RouteLine attachToLine = attachTo; RouteGraphDelta delta = new RouteGraphDelta(before, after); Simantics.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { graph.markUndoPoint(); Resource connection = ElementUtils.getObject(endTerminal.e); if (!delta.isEmpty()) { new RouteGraphConnection(graph, connection).synchronize(graph, before, after, delta); } Resource line = RouteGraphConnection.deserialize(graph, attachToLine.getData()); Deque cps = new ArrayDeque<>(); for (Iterator iterator = controlPoints.descendingIterator(); iterator.hasNext();) cps.add(iterator.next()); builder.attachToRouteGraph(graph, judgment, connection, line, cps, startTerminal, FlagClass.Type.In); } }, new Callback() { @Override public void run(DatabaseException parameter) { if (parameter != null) ExceptionUtils.logAndShowError(parameter); } }); } protected boolean cancelPreviousBend() { if (!routePointsAllowed()) return false; // Just to make this code more comprehensible, prevent an editing // case that requires ugly code to work. if (isEndingInFlag()) return true; // If there are no real route points, cancel whole connection. if (controlPoints.size() <= 2) { setDirty(); remove(); return true; } // Cancel last bend controlPoints.removeLast(); controlPoints.getLast().setPosition(lastMouseCanvasPos); resetForcedBranchPointDirection(); updateSG(); return true; } /** * Rotates the last branch point in the created connection in either * clockwise or counter-clockwise direction as a response to a user * interaction. * *

* At the same time it use {@link #forcedBranchPointDirection} to mark the * current last branch point to be forcefully oriented according to the * users wishes instead of calculating a default value for the orientation * from the routed connection path. See * {@link #calculateCurrentBranchPointDirection()} for more information on * this. * *

* The logic of this method goes as follows: *

* * @param clockwise * @return true if the rotation was successful */ protected boolean rotateLastBranchPoint(boolean clockwise) { Direction oldDir = calculateCurrentBranchPointDirection(); if (forcedBranchPointDirection == null) { forcedBranchPointDirection = oldDir.toggleDetermined(); } else { forcedBranchPointDirection = clockwise ? oldDir.cycleNext() : oldDir.cyclePrevious(); } controlPoints.getLast().setDirection(forcedBranchPointDirection); updateSG(); return true; } /** * Set preferred direction for a branch/route point element. * * @param branchPoint the element to set the direction for * @param direction the direction to set * @return */ protected void setDirection(IElement branchPoint, Direction direction) { branchPoint.getElementClass().getSingleItem(BranchPoint.class).setDirectionPreference(branchPoint, direction); } protected Direction forcedBranchPointDirection() { return forcedBranchPointDirection; } protected void resetForcedBranchPointDirection() { forcedBranchPointDirection = null; } protected void forceBranchPointDirection(Direction direction) { forcedBranchPointDirection = direction; } /** * @return */ protected Direction calculateCurrentBranchPointDirection() { // If this is not the first branch point, toggle direction compared to // last. if (forcedBranchPointDirection != null) return forcedBranchPointDirection; if (controlPoints.size() > 2) { // This is not the first edge segment, toggle route point // directions. Iterator it = controlPoints.descendingIterator(); it.next(); ControlPoint secondLastCp = it.next(); Direction dir = secondLastCp.getDirection(); switch (dir) { case Horizontal: return Direction.Vertical; case Vertical: return Direction.Horizontal; case Any: } } // If this is the first branch point, calculate based on edge segment // angle. if (controlPoints.size() > 1) { Iterator it = controlPoints.descendingIterator(); ControlPoint last = it.next(); ControlPoint secondLast = it.next(); double angle = Math.atan2(Math.abs(last.getPosition().getY() - secondLast.getPosition().getY()), Math.abs(last.getPosition().getX() - secondLast.getPosition().getX())); if (angle >= 0 && angle < Math.PI / 4) { return Direction.Horizontal; } else if (angle > Math.PI / 4 && angle <= Math.PI / 2) { return Direction.Vertical; } } return Direction.Any; } protected boolean isEndingInFlag() { return endFlag != null; } /** * @param mousePos * @param altDown * @return true if updateSG was executed, false * otherwise */ protected boolean endWithoutTerminal(Point2D mousePos, boolean altDown) { // Just go with branch points if flags are not allowed. if (!createFlags) return false; boolean endTerminalDefined = isEndTerminalDefined(); if (altDown) { if (!isEndingInFlag()) { endFlag = createFlag(EdgeEnd.End); endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos); controlPoints.getLast() .setDirection(calculateCurrentBranchPointDirection()) .setAttachedToTerminal(endFlag); // TerminalPainter must refresh setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); updateSG(); return true; } } else { if (isEndingInFlag()) { // Currently ending with flag but ALT is no longer down // so that flag must be removed. endFlag = null; endFlagNode.remove(); endFlagNode = null; ControlPoint cp = controlPoints.getLast(); cp.setDirection(calculateCurrentBranchPointDirection()) .setAttachedToTerminal(endTerminal); if (endTerminalDefined) { cp.setPosition(endTerminal.posDia); } else { cp.setPosition(mousePos); } // Force TerminalPainter refresh setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); updateSG(); return true; } } return false; } protected void createConnection() { createConnection( this.selectedStartTerminal, this.endTerminal, this.connectionJudgment, this.controlPoints); } protected void createConnection( final TerminalInfo startTerminal, final TerminalInfo endTerminal, final ConnectionJudgement judgement, final Deque controlPoints) { TimeLogger.resetTimeAndLog(getClass(), "createConnection"); if (judgement == null) { // Inform the user why connection couldn't be created. String tmsg = terminalsToString(Arrays.asList(startTerminal, endTerminal)); ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity when connecting the terminals:\n" + tmsg, null); return; } final ConnectionBuilder builder = new ConnectionBuilder(this.diagram); Simantics.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { builder.create(graph, judgement, controlPoints, startTerminal, endTerminal); } }, new Callback() { @Override public void run(DatabaseException parameter) { if (parameter != null) ExceptionUtils.logAndShowError(parameter); } }); } /** * @param canvasPos * @return */ protected ControlPoint newControlPointWithCalculatedDirection(Point2D canvasPos) { return new ControlPoint(canvasPos, calculateCurrentBranchPointDirection()); } /** * @param e * @param t * @return true if the specified element terminal matches any * TerminalInfo in {@link #startTerminals} */ protected boolean isStartTerminal(IElement e, Terminal t) { if (startTerminal == null) return false; for (TerminalInfo st : startTerminals) { if (st.e == e && st.t == t) { return true; } } return false; } /** * @param e * @param t * @return true if the specified element terminal matches any * TerminalInfo in {@link #startTerminals} */ protected boolean containsStartTerminal(List tis) { if (startTerminal == null) return false; for (TerminalInfo st : startTerminals) { for (TerminalInfo et : tis) { if (st.e == et.e && st.t == et.t) { return true; } } } return false; } protected static FlagClass.Type endToFlagType(EdgeEnd end) { switch (end) { case Begin: return FlagClass.Type.In; case End: return FlagClass.Type.Out; default: throw new IllegalArgumentException("unrecognized edge end: " + end); } } protected TerminalInfo createFlag(EdgeEnd connectionEnd) { ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG); IElement e = Element.spawnNew(flagClass); e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd)); e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal); TerminalInfo ti = new TerminalInfo(); ti.e = e; ti.t = ElementUtils.getSingleTerminal(e); ti.posElem = TerminalUtil.getTerminalPosOnElement(e, ti.t); ti.posDia = TerminalUtil.getTerminalPosOnDiagram(e, ti.t); return ti; } protected boolean shouldEndWithFlag(MouseEvent me) { return shouldEndWithFlag( me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) ); } protected boolean shouldEndWithFlag(boolean altPressed) { return altPressed && !isEndTerminalDefined() && createFlags && startFlag == null; } protected boolean isEndTerminalDefined() { return endTerminal != null; } protected boolean isFlagTerminal(TerminalInfo ti) { return ti.e.getElementClass().containsClass(FlagHandler.class); } protected boolean allowReflexiveConnections() { return false; } protected boolean routePointsAllowed() { return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS)); } /** * @param endElement * @param endTerminal * @return */ @SuppressWarnings("unchecked") protected final Pair canConnect(IElement endElement, Terminal endTerminal) { IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR); Object judgement = canConnect(advisor, endElement, endTerminal); if (judgement == null) return null; if (judgement instanceof Pair) return (Pair) judgement; return Pair.make((ConnectionJudgement) judgement, startTerminal); } protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) { if (advisor == null) return Pair.make(ConnectionJudgement.CANBEMADELEGAL, startTerminal); if (startTerminals.isEmpty()) { ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, null, null, endElement, endTerminal); return obj != null ? Pair.make(obj, null) : null; } for (TerminalInfo st : startTerminals) { ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, st.e, st.t, endElement, endTerminal); if (obj != null) { return Pair.make(obj, st); } } return null; } /** * For generating debugging information of what was attempted by the user * when a connection couldn't be created. * * @param ts * @return */ private String terminalsToString(final Iterable ts) { try { return Simantics.sync(new UniqueRead() { @Override public String perform(ReadGraph graph) throws DatabaseException { DiagramResource DIA = DiagramResource.getInstance(graph); ModelingResources MOD = ModelingResources.getInstance(graph); StringBuilder sb = new StringBuilder(); boolean first = true; for (TerminalInfo ti : ts) { if (!first) sb.append("\n"); first = false; sb.append("element "); Object o = ElementUtils.getObject(ti.e); if (o instanceof Resource) { Resource er = (Resource) o; Resource cer = graph.getPossibleObject(er, MOD.ElementToComponent); Resource r = cer != null ? cer : er; sb.append(NameUtils.getSafeName(graph, r)).append(" : "); for (Resource type : graph.getPrincipalTypes(r)) { sb.append(NameUtils.getSafeName(graph, type, true)); } } else { sb.append(ti.e.toString()); } sb.append(", terminal "); if (ti.t instanceof ResourceTerminal) { Resource tr = ((ResourceTerminal) ti.t).getResource(); Resource cp = graph.getPossibleObject(tr, DIA.HasConnectionPoint); Resource r = cp != null ? cp : tr; sb.append(NameUtils.getSafeName(graph, r, true)); } else { sb.append(ti.t.toString()); } } return sb.toString(); } }); } catch (DatabaseException e) { return e.getMessage(); } } }