1 package org.simantics.diagram.flag;
\r
3 import java.awt.geom.AffineTransform;
\r
4 import java.awt.geom.Line2D;
\r
5 import java.awt.geom.Point2D;
\r
6 import java.util.ArrayDeque;
\r
7 import java.util.Deque;
\r
8 import java.util.HashSet;
\r
9 import java.util.Set;
\r
11 import javax.vecmath.Tuple2d;
\r
12 import javax.vecmath.Vector2d;
\r
14 import org.simantics.databoard.Bindings;
\r
15 import org.simantics.db.ReadGraph;
\r
16 import org.simantics.db.Resource;
\r
17 import org.simantics.db.Statement;
\r
18 import org.simantics.db.WriteGraph;
\r
19 import org.simantics.db.common.utils.OrderedSetUtils;
\r
20 import org.simantics.db.exception.DatabaseException;
\r
21 import org.simantics.db.exception.ServiceException;
\r
22 import org.simantics.diagram.content.ConnectionUtil;
\r
23 import org.simantics.diagram.content.EdgeResource;
\r
24 import org.simantics.diagram.stubs.DiagramResource;
\r
25 import org.simantics.diagram.synchronization.graph.AddElement;
\r
26 import org.simantics.diagram.synchronization.graph.DiagramGraphUtil;
\r
27 import org.simantics.g2d.element.IElement;
\r
28 import org.simantics.g2d.elementclass.FlagClass;
\r
29 import org.simantics.g2d.elementclass.FlagClass.Type;
\r
30 import org.simantics.layer0.Layer0;
\r
31 import org.simantics.modeling.ModelingResources;
\r
32 import org.simantics.structural.stubs.StructuralResource2;
\r
35 * A class that handles splitting a connection in two with diagram local flags.
\r
37 * The connection splitting process consists of the following steps:
\r
39 * <li>Disconnect the end points of the selected connection edge segment (SEG).
\r
40 * An end point is either :DIA.BranchPoint or (terminal) DIA:Connector. This
\r
41 * operation will always split a valid connection into two separate parts.</li>
\r
42 * <li>Calculate the contents of the two separated connection parts (branch
\r
43 * points and connectors) and decide which part to leave with the existing
\r
44 * connection (=P1) and which part to move into a new connection (=P2). The
\r
45 * current strategy is to move the parts that
\r
46 * <em>do not contain output terminal connectors</em> into a new connection.
\r
47 * This works well with diagram to composite mappings where output terminals
\r
48 * generate modules behind connections.</li>
\r
49 * <li>Create new connection C' with the same type and STR.HasConnectionType as
\r
50 * the original connection C.</li>
\r
51 * <li>Copy connection routing settings from C to C'.</li>
\r
52 * <li>Move P2 into C'.</li>
\r
53 * <li>Create two new flag elements into the same diagram, set their type and
\r
54 * interpolate a proper transformation for both along the existing connection
\r
56 * <li>Connect the new flags to begin(SEG) and end(SEG) connectors.</li>
\r
57 * <li>Join the flags together.</li>
\r
60 * @author Tuukka Lehtonen
\r
62 public class Splitter {
\r
65 DiagramResource DIA;
\r
66 StructuralResource2 STR;
\r
67 ModelingResources MOD;
\r
69 public Splitter(ReadGraph graph) {
\r
70 this.L0 = Layer0.getInstance(graph);
\r
71 this.DIA = DiagramResource.getInstance(graph);
\r
72 this.STR = StructuralResource2.getInstance(graph);
\r
73 this.MOD = ModelingResources.getInstance(graph);
\r
76 public void split(WriteGraph graph, IElement edgeElement, EdgeResource edge, Point2D splitCanvasPos) throws DatabaseException {
\r
77 ConnectionUtil cu = new ConnectionUtil(graph);
\r
79 Resource connection = ConnectionUtil.getConnection(graph, edge);
\r
80 Resource diagram = OrderedSetUtils.getSingleOwnerList(graph, connection, DIA.Diagram);
\r
82 // Disconnect the edge to calculate the two parts that remain.
\r
83 cu.disconnect(edge);
\r
85 Splitter.Parts parts1 = Parts.calculate(graph, edge.first());
\r
86 Splitter.Parts parts2 = Parts.calculate(graph, edge.second());
\r
88 // Resolve which part contains the "output" and which contains
\r
89 // "input" to properly position the created flags.
\r
90 Splitter.Parts moveToNewConnection = parts2;
\r
91 Resource keepConnector = edge.first();
\r
92 Resource moveToNewConnectionConnector = edge.second();
\r
94 boolean inputsOnly1 = parts1.hasInputsOnly(graph);
\r
95 boolean inputsOnly2 = parts2.hasInputsOnly(graph);
\r
97 if (inputsOnly1 && inputsOnly2) {
\r
98 // Let it be, can't really do better with this information.
\r
99 } else if (inputsOnly1) {
\r
100 // Parts1 has input connectors only, therefore we should
\r
101 // move those to the new connection instead of parts2.
\r
102 moveToNewConnection = parts1;
\r
103 keepConnector = edge.second();
\r
104 moveToNewConnectionConnector = edge.first();
\r
107 Resource connectionType = graph.getSingleType(connection, DIA.Connection);
\r
108 Resource hasConnectionType = graph.getPossibleObject(connection, STR.HasConnectionType);
\r
109 Resource newConnection = cu.newConnection(diagram, connectionType);
\r
110 if (hasConnectionType != null)
\r
111 graph.claim(newConnection, STR.HasConnectionType, null, hasConnectionType);
\r
113 // Copy connection routing
\r
114 for (Statement routing : graph.getStatements(connection, DIA.Routing))
\r
115 graph.claim(newConnection, routing.getPredicate(), newConnection);
\r
117 for (Statement stm : moveToNewConnection.parts) {
\r
119 graph.claim(stm.getSubject(), stm.getPredicate(), newConnection);
\r
122 // 1 = output, 2 = input
\r
123 Type type1 = FlagClass.Type.Out;
\r
124 Type type2 = FlagClass.Type.In;
\r
126 // Resolve the "positive" direction of the clicked edge to be split.
\r
127 Line2D nearestEdge= ConnectionUtil.resolveNearestEdgeLineSegment(splitCanvasPos, edgeElement);
\r
129 // Calculate split position and edge line nearest intersection point
\r
130 // ab = normalize( vec(a -> b) )
\r
131 // ap = vec(a -> split pos)
\r
132 Vector2d ab = new Vector2d(nearestEdge.getX2() - nearestEdge.getX1(), nearestEdge.getY2() - nearestEdge.getY1());
\r
133 Vector2d ap = new Vector2d(splitCanvasPos.getX() - nearestEdge.getX1(), splitCanvasPos.getY() - nearestEdge.getY1());
\r
134 double theta = Math.atan2(ab.y, ab.x);
\r
137 // intersection = a + ab*(ap.ab)
\r
138 Vector2d intersection = new Vector2d(ab);
\r
139 intersection.scale(ap.dot(ab));
\r
140 intersection.add(new Vector2d(nearestEdge.getX1(), nearestEdge.getY1()));
\r
142 // Offset flag positions from the intersection point.
\r
143 Vector2d pos1 = new Vector2d(intersection);
\r
144 Vector2d pos2 = new Vector2d(intersection);
\r
146 // TODO: improve logic for flag positioning, flags just move on the nearest line without boundaries
\r
153 FlagLabelingScheme scheme = DiagramFlagPreferences.getActiveFlagLabelingScheme(graph);
\r
154 String commonLabel = scheme.generateLabel(graph, diagram);
\r
156 // Create flags and connect both disconnected ends to them.
\r
157 Resource flag1 = createFlag(graph, diagram, getFlagTransform(pos1, theta), type1, commonLabel);
\r
158 Resource flag2 = createFlag(graph, diagram, getFlagTransform(pos2, theta), type2, commonLabel);
\r
160 Resource flagConnector1 = cu.newConnector(connection, DIA.HasArrowConnector);
\r
161 Resource flagConnector2 = cu.newConnector(newConnection, DIA.HasPlainConnector);
\r
162 graph.claim(flag1, DIA.Flag_ConnectionPoint, flagConnector1);
\r
163 graph.claim(flag2, DIA.Flag_ConnectionPoint, flagConnector2);
\r
165 graph.claim(flagConnector1, DIA.AreConnected, keepConnector);
\r
166 graph.claim(flagConnector2, DIA.AreConnected, moveToNewConnectionConnector);
\r
168 FlagUtil.join(graph, flag1, flag2);
\r
171 private AffineTransform getFlagTransform(Tuple2d pos, double theta) {
\r
172 AffineTransform at = AffineTransform.getTranslateInstance(pos.x, pos.y);
\r
177 private Resource createFlag(WriteGraph graph, Resource diagram, AffineTransform tr, FlagClass.Type type, String label) throws DatabaseException {
\r
178 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
180 Resource flag = graph.newResource();
\r
181 graph.claim(flag, L0.InstanceOf, null, DIA.Flag);
\r
183 AddElement.claimFreshElementName(graph, diagram, flag);
\r
184 graph.claim(flag, L0.PartOf, L0.ConsistsOf, diagram);
\r
186 DiagramGraphUtil.setTransform(graph, flag, tr);
\r
188 FlagUtil.setFlagType(graph, flag, type);
\r
191 graph.claimLiteral(flag, L0.HasLabel, DIA.FlagLabel, label, Bindings.STRING);
\r
193 OrderedSetUtils.add(graph, diagram, flag);
\r
197 static class Parts {
\r
198 DiagramResource DIA;
\r
199 Set<Statement> parts = new HashSet<Statement>();
\r
201 private Parts(ReadGraph graph) {
\r
202 this.DIA = DiagramResource.getInstance(graph);
\r
205 public int size() {
\r
206 return parts.size();
\r
209 public static Splitter.Parts calculate(ReadGraph graph, Resource routeNode) throws DatabaseException {
\r
210 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
212 Splitter.Parts p = new Parts(graph);
\r
213 Deque<Resource> todo = new ArrayDeque<Resource>();
\r
214 Set<Resource> visited = new HashSet<Resource>();
\r
216 todo.add(routeNode);
\r
217 while (!todo.isEmpty()) {
\r
218 Resource part = todo.poll();
\r
219 if (!visited.add(part))
\r
222 todo.addAll(graph.getObjects(part, DIA.AreConnected));
\r
224 Statement toConnection = graph.getPossibleStatement(part, DIA.IsConnectorOf);
\r
225 if (toConnection == null)
\r
226 toConnection = graph.getPossibleStatement(part, DIA.HasInteriorRouteNode_Inverse);
\r
227 if (toConnection == null)
\r
230 p.parts.add(toConnection);
\r
236 public boolean hasInputsOnly(ReadGraph graph) throws ServiceException {
\r
237 return hasInputs(graph) && !hasOutputs(graph);
\r
240 public boolean hasInputs(ReadGraph graph) throws ServiceException {
\r
241 return hasRelations(graph, DIA.IsArrowConnectorOf);
\r
244 public boolean hasOutputs(ReadGraph graph) throws ServiceException {
\r
245 return hasRelations(graph, DIA.IsPlainConnectorOf);
\r
248 public boolean hasRelations(ReadGraph graph, Resource relation) throws ServiceException {
\r
249 for (Statement stm : parts)
\r
250 if (graph.isSubrelationOf(stm.getPredicate(), relation))
\r