/*******************************************************************************
* 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.List;
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.MouseButtonReleasedEvent;
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.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