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