package org.simantics.diagram.flag; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Collection; import java.util.List; 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.request.PossibleTypedParent; import org.simantics.db.common.utils.NameUtils; import org.simantics.db.common.utils.OrderedSetUtils; import org.simantics.db.exception.DatabaseException; import org.simantics.db.service.SerialisationSupport; import org.simantics.diagram.adapter.RouteGraphUtils; import org.simantics.diagram.connection.RouteGraph; import org.simantics.diagram.connection.RouteLine; import org.simantics.diagram.connection.RouteNode; import org.simantics.diagram.connection.RoutePoint; import org.simantics.diagram.connection.RouteTerminal; import org.simantics.diagram.connection.splitting.SplittedRouteGraph; import org.simantics.diagram.connection.splitting.SplittedRouteGraph.PickResult; import org.simantics.diagram.content.ConnectionUtil; import org.simantics.diagram.stubs.DiagramResource; import org.simantics.diagram.synchronization.graph.AddElement; import org.simantics.diagram.synchronization.graph.BasicResources; import org.simantics.diagram.synchronization.graph.DiagramGraphUtil; import org.simantics.diagram.synchronization.graph.RouteGraphModification; import org.simantics.g2d.elementclass.FlagClass; import org.simantics.layer0.Layer0; import org.simantics.modeling.ModelingResources; import org.simantics.structural.stubs.StructuralResource2; import gnu.trove.map.hash.TObjectIntHashMap; /** * A class that handles splitting a route graph 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 * @author Hannu Niemistö */ public class RouteGraphConnectionSplitter { private final static boolean DEBUG = false; Layer0 L0; DiagramResource DIA; StructuralResource2 STR; ModelingResources MOD; SerialisationSupport ss; public RouteGraphConnectionSplitter(ReadGraph graph) { this.L0 = Layer0.getInstance(graph); this.DIA = DiagramResource.getInstance(graph); this.STR = StructuralResource2.getInstance(graph); this.MOD = ModelingResources.getInstance(graph); this.ss = graph.getService(SerialisationSupport.class); } public void split(WriteGraph graph, Resource connection, RouteGraph rg, Point2D splitCanvasPos) throws DatabaseException { // Create modification writer RouteGraphModification modis = new RouteGraphModification(ss, rg); TObjectIntHashMap idMap = modis.getIdMap(); if (DEBUG) { System.out.println("Split canvas position: " + splitCanvasPos); rg.print(); } // Find the edge to disconnect in the graph. // Bisect the nearest route line. PickResult picked = SplittedRouteGraph.pickNearestLine(rg, splitCanvasPos.getX(), splitCanvasPos.getY()); if (picked == null) return; RouteLine line = picked.nearestLine; if (DEBUG) { System.out.println("picked nearest line:"); line.print(System.out); for (RoutePoint rp : line.getPoints()) System.out.println("RP: " + rp.getX() + ", " + rp.getY()); } // Get exact intersection point on the line double isectX = picked.intersectionPoint.getX(); double isectY = picked.intersectionPoint.getY(); SplittedRouteGraph srg = rg.splitGraph(line, line.isHorizontal() ? isectX : isectY); if (DEBUG) System.out.println(srg); // Disconnect if(rg.isSimpleConnection()) { RouteNode na = srg.terminals1.iterator().next(); RouteNode nb = srg.terminals2.iterator().next(); Resource a = ss.getResource((Long)na.getData()); Resource b = ss.getResource((Long)nb.getData()); graph.deny(a, DIA.AreConnected, b); modis.addModi(new RouteGraphModification.RemoveLink( idMap.get(na), idMap.get(nb) )); } else if(srg.splitLine.isTransient()) { RouteTerminal terminal = srg.splitLine.getTerminal(); Resource connector = ss.getResource((Long)terminal.getData()); graph.deny(connector, DIA.AreConnected); modis.addModi(new RouteGraphModification.RemoveLink( idMap.get(terminal), idMap.get(terminal.getLine()) )); } else { graph.deny(ss.getResource((Long)srg.splitLine.getData())); // TODO remove links modis.addModi(new RouteGraphModification.RemoveLine( idMap.get(srg.splitLine) )); } ArrayList terminals1Resources = toResources(srg.terminals1); ArrayList terminals2Resources = toResources(srg.terminals2); boolean mustFlip = analyzePartInputs(graph, terminals1Resources, terminals2Resources); ArrayList interfaceNodes1 = toResources(mustFlip ? srg.interfaceNodes2 : srg.interfaceNodes1); ArrayList interfaceNodes2 = toResources(mustFlip ? srg.interfaceNodes1 : srg.interfaceNodes2); ArrayList lines2 = toResources(mustFlip ? srg.lines1 : srg.lines2); ArrayList terminals1 = mustFlip ? terminals2Resources : terminals1Resources; ArrayList terminals2 = mustFlip ? terminals1Resources : terminals2Resources; doSplit(graph, connection, interfaceNodes1, interfaceNodes2, lines2, terminals1, terminals2, line.isHorizontal(), mustFlip, isectX, isectY); modis.addModi(new RouteGraphModification.Split( modis.toIds(interfaceNodes1), modis.toIds(interfaceNodes2), modis.toIds(lines2), modis.toIds(terminals1), modis.toIds(terminals2), line.isHorizontal(), mustFlip, isectX, isectY )); } private ArrayList toResources(Collection nodes) throws DatabaseException { ArrayList result = new ArrayList<>(nodes.size()); for (RouteNode n : nodes) result.add(ss.getResource((Long)n.getData())); return result; } /** * @param graph * @param terminals1 * @param terminals2 * @return true if inputs need to be flipped, i.e. if terminals2 * contains the output terminals and terminals1 doesn't. * @throws DatabaseException */ private boolean analyzePartInputs(ReadGraph graph, List terminals1, List terminals2) throws DatabaseException { @SuppressWarnings("unused") int inputs1 = 0, outputs1 = 0; for(Resource connector : terminals1) { if(graph.hasStatement(connector, DIA.IsHeadConnectorOf)) ++inputs1; else ++outputs1; } @SuppressWarnings("unused") int inputs2 = 0, outputs2 = 0; for(Resource connector : terminals2) { if(graph.hasStatement(connector, DIA.IsHeadConnectorOf)) ++inputs2; else ++outputs2; } boolean mustFlip = outputs1 == 0; if (DEBUG) { System.out.println("inputs1: " + inputs1); System.out.println("outputs1: " + outputs1); System.out.println("inputs2: " + inputs2); System.out.println("outputs2: " + outputs2); System.out.println("=> type1: " + (mustFlip ? FlagClass.Type.In : FlagClass.Type.Out)); System.out.println("=> type2: " + (mustFlip ? FlagClass.Type.Out : FlagClass.Type.In)); System.out.println("=> must flip route graph parts to split: " + mustFlip); } return mustFlip; } private static String routeNodeDebugInfo(ReadGraph graph, Resource c) throws DatabaseException { BasicResources BR = BasicResources.getInstance(graph); String ctr = NameUtils.getSafeName(graph, c, true); for (Resource e : graph.getObjects(c, BR.STR.Connects)) { ctr += " --> " + NameUtils.getSafeName(graph, e); } for (Resource e : graph.getObjects(c, BR.DIA.AreConnected)) { ctr += " <-> " + NameUtils.getSafeName(graph, e); } return ctr; } /** * Internal routine that is only public because * {@link RouteGraphModification#runUpdates(WriteGraph)} needs to invoke it. * * Assumes that #1 parameters will stay with the existing connection and #2 * parameters will go to the newly created connection. */ public void doSplit(WriteGraph graph, Resource connection, ArrayList interfaceNodes1Resources, ArrayList interfaceNodes2Resources, ArrayList lines2Resources, ArrayList terminals1Resources, ArrayList terminals2Resources, boolean isHorizontal, boolean invertFlagRotation, double isectX, double isectY) throws DatabaseException { // 1 = output, 2 = input FlagClass.Type type1 = FlagClass.Type.Out, type2 = FlagClass.Type.In; if (DEBUG) { System.out.println("doSplit:"); System.out.println(NameUtils.getSafeName(graph, connection, true)); for (Resource i : interfaceNodes1Resources) System.out.println("i1: " + routeNodeDebugInfo(graph, i)); for (Resource i : interfaceNodes2Resources) System.out.println("i2: " + routeNodeDebugInfo(graph, i)); for (Resource l : lines2Resources) System.out.println("l2r: " + routeNodeDebugInfo(graph, l)); for (Resource t : terminals1Resources) System.out.println("t1: " + routeNodeDebugInfo(graph, t)); for (Resource t : terminals2Resources) System.out.println("t2: " + routeNodeDebugInfo(graph, t)); System.out.println("is horizontal: " + isHorizontal); System.out.println("@(x,y): " + isectX + ", " + isectY); } ConnectionUtil cu = new ConnectionUtil(graph); Resource diagram = OrderedSetUtils.getSingleOwnerList(graph, connection, DIA.Diagram); Resource diagramConnectionType = graph.getSingleType(connection, DIA.Connection); Resource hasConnectionType = graph.getPossibleObject(connection, STR.HasConnectionType); Resource newConnection = cu.newConnection(diagram, diagramConnectionType); if (hasConnectionType != null) graph.claim(newConnection, STR.HasConnectionType, null, hasConnectionType); // Give running name to connection increment the counter attached to the diagram. AddElement.claimFreshElementName(graph, diagram, newConnection); String commonLabel = DiagramFlagPreferences .getActiveFlagLabelingScheme(graph) .generateLabel(graph, diagram); Point2D pos1, pos2; double theta; double flagDist = 3.0; if(isHorizontal) { theta = 0.0; pos1 = new Point2D.Double(isectX-flagDist, isectY); pos2 = new Point2D.Double(isectX+flagDist, isectY); } else { theta = Math.PI*0.5; pos1 = new Point2D.Double(isectX, isectY-flagDist); pos2 = new Point2D.Double(isectX, isectY+flagDist); } if (invertFlagRotation) { theta += Math.PI; Point2D p = pos1; pos1 = pos2; pos2 = p; } // WORKAROUND for mapping problems: // If any terminal of the split connection contains a flag, make sure their STR.Joins relations are all removed // to give mapping a chance to fix them properly. removeFlagJoins(graph, cu, connection, terminals1Resources); removeFlagJoins(graph, cu, connection, terminals2Resources); // Move route nodes to correct connections for(Resource rn : lines2Resources) { // TODO: use same predicate that was removed graph.denyStatement(connection, DIA.HasInteriorRouteNode, rn); graph.claim(newConnection, DIA.HasInteriorRouteNode, rn); } for(Resource rn : terminals2Resources) { Statement stat = graph.getSingleStatement(rn, DIA.IsConnectorOf); Resource predicate = stat.getPredicate(); graph.deny(rn, predicate); graph.claim(rn, predicate, newConnection); } // 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); if (DEBUG) { System.out.println("LABEL FOR NEW FLAGS: " + commonLabel); System.out.println("FLAG1: " + NameUtils.getSafeName(graph, flag1, true)); System.out.println("FLAG2: " + NameUtils.getSafeName(graph, flag2, true)); } 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); double position = isHorizontal ? isectY : isectX; connectFlag(graph, isHorizontal, position, connection, flagConnector1, interfaceNodes1Resources); connectFlag(graph, isHorizontal, position, newConnection, flagConnector2, interfaceNodes2Resources); // Join the flags without activatingn diagram mapping at this point FlagUtil.join(graph, flag1, flag2, false); FlagUtil.fixBindsStatements(graph, graph.getPossibleObject(connection, MOD.DiagramConnectionToConnection)); // Finally ensure that all the diagrams related to the operation are mapped properly in one go FlagUtil.activateMappingForParentDiagramsOf(graph, flag1, flag2); } /** * A workaround for problems with mapping not removing the necessary * STR.Joins relations from flags after a split. * * @param graph * @param cu * @param connection * @param connectors * @throws DatabaseException */ private void removeFlagJoins(WriteGraph graph, ConnectionUtil cu, Resource connection, ArrayList connectors) throws DatabaseException { for (Resource connector : connectors) { Resource e = cu.getConnectedComponent(connection, connector); if (graph.isInstanceOf(e, DIA.Flag)) { Resource diagram = graph.syncRequest(new PossibleTypedParent(e, DIA.Diagram)); if (diagram == null) continue; for (Resource join : graph.getObjects(e, DIA.FlagIsJoinedBy)) { Collection joinsComposites = graph.getObjects(join, STR.JoinsComposite); if (joinsComposites.size() == 1) { // Only remove joins that are internal to a diagram. graph.deny(join, STR.Joins); } else if (joinsComposites.size() == 2) { // Only remove the joins relations that refer to // connections that are part of the same diagram. for (Resource joins : graph.getObjects(join, STR.Joins)) { Resource diagramConnection = graph.getPossibleObject(joins, MOD.ConnectionToDiagramConnection); if (diagramConnection == null) continue; Resource partOfDiagram = graph.syncRequest(new PossibleTypedParent(diagramConnection, DIA.Diagram)); if (diagram.equals(partOfDiagram)) { graph.deny(join, STR.Joins, joins); } } } } } } } private void connectFlag(WriteGraph graph, boolean isHorizontal, double position, Resource connection, Resource flagConnector, Collection interfaceNodes) throws DatabaseException { if(interfaceNodes.size() > 1) { Resource routeLine = graph.newResource(); graph.claim(routeLine, L0.InstanceOf, DIA.RouteLine); graph.claim(connection, DIA.HasInteriorRouteNode, routeLine); graph.claimLiteral(routeLine, DIA.IsHorizontal, isHorizontal); graph.claimLiteral(routeLine, DIA.HasPosition, position); graph.claim(routeLine, DIA.AreConnected, flagConnector); flagConnector = routeLine; } for(Resource rn : interfaceNodes) { graph.claim(flagConnector, DIA.AreConnected, rn); } } private AffineTransform getFlagTransform(Point2D 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; } public static void splitConnection(WriteGraph graph, Resource connection, double x, double y) throws DatabaseException { // TODO: provide a proper runtimeDiagram parameter to load to support also connections attached to flags attached to diagram template flag tables RouteGraph rg = RouteGraphUtils.load(graph, null, connection); new RouteGraphConnectionSplitter(graph).split(graph, connection, rg, new Point2D.Double(x, y)); } }