/******************************************************************************* * Copyright (c) 2007, 2016 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 * Semantum Oy - Fixed bug #6364 *******************************************************************************/ package org.simantics.diagram.participant; import java.awt.AlphaComposite; import java.awt.Composite; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.simantics.Simantics; import org.simantics.db.Resource; import org.simantics.db.WriteGraph; import org.simantics.db.common.request.WriteRequest; 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.RoutePoint; import org.simantics.diagram.connection.RouteTerminal; import org.simantics.diagram.connection.delta.RouteGraphDelta; import org.simantics.diagram.connection.rendering.IRouteGraphRenderer; import org.simantics.diagram.connection.rendering.arrows.ArrowLineEndStyle; import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle; import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle; 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.SGNodeReflection.SGCleanup; import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit; import org.simantics.g2d.connection.ConnectionEntity; 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.PickRequest; import org.simantics.g2d.diagram.handler.Topology.Connection; 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.ElementHints; 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.FlagClass; import org.simantics.g2d.elementclass.FlagHandler; import org.simantics.g2d.participant.TransformUtil; import org.simantics.g2d.utils.geom.DirectionSet; 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.ConnectionNode; import org.simantics.scenegraph.g2d.nodes.connection.RouteGraphNode; import org.simantics.scenegraph.g2d.snap.ISnapAdvisor; import org.simantics.scenegraph.utils.GeometryUtils; import org.simantics.structural2.modelingRules.ConnectionJudgement; import org.simantics.utils.datastructures.Callback; import org.simantics.utils.datastructures.Pair; import org.simantics.utils.datastructures.Triple; 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 RouteGraphConnectTool extends AbstractMode { private static final String END_TERMINAL_DATA = "END"; public static final int PAINT_PRIORITY = ElementPainter.ELEMENT_PAINT_PRIORITY + 5; @Dependency protected TransformUtil util; @Dependency protected ElementPainter diagramPainter; @Dependency protected PointerInteractor pi; /** * Starting point designation. * * The value is received by the constructor. */ protected RouteGraphTarget startingPoint; protected TerminalInfo startTerminal = new TerminalInfo(); /** * true if this tool should create connection continuation * flags, false otherwise. */ protected boolean createFlags; /** * */ protected IElementClassProvider elementClassProvider; /** * */ protected Deque controlPoints = new ArrayDeque(); /** * 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 #startTerminal} and {@link #endTerminal}. */ protected ConnectionJudgement connectionJudgment; /** * 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) { 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 ConnectionNode ghostNode; protected RouteGraphNode rgNode; protected RouteGraph routeGraph; protected RouteTerminal endRouteTerminal; private ILineEndStyle endTerminalStyle; /** * Indicates whether the connection is about to be ended into a new * flag/branchpoint or not. */ protected TerminalInfo endFlag; protected G2DParentNode endFlagNode; private RouteLine attachedToRouteLine; protected RouteGraph beforeRouteGraph; private IRouteGraphRenderer beforeRenderer; private boolean beforeEditable; protected RouteGraphDelta routeGraphDelta; private Set> alreadyConnected; /** * @param startElement * @param routeGraphConnection * @param mouseId * @param startCanvasPos */ public RouteGraphConnectTool(RouteGraphTarget startingPoint, int mouseId) { super(mouseId); Point2D intersection = startingPoint.getIntersectionPosition(); this.startingPoint = startingPoint; this.lastMouseCanvasPos.setLocation(intersection); BranchPointTerminal t = BranchPointTerminal.existingTerminal( AffineTransform.getTranslateInstance(intersection.getX(), intersection.getY()), DirectionSet.ANY, BranchPointNode.SHAPE); Point2D p = startingPoint.getIntersectionPosition(); AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY()); startTerminal.e = startingPoint.getElement(); startTerminal.t = t; startTerminal.posElem = at; startTerminal.posDia = at; startTerminal.shape = t.getShape(); controlPoints.add( newControlPoint( startingPoint.getIntersectionPosition() ) ); controlPoints.add( newControlPoint( startingPoint.getCanvasPosition() ) ); alreadyConnected = new HashSet>(); IElement connection = startingPoint.getElement(); ConnectionEntity ce = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); Collection tcs = ce.getTerminalConnections(null); for (Connection tc : tcs) alreadyConnected.add(Pair.make(tc.node, tc.terminal)); } @Override public void addedToContext(ICanvasContext ctx) { super.addedToContext(ctx); // 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)); } } @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); } super.removedFromContext(ctx); } int straightDirections(RouteLine line) { return line.isHorizontal() ? (RouteTerminal.DIR_DOWN | RouteTerminal.DIR_UP) : (RouteTerminal.DIR_LEFT | RouteTerminal.DIR_RIGHT); } private static RouteTerminal addTerminal(Object data, RouteGraph rg, double x, double y, Rectangle2D bounds, int allowedDirections, ILineEndStyle style) { RouteTerminal rt; if (bounds != null) rt = rg.addTerminal(x, y, bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY(), allowedDirections, style); else rt = rg.addTerminal(x, y, x, y, x, y, allowedDirections, style); rt.setData( data ); return rt; } private RouteTerminal setEndTerminal(double x, double y, Rectangle2D bounds, int allowedDirections) { // First add then remove to prevent deletion of route lines in case there are 2 only terminals RouteTerminal toRemove = endRouteTerminal; endRouteTerminal = addTerminal( END_TERMINAL_DATA, routeGraph, x, y, bounds, allowedDirections, endTerminalStyle ); routeGraph.link( attachedToRouteLine, endRouteTerminal ); if (toRemove != null) routeGraph.remove(toRemove); return endRouteTerminal; } protected void setEndTerminal(TerminalInfo ti) { Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double()); GeometryUtils.expandRectangle(bounds, 2); int dir = RouteGraphConnectionClass.shortestDirectionOutOfBounds( ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds); setEndTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds, dir); } @SGInit public void initSG(G2DParentNode parent) { ghostNode = parent.addNode("branched connection", ConnectionNode.class); //ghostNode.setComposite(AlphaComposite.SrcOver.derive(0.5f)); ghostNode.setZIndex(PAINT_PRIORITY); rgNode = ghostNode.addNode("branch", RouteGraphNode.class); double ex = startingPoint.getCanvasPosition().getX(); double ey = startingPoint.getCanvasPosition().getY(); beforeRouteGraph = startingPoint.getNode().getRouteGraph(); beforeEditable = startingPoint.getNode().isEditable(); RouteGraphNode beforeRgNode = startingPoint.getNode(); beforeRenderer = beforeRgNode.getRenderer(); rgNode.setRenderer(beforeRenderer); rgNode.setEditable(false); beforeRgNode.setEditable(beforeEditable); beforeRgNode.setRenderer(null); initRG(ex, ey); } public void initRG(double ex, double ey) { THashMap map = new THashMap(); routeGraph = beforeRouteGraph.copy(map); endTerminalStyle = PlainLineEndStyle.INSTANCE; for (RouteTerminal t : routeGraph.getTerminals()) { if (t.getRenderStyle() instanceof ArrowLineEndStyle) { endTerminalStyle = t.getRenderStyle(); break; } } attachedToRouteLine = (RouteLine) map.get(startingPoint.getLine()); routeGraph.makePersistent(attachedToRouteLine); for (RouteLine line : routeGraph.getAllLines()) { if (!line.isTransient() && line.isHorizontal() == attachedToRouteLine.isHorizontal() && line.getPosition() == attachedToRouteLine.getPosition()) { attachedToRouteLine = line; break; } } routeGraphDelta = new RouteGraphDelta(beforeRouteGraph, routeGraph); // beforeRouteGraph.print(); // routeGraph.print(); // routeGraphDelta.print(); setEndTerminal(ex, ey, null, 0xf); rgNode.setRouteGraph(routeGraph); } @SGCleanup public void cleanupSG() { RouteGraphNode beforeRgNode = startingPoint.getNode(); beforeRgNode.setRouteGraph(beforeRouteGraph); beforeRgNode.setRenderer(beforeRenderer); beforeRgNode.setEditable(beforeEditable); ghostNode.remove(); ghostNode = null; setDirty(); } @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)) { // TODO: rotate flag? } 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())); } TerminalInfo ti = pi.pickTerminal(me.controlPosition); if (ti != null) { Object canConnect = canConnect(ti.e, ti.t); if (canConnect != null) { connectionJudgment = (ConnectionJudgement) canConnect; if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) { controlPoints.getLast() .setPosition(ti.posDia) .setAttachedToTerminal(ti); endTerminal = ti; connect(ti); } // Make sure that we are ending with a flag if ALT is pressed // and no end terminal is defined. endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)); updateSG(new Point2D.Double(ti.posDia.getTranslateX(), ti.posDia.getTranslateY())); return false; } } 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. disconnect(mouseCanvasPos); controlPoints.getLast() .setPosition(mouseCanvasPos) .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); } // Make sure that we are ending with a flag if ALT is pressed and no end // terminal is defined. endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)); updateSG(lastMouseCanvasPos); return false; } protected void connect(TerminalInfo ti) { setEndTerminal(ti); } protected void disconnect(Point2D mouseCanvasPos) { setEndTerminal(mouseCanvasPos.getX(), mouseCanvasPos.getY(), null, 0xf); } 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); // Clicked on an allowed end terminal. End connection & end mode. if (isEndTerminalDefined()) { createConnection(); remove(); return true; } else { // Finish connection in thin air only if the // connection was started from a valid terminal. if ((me.stateMask & MouseEvent.ALT_MASK) != 0 && startTerminal != null) { connectionJudgment = (ConnectionJudgement) canConnect(null, null); if (connectionJudgment == null) return true; createConnection(); remove(); return true; } else if (routePointsAllowed() && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) { // Add new connection control point. controlPoints.add(newControlPoint(mouseCanvasPos)); updateSG(mouseCanvasPos); } } // 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; } 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); updateSG(lastMouseCanvasPos); return true; } protected void updateSG(Point2D mousePos) { routeGraph.setLocation(endRouteTerminal, mousePos.getX(), mousePos.getY()); //routeGraph.print(System.err); setDirty(); } protected boolean shouldEndWithFlag(MouseEvent me) { return shouldEndWithFlag((me.stateMask & MouseEvent.ALT_MASK) != 0); } protected boolean shouldEndWithFlag(boolean altPressed) { return altPressed && !isEndTerminalDefined() && createFlags; } protected boolean isEndTerminalDefined() { return endTerminal != null; } protected boolean isFlagTerminal(TerminalInfo ti) { return ti.e.getElementClass().containsClass(FlagHandler.class); } protected boolean isEndingInFlag() { return endFlag != null; } protected void endWithoutTerminal(Point2D mousePos, boolean altDown) { // Just go with branch points if flags are not allowed. if (!createFlags) return; boolean endTerminalDefined = isEndTerminalDefined(); if (altDown) { if (!isEndingInFlag()) { endFlag = createFlag(EdgeEnd.End); endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos); controlPoints.getLast() .setAttachedToTerminal(endFlag); // TerminalPainter must refresh setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); updateSG(mousePos); } } 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.setAttachedToTerminal(endTerminal); if (endTerminalDefined) { cp.setPosition(endTerminal.posDia); } else { cp.setPosition(mousePos); } // Force Terminalpainter refresh setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); updateSG(mousePos); } } } 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; } /** * @param canvasPos * @return */ protected ControlPoint newControlPoint(Point2D canvasPos) { return new ControlPoint(canvasPos); } protected Triple prepareRouteGraphDelta() { // Prevent route graph connection synchronization from crashing on the // transient route terminal that doesn't exist yet. It is created after // persisting the route line, which is the only purpose of this route // graph synchronization. routeGraph.remove(endRouteTerminal); return Triple.make(beforeRouteGraph, routeGraph, routeGraphDelta); } protected void createConnection() { TimeLogger.resetTimeAndLog(getClass(), "createConnection"); final ConnectionJudgement judgment = this.connectionJudgment; if (judgment == null) { ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity", null); return; } final ConnectionBuilder builder = new ConnectionBuilder(this.diagram); final Triple rgs = prepareRouteGraphDelta(); Simantics.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { graph.markUndoPoint(); Resource connection = ElementUtils.getObject(startTerminal.e); if (!rgs.third.isEmpty()) { new RouteGraphConnection(graph, connection).synchronize(graph, rgs.first, rgs.second, rgs.third); } Resource attachToLine = RouteGraphConnection.deserialize(graph, attachedToRouteLine.getData()); builder.attachToRouteGraph(graph, judgment, connection, attachToLine, controlPoints, endTerminal, FlagClass.Type.Out); } }, new Callback() { @Override public void run(DatabaseException parameter) { if (parameter != null) ExceptionUtils.logAndShowError(parameter); } }); } protected boolean routePointsAllowed() { return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS)); } protected Object canConnect(IElement endElement, Terminal endTerminal) { IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR); return canConnect(advisor, endElement, endTerminal); } protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) { if (advisor == null) return ConnectionJudgement.CANBEMADELEGAL; if (alreadyConnected.contains(Pair.make(endElement, endTerminal))) return null; return advisor.canBeConnected(null, startTerminal.e, startTerminal.t, endElement, endTerminal); } 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; } // ------------------------------------------------------------------------ static RouteGraphTarget pickRouteGraphConnection(IDiagram diagram, Shape pickShape, double pickDistance) { ArrayList elements = new ArrayList(); PickRequest req = new PickRequest(pickShape); DiagramUtils.pick(diagram, req, elements); for (Iterator it = elements.iterator(); it.hasNext();) { IElement e = it.next(); RouteGraphNode rgn = e.getHint(RouteGraphConnectionClass.KEY_RG_NODE); if (rgn == null || rgn.getRouteGraph() == null) it.remove(); } if (elements.isEmpty()) return null; Rectangle2D pickRect = pickShape.getBounds2D(); final double x = pickRect.getCenterX(); final double y = pickRect.getCenterY(); return pickNearestRouteGraphConnection(elements, x, y, pickDistance); } private static RouteGraphTarget pickNearestRouteGraphConnection(ArrayList elements, double x, double y, double pd) { // Find the nearest distance at which we get hits. double hi = pd + 1; double lo = hi * .01; double limit = 0.5; while (true) { double delta = (hi - lo); if (delta <= limit) break; pd = (lo + hi) * .5; boolean hit = false; for (IElement connection : elements) { RouteGraphNode rgn = connection.getHint(RouteGraphConnectionClass.KEY_RG_NODE); RouteLine line = rgn.getRouteGraph().pickLine(x, y, pd); if (line != null) { hit = true; break; } } if (hit) hi = pd; else lo = pd; } // Now that the nearest hitting distance is found, find the nearest intersection. RouteGraphTarget nearestTarget = null; double nearest = Double.MAX_VALUE; for (IElement connection : elements) { RouteGraphNode rgn = connection.getHint(RouteGraphConnectionClass.KEY_RG_NODE); RouteLine line = rgn.getRouteGraph().pickLine(x, y, pd); if (line == null) continue; Point2D intersection = intersectionPoint(x, y, line); if (intersection == null) continue; double dx = intersection.getX() - x; double dy = intersection.getY() - y; double dist = Math.sqrt(dx*dx + dy*dy); if (dist < nearest) { nearest = dist; nearestTarget = new RouteGraphTarget(connection, rgn, line, new Point2D.Double(x, y), intersection); } } return nearestTarget; } static Point2D intersectionPoint(double x, double y, RouteLine line) { Collection points = line.getPoints(); if (points.size() < 2) return null; RoutePoint s = line.getBegin(); RoutePoint e = line.getEnd(); return GeometryUtils.intersectionToLine(s.getX(), s.getY(), e.getX(), e.getY(), x, y); } }