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 org.apache.commons.math3.geometry.euclidean.twod.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: *
    *
  1. 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.
  2. *
  3. 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.
  4. *
  5. Create new connection C' with the same type and STR.HasConnectionType as * the original connection C.
  6. *
  7. Copy connection routing settings from C to C'.
  8. *
  9. Move P2 into C'.
  10. *
  11. Create two new flag elements into the same diagram, set their type and * interpolate a proper transformation for both along the existing connection * line.
  12. *
  13. Connect the new flags to begin(SEG) and end(SEG) connectors.
  14. *
  15. Join the flags together.
  16. *
* * @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 a = new Vector2D(nearestEdge.getX1(), nearestEdge.getY1()); 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.getY(), ab.getX()); ab = ab.normalize(); // intersection = a + ab*(ap.ab) Vector2D intersection = a.add( ab.scalarMultiply(ap.dotProduct(ab)) ); // Offset flag positions from the intersection point. // TODO: improve logic for flag positioning, flags just move on the nearest line without boundaries Vector2D pos1 = intersection.subtract(5, ab); Vector2D pos2 = intersection.add(5, 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(Vector2D pos, double theta) { AffineTransform at = AffineTransform.getTranslateInstance(pos.getX(), pos.getY()); 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; } } }