package org.simantics.diagram.flag;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
import javax.vecmath.Tuple2d;
import javax.vecmath.Vector2d;
import org.simantics.databoard.Bindings;
import org.simantics.db.ReadGraph;
import org.simantics.db.Resource;
import org.simantics.db.Statement;
import org.simantics.db.WriteGraph;
import org.simantics.db.common.utils.OrderedSetUtils;
import org.simantics.db.exception.DatabaseException;
import org.simantics.db.exception.ServiceException;
import org.simantics.diagram.content.ConnectionUtil;
import org.simantics.diagram.content.EdgeResource;
import org.simantics.diagram.stubs.DiagramResource;
import org.simantics.diagram.synchronization.graph.AddElement;
import org.simantics.diagram.synchronization.graph.DiagramGraphUtil;
import org.simantics.g2d.element.IElement;
import org.simantics.g2d.elementclass.FlagClass;
import org.simantics.g2d.elementclass.FlagClass.Type;
import org.simantics.layer0.Layer0;
import org.simantics.modeling.ModelingResources;
import org.simantics.structural.stubs.StructuralResource2;
/**
* A class that handles splitting a connection in two with diagram local flags.
*
* The connection splitting process consists of the following steps:
*
* - Disconnect the end points of the selected connection edge segment (SEG).
* An end point is either :DIA.BranchPoint or (terminal) DIA:Connector. This
* operation will always split a valid connection into two separate parts.
* - Calculate the contents of the two separated connection parts (branch
* points and connectors) and decide which part to leave with the existing
* connection (=P1) and which part to move into a new connection (=P2). The
* current strategy is to move the parts that
* do not contain output terminal connectors into a new connection.
* This works well with diagram to composite mappings where output terminals
* generate modules behind connections.
* - Create new connection C' with the same type and STR.HasConnectionType as
* the original connection C.
* - Copy connection routing settings from C to C'.
* - Move P2 into C'.
* - Create two new flag elements into the same diagram, set their type and
* interpolate a proper transformation for both along the existing connection
* line.
* - Connect the new flags to begin(SEG) and end(SEG) connectors.
* - Join the flags together.
*
*
* @author Tuukka Lehtonen
*/
public class Splitter {
Layer0 L0;
DiagramResource DIA;
StructuralResource2 STR;
ModelingResources MOD;
public Splitter(ReadGraph graph) {
this.L0 = Layer0.getInstance(graph);
this.DIA = DiagramResource.getInstance(graph);
this.STR = StructuralResource2.getInstance(graph);
this.MOD = ModelingResources.getInstance(graph);
}
public void split(WriteGraph graph, IElement edgeElement, EdgeResource edge, Point2D splitCanvasPos) throws DatabaseException {
ConnectionUtil cu = new ConnectionUtil(graph);
Resource connection = ConnectionUtil.getConnection(graph, edge);
Resource diagram = OrderedSetUtils.getSingleOwnerList(graph, connection, DIA.Diagram);
// Disconnect the edge to calculate the two parts that remain.
cu.disconnect(edge);
Splitter.Parts parts1 = Parts.calculate(graph, edge.first());
Splitter.Parts parts2 = Parts.calculate(graph, edge.second());
// Resolve which part contains the "output" and which contains
// "input" to properly position the created flags.
Splitter.Parts moveToNewConnection = parts2;
Resource keepConnector = edge.first();
Resource moveToNewConnectionConnector = edge.second();
boolean inputsOnly1 = parts1.hasInputsOnly(graph);
boolean inputsOnly2 = parts2.hasInputsOnly(graph);
if (inputsOnly1 && inputsOnly2) {
// Let it be, can't really do better with this information.
} else if (inputsOnly1) {
// Parts1 has input connectors only, therefore we should
// move those to the new connection instead of parts2.
moveToNewConnection = parts1;
keepConnector = edge.second();
moveToNewConnectionConnector = edge.first();
}
Resource connectionType = graph.getSingleType(connection, DIA.Connection);
Resource hasConnectionType = graph.getPossibleObject(connection, STR.HasConnectionType);
Resource newConnection = cu.newConnection(diagram, connectionType);
if (hasConnectionType != null)
graph.claim(newConnection, STR.HasConnectionType, null, hasConnectionType);
// Copy connection routing
for (Statement routing : graph.getStatements(connection, DIA.Routing))
graph.claim(newConnection, routing.getPredicate(), newConnection);
for (Statement stm : moveToNewConnection.parts) {
graph.deny(stm);
graph.claim(stm.getSubject(), stm.getPredicate(), newConnection);
}
// 1 = output, 2 = input
Type type1 = FlagClass.Type.Out;
Type type2 = FlagClass.Type.In;
// Resolve the "positive" direction of the clicked edge to be split.
Line2D nearestEdge= ConnectionUtil.resolveNearestEdgeLineSegment(splitCanvasPos, edgeElement);
// Calculate split position and edge line nearest intersection point
// ab = normalize( vec(a -> b) )
// ap = vec(a -> split pos)
Vector2d ab = new Vector2d(nearestEdge.getX2() - nearestEdge.getX1(), nearestEdge.getY2() - nearestEdge.getY1());
Vector2d ap = new Vector2d(splitCanvasPos.getX() - nearestEdge.getX1(), splitCanvasPos.getY() - nearestEdge.getY1());
double theta = Math.atan2(ab.y, ab.x);
ab.normalize();
// intersection = a + ab*(ap.ab)
Vector2d intersection = new Vector2d(ab);
intersection.scale(ap.dot(ab));
intersection.add(new Vector2d(nearestEdge.getX1(), nearestEdge.getY1()));
// Offset flag positions from the intersection point.
Vector2d pos1 = new Vector2d(intersection);
Vector2d pos2 = new Vector2d(intersection);
// TODO: improve logic for flag positioning, flags just move on the nearest line without boundaries
ab.normalize();
ab.scale(5);
pos2.add(ab);
ab.negate();
pos1.add(ab);
FlagLabelingScheme scheme = DiagramFlagPreferences.getActiveFlagLabelingScheme(graph);
String commonLabel = scheme.generateLabel(graph, diagram);
// Create flags and connect both disconnected ends to them.
Resource flag1 = createFlag(graph, diagram, getFlagTransform(pos1, theta), type1, commonLabel);
Resource flag2 = createFlag(graph, diagram, getFlagTransform(pos2, theta), type2, commonLabel);
Resource flagConnector1 = cu.newConnector(connection, DIA.HasArrowConnector);
Resource flagConnector2 = cu.newConnector(newConnection, DIA.HasPlainConnector);
graph.claim(flag1, DIA.Flag_ConnectionPoint, flagConnector1);
graph.claim(flag2, DIA.Flag_ConnectionPoint, flagConnector2);
graph.claim(flagConnector1, DIA.AreConnected, keepConnector);
graph.claim(flagConnector2, DIA.AreConnected, moveToNewConnectionConnector);
FlagUtil.join(graph, flag1, flag2);
}
private AffineTransform getFlagTransform(Tuple2d pos, double theta) {
AffineTransform at = AffineTransform.getTranslateInstance(pos.x, pos.y);
at.rotate(theta);
return at;
}
private Resource createFlag(WriteGraph graph, Resource diagram, AffineTransform tr, FlagClass.Type type, String label) throws DatabaseException {
DiagramResource DIA = DiagramResource.getInstance(graph);
Resource flag = graph.newResource();
graph.claim(flag, L0.InstanceOf, null, DIA.Flag);
AddElement.claimFreshElementName(graph, diagram, flag);
graph.claim(flag, L0.PartOf, L0.ConsistsOf, diagram);
DiagramGraphUtil.setTransform(graph, flag, tr);
if (type != null)
FlagUtil.setFlagType(graph, flag, type);
if (label != null)
graph.claimLiteral(flag, L0.HasLabel, DIA.FlagLabel, label, Bindings.STRING);
OrderedSetUtils.add(graph, diagram, flag);
return flag;
}
static class Parts {
DiagramResource DIA;
Set parts = new HashSet();
private Parts(ReadGraph graph) {
this.DIA = DiagramResource.getInstance(graph);
}
public int size() {
return parts.size();
}
public static Splitter.Parts calculate(ReadGraph graph, Resource routeNode) throws DatabaseException {
DiagramResource DIA = DiagramResource.getInstance(graph);
Splitter.Parts p = new Parts(graph);
Deque todo = new ArrayDeque();
Set visited = new HashSet();
todo.add(routeNode);
while (!todo.isEmpty()) {
Resource part = todo.poll();
if (!visited.add(part))
continue;
todo.addAll(graph.getObjects(part, DIA.AreConnected));
Statement toConnection = graph.getPossibleStatement(part, DIA.IsConnectorOf);
if (toConnection == null)
toConnection = graph.getPossibleStatement(part, DIA.HasInteriorRouteNode_Inverse);
if (toConnection == null)
continue;
p.parts.add(toConnection);
}
return p;
}
public boolean hasInputsOnly(ReadGraph graph) throws ServiceException {
return hasInputs(graph) && !hasOutputs(graph);
}
public boolean hasInputs(ReadGraph graph) throws ServiceException {
return hasRelations(graph, DIA.IsArrowConnectorOf);
}
public boolean hasOutputs(ReadGraph graph) throws ServiceException {
return hasRelations(graph, DIA.IsPlainConnectorOf);
}
public boolean hasRelations(ReadGraph graph, Resource relation) throws ServiceException {
for (Statement stm : parts)
if (graph.isSubrelationOf(stm.getPredicate(), relation))
return true;
return false;
}
}
}