1 package org.simantics.diagram.flag;
3 import java.awt.geom.AffineTransform;
4 import java.awt.geom.Line2D;
5 import java.awt.geom.Point2D;
6 import java.util.ArrayDeque;
7 import java.util.Deque;
8 import java.util.HashSet;
11 import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;
12 import org.simantics.databoard.Bindings;
13 import org.simantics.db.ReadGraph;
14 import org.simantics.db.Resource;
15 import org.simantics.db.Statement;
16 import org.simantics.db.WriteGraph;
17 import org.simantics.db.common.utils.OrderedSetUtils;
18 import org.simantics.db.exception.DatabaseException;
19 import org.simantics.db.exception.ServiceException;
20 import org.simantics.diagram.content.ConnectionUtil;
21 import org.simantics.diagram.content.EdgeResource;
22 import org.simantics.diagram.stubs.DiagramResource;
23 import org.simantics.diagram.synchronization.graph.AddElement;
24 import org.simantics.diagram.synchronization.graph.DiagramGraphUtil;
25 import org.simantics.g2d.element.IElement;
26 import org.simantics.g2d.elementclass.FlagClass;
27 import org.simantics.g2d.elementclass.FlagClass.Type;
28 import org.simantics.layer0.Layer0;
29 import org.simantics.modeling.ModelingResources;
30 import org.simantics.structural.stubs.StructuralResource2;
33 * A class that handles splitting a connection in two with diagram local flags.
35 * The connection splitting process consists of the following steps:
37 * <li>Disconnect the end points of the selected connection edge segment (SEG).
38 * An end point is either :DIA.BranchPoint or (terminal) DIA:Connector. This
39 * operation will always split a valid connection into two separate parts.</li>
40 * <li>Calculate the contents of the two separated connection parts (branch
41 * points and connectors) and decide which part to leave with the existing
42 * connection (=P1) and which part to move into a new connection (=P2). The
43 * current strategy is to move the parts that
44 * <em>do not contain output terminal connectors</em> into a new connection.
45 * This works well with diagram to composite mappings where output terminals
46 * generate modules behind connections.</li>
47 * <li>Create new connection C' with the same type and STR.HasConnectionType as
48 * the original connection C.</li>
49 * <li>Copy connection routing settings from C to C'.</li>
50 * <li>Move P2 into C'.</li>
51 * <li>Create two new flag elements into the same diagram, set their type and
52 * interpolate a proper transformation for both along the existing connection
54 * <li>Connect the new flags to begin(SEG) and end(SEG) connectors.</li>
55 * <li>Join the flags together.</li>
58 * @author Tuukka Lehtonen
60 public class Splitter {
64 StructuralResource2 STR;
65 ModelingResources MOD;
67 public Splitter(ReadGraph graph) {
68 this.L0 = Layer0.getInstance(graph);
69 this.DIA = DiagramResource.getInstance(graph);
70 this.STR = StructuralResource2.getInstance(graph);
71 this.MOD = ModelingResources.getInstance(graph);
74 public void split(WriteGraph graph, IElement edgeElement, EdgeResource edge, Point2D splitCanvasPos) throws DatabaseException {
75 ConnectionUtil cu = new ConnectionUtil(graph);
77 Resource connection = ConnectionUtil.getConnection(graph, edge);
78 Resource diagram = OrderedSetUtils.getSingleOwnerList(graph, connection, DIA.Diagram);
80 // Disconnect the edge to calculate the two parts that remain.
83 Splitter.Parts parts1 = Parts.calculate(graph, edge.first());
84 Splitter.Parts parts2 = Parts.calculate(graph, edge.second());
86 // Resolve which part contains the "output" and which contains
87 // "input" to properly position the created flags.
88 Splitter.Parts moveToNewConnection = parts2;
89 Resource keepConnector = edge.first();
90 Resource moveToNewConnectionConnector = edge.second();
92 boolean inputsOnly1 = parts1.hasInputsOnly(graph);
93 boolean inputsOnly2 = parts2.hasInputsOnly(graph);
95 if (inputsOnly1 && inputsOnly2) {
96 // Let it be, can't really do better with this information.
97 } else if (inputsOnly1) {
98 // Parts1 has input connectors only, therefore we should
99 // move those to the new connection instead of parts2.
100 moveToNewConnection = parts1;
101 keepConnector = edge.second();
102 moveToNewConnectionConnector = edge.first();
105 Resource connectionType = graph.getSingleType(connection, DIA.Connection);
106 Resource hasConnectionType = graph.getPossibleObject(connection, STR.HasConnectionType);
107 Resource newConnection = cu.newConnection(diagram, connectionType);
108 if (hasConnectionType != null)
109 graph.claim(newConnection, STR.HasConnectionType, null, hasConnectionType);
111 // Copy connection routing
112 for (Statement routing : graph.getStatements(connection, DIA.Routing))
113 graph.claim(newConnection, routing.getPredicate(), newConnection);
115 for (Statement stm : moveToNewConnection.parts) {
117 graph.claim(stm.getSubject(), stm.getPredicate(), newConnection);
120 // 1 = output, 2 = input
121 Type type1 = FlagClass.Type.Out;
122 Type type2 = FlagClass.Type.In;
124 // Resolve the "positive" direction of the clicked edge to be split.
125 Line2D nearestEdge= ConnectionUtil.resolveNearestEdgeLineSegment(splitCanvasPos, edgeElement);
127 // Calculate split position and edge line nearest intersection point
128 // ab = normalize( vec(a -> b) )
129 // ap = vec(a -> split pos)
130 Vector2D a = new Vector2D(nearestEdge.getX1(), nearestEdge.getY1());
131 Vector2D ab = new Vector2D(nearestEdge.getX2() - nearestEdge.getX1(), nearestEdge.getY2() - nearestEdge.getY1());
132 Vector2D ap = new Vector2D(splitCanvasPos.getX() - nearestEdge.getX1(), splitCanvasPos.getY() - nearestEdge.getY1());
133 double theta = Math.atan2(ab.getY(), ab.getX());
136 // intersection = a + ab*(ap.ab)
137 Vector2D intersection = a.add( ab.scalarMultiply(ap.dotProduct(ab)) );
139 // Offset flag positions from the intersection point.
140 // TODO: improve logic for flag positioning, flags just move on the nearest line without boundaries
141 Vector2D pos1 = intersection.subtract(5, ab);
142 Vector2D pos2 = intersection.add(5, ab);
144 FlagLabelingScheme scheme = DiagramFlagPreferences.getActiveFlagLabelingScheme(graph);
145 String commonLabel = scheme.generateLabel(graph, diagram);
147 // Create flags and connect both disconnected ends to them.
148 Resource flag1 = createFlag(graph, diagram, getFlagTransform(pos1, theta), type1, commonLabel);
149 Resource flag2 = createFlag(graph, diagram, getFlagTransform(pos2, theta), type2, commonLabel);
151 Resource flagConnector1 = cu.newConnector(connection, DIA.HasArrowConnector);
152 Resource flagConnector2 = cu.newConnector(newConnection, DIA.HasPlainConnector);
153 graph.claim(flag1, DIA.Flag_ConnectionPoint, flagConnector1);
154 graph.claim(flag2, DIA.Flag_ConnectionPoint, flagConnector2);
156 graph.claim(flagConnector1, DIA.AreConnected, keepConnector);
157 graph.claim(flagConnector2, DIA.AreConnected, moveToNewConnectionConnector);
159 FlagUtil.join(graph, flag1, flag2);
162 private AffineTransform getFlagTransform(Vector2D pos, double theta) {
163 AffineTransform at = AffineTransform.getTranslateInstance(pos.getX(), pos.getY());
168 private Resource createFlag(WriteGraph graph, Resource diagram, AffineTransform tr, FlagClass.Type type, String label) throws DatabaseException {
169 DiagramResource DIA = DiagramResource.getInstance(graph);
171 Resource flag = graph.newResource();
172 graph.claim(flag, L0.InstanceOf, null, DIA.Flag);
174 AddElement.claimFreshElementName(graph, diagram, flag);
175 graph.claim(flag, L0.PartOf, L0.ConsistsOf, diagram);
177 DiagramGraphUtil.setTransform(graph, flag, tr);
179 FlagUtil.setFlagType(graph, flag, type);
182 graph.claimLiteral(flag, L0.HasLabel, DIA.FlagLabel, label, Bindings.STRING);
184 OrderedSetUtils.add(graph, diagram, flag);
190 Set<Statement> parts = new HashSet<Statement>();
192 private Parts(ReadGraph graph) {
193 this.DIA = DiagramResource.getInstance(graph);
200 public static Splitter.Parts calculate(ReadGraph graph, Resource routeNode) throws DatabaseException {
201 DiagramResource DIA = DiagramResource.getInstance(graph);
203 Splitter.Parts p = new Parts(graph);
204 Deque<Resource> todo = new ArrayDeque<Resource>();
205 Set<Resource> visited = new HashSet<Resource>();
208 while (!todo.isEmpty()) {
209 Resource part = todo.poll();
210 if (!visited.add(part))
213 todo.addAll(graph.getObjects(part, DIA.AreConnected));
215 Statement toConnection = graph.getPossibleStatement(part, DIA.IsConnectorOf);
216 if (toConnection == null)
217 toConnection = graph.getPossibleStatement(part, DIA.HasInteriorRouteNode_Inverse);
218 if (toConnection == null)
221 p.parts.add(toConnection);
227 public boolean hasInputsOnly(ReadGraph graph) throws ServiceException {
228 return hasInputs(graph) && !hasOutputs(graph);
231 public boolean hasInputs(ReadGraph graph) throws ServiceException {
232 return hasRelations(graph, DIA.IsArrowConnectorOf);
235 public boolean hasOutputs(ReadGraph graph) throws ServiceException {
236 return hasRelations(graph, DIA.IsPlainConnectorOf);
239 public boolean hasRelations(ReadGraph graph, Resource relation) throws ServiceException {
240 for (Statement stm : parts)
241 if (graph.isSubrelationOf(stm.getPredicate(), relation))