Commit cc78488e authored by Tuukka Lehtonen's avatar Tuukka Lehtonen
Browse files

Fixed route graph splitting and diagram mapping race condition problem

RouteGraphConnectionSplitter.doSplit now always splits the connection so
that part #1 is always the part that stays with the existing diagram
connection and part #2 contains the entities that are moved to the newly
created route graph connection resource. Part #1 is the part where the
"output terminal" attached to the connection lies. This simplifies and
clarifies the implementation of doSplit and does not require moving
diagram mapping statements around.

Also, more importantly RouteGraphConnectionSplitter and FlagUtil changes
ensure that diagram mapping is *not* executed in FlagUtil.join, which
was the main cause of the previous corruption. Diagram mapping is only
activated once for the affected diagram(s) after everything else is
done.

gitlab #549
gitlab #586

Change-Id: Icf7479e8e111bf663f637d6909419267cfa4eec4
parent 2526602d
......@@ -113,10 +113,17 @@ public class RouteLine implements RouteNode, Serializable {
out.print(" HOR");
else
out.print(" VER");
if (hidden)
out.print(" HIDDEN");
out.print(" @ " + position);
for(RoutePoint point : points) {
out.print(" ("+point.x+","+point.y+")");
}
out.print(" (data=" + data + ")");
if (nextTransient != null)
out.print(" (next transient line=" + nextTransient.getData() + ")");
if (terminal != null)
out.print(" (terminal=" + terminal.getData() + ")");
out.println();
}
......
......@@ -28,4 +28,9 @@ public class Segment {
public boolean isDegenerated() {
return p1.getX() == p2.getX() && p1.getY() == p2.getY();
}
@Override
public String toString() {
return String.format("(%f, %f) (%f, %f)", p1.getX(), p1.getY(), p2.getX(), p2.getY());
}
}
package org.simantics.diagram.connection.splitting;
import gnu.trove.set.hash.THashSet;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
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.segments.Segment;
import gnu.trove.set.hash.THashSet;
public class SplittedRouteGraph {
public final RouteLine splitLine;
......@@ -91,7 +95,75 @@ public class SplittedRouteGraph {
}
}
/**
public static final class PickResult {
/**
* The connection route line nearest to {@link #pickPoint}.
*/
public final RouteLine nearestLine;
/**
* Original pick point in canvas coordinates.
*/
public final Point2D pickPoint;
/**
* Intersection point in canvas coordinates of {@link #nearestLine} and
* perpendicular line from {@link #pickPoint} to {@link #nearestLine}.
*/
public final Point2D intersectionPoint;
public PickResult(RouteLine nearestLine, Point2D pickPoint, Point2D intersectionPoint) {
this.nearestLine = nearestLine;
this.pickPoint = pickPoint;
this.intersectionPoint = intersectionPoint;
}
}
public static PickResult pickNearestLine(RouteGraph rg, double x, double y) {
Segment nearestSegment = null;
RouteLine nearestLine = null;
ArrayList<Segment> segments = new ArrayList<>();
double minDistanceSq = Double.MAX_VALUE;
for (RouteLine line : rg.getAllLines()) {
segments.clear();
line.collectSegments(segments);
for (Segment segment : segments) {
RoutePoint p1 = segment.p1;
RoutePoint p2 = segment.p2;
double distanceSq = Line2D.ptSegDistSq(p1.getX(), p1.getY(), p2.getX(), p2.getY(), x, y);
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
nearestSegment = segment;
nearestLine = line;
}
}
}
if (nearestSegment == null)
return null;
RoutePoint p1 = nearestSegment.p1;
RoutePoint p2 = nearestSegment.p2;
Point2D p = pointToLineIntersection(p1.getX(), p1.getY(), p2.getX(), p2.getY(), x, y);
return new PickResult(nearestLine, new Point2D.Double(x, y), p);
}
private static Point2D pointToLineIntersection(double x1, double y1, double x2, double y2, double px, double py) {
double d = Math.pow(x2 - x1, 2.0) + Math.pow(y2 - y1, 2.0);
if (d == 0) {
return new Point2D.Double(x1, y1);
} else {
double u = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / d;
if (u > 1.0) {
return new Point2D.Double(x2, y2);
} else if (u <= 0.0) {
return new Point2D.Double(x1, y1);
} else {
return new Point2D.Double(x2 * u + x1 * (1.0-u), (y2 * u + y1 * (1.0- u)));
}
}
}
/**
* @param point
* @param line
* @return the specified point instance snapped to the specified line
......
......@@ -176,6 +176,17 @@ public final class FlagUtil {
* @throws DatabaseException
*/
public static Resource join(WriteGraph g, Resource flag, Resource otherFlag) throws DatabaseException {
return join(g, flag, otherFlag, true);
}
/**
* @param g
* @param flag
* @param otherFlag
* @return the created DIA.ConnectionJoin instance
* @throws DatabaseException
*/
public static Resource join(WriteGraph g, Resource flag, Resource otherFlag, boolean activateDiagramMapping) throws DatabaseException {
DiagramResource DIA = DiagramResource.getInstance(g);
StructuralResource2 STR = StructuralResource2.getInstance(g);
Resource connectionJoin = g.newResource();
......@@ -184,14 +195,24 @@ public final class FlagUtil {
g.claim(connectionJoin, DIA.JoinsFlag, flag);
g.claim(connectionJoin, DIA.JoinsFlag, otherFlag);
IActivationManager manager = g.getService(IActivationManager.class);
for(Resource diagram : OrderedSetUtils.getSubjects(g, flag))
manager.activateOnce(g, diagram);
for(Resource diagram : OrderedSetUtils.getSubjects(g, otherFlag))
manager.activateOnce(g, diagram);
if (activateDiagramMapping) {
activateMappingForParentDiagramsOf(g, flag, otherFlag);
}
return connectionJoin;
}
public static void activateMappingForParentDiagramsOf(WriteGraph graph, Resource... elements) throws DatabaseException {
IActivationManager manager = graph.getService(IActivationManager.class);
Set<Resource> diagrams = new HashSet<>(elements.length);
for (Resource e : elements) {
diagrams.addAll(OrderedSetUtils.getSubjects(graph, e));
}
for (Resource diagram : diagrams) {
manager.activateOnce(graph, diagram);
}
}
public static void disconnectFlag(WriteGraph graph, Resource flag) throws DatabaseException {
// Remove any :ConnectionJoin's this flag is joined by
// if there's less than two flags joined by the join.
......@@ -531,5 +552,4 @@ public final class FlagUtil {
return flags;
}
}
}
\ No newline at end of file
package org.simantics.diagram.flag;
import gnu.trove.map.hash.TObjectIntHashMap;
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;
......@@ -24,9 +23,11 @@ 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;
......@@ -34,6 +35,8 @@ 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.
......@@ -87,34 +90,33 @@ public class RouteGraphConnectionSplitter {
RouteGraphModification modis = new RouteGraphModification(ss, rg);
TObjectIntHashMap<RouteNode> 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.
RouteLine line = SplittedRouteGraph.findNearestLine(rg, splitCanvasPos);
if (DEBUG)
rg.print();
if (line == null)
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 = splitCanvasPos.getX();
double isectY = splitCanvasPos.getY();
SplittedRouteGraph srg;
if (line.isHorizontal()) {
isectY = line.getPosition();
srg = rg.splitGraph(line, isectX);
}
else {
isectX = line.getPosition();
srg = rg.splitGraph(line, isectY);
}
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();
......@@ -141,44 +143,106 @@ public class RouteGraphConnectionSplitter {
idMap.get(srg.splitLine)
));
}
ArrayList<Resource> interfaceNodes1Resources = new ArrayList<Resource>(srg.interfaceNodes1.size());
for(RouteNode n : srg.interfaceNodes1)
interfaceNodes1Resources.add(ss.getResource((Long)n.getData()));
ArrayList<Resource> interfaceNodes2Resources = new ArrayList<Resource>(srg.interfaceNodes2.size());
for(RouteNode n : srg.interfaceNodes2)
interfaceNodes2Resources.add(ss.getResource((Long)n.getData()));
ArrayList<Resource> lines2Resources = new ArrayList<Resource>(srg.lines2.size());
for(RouteLine n : srg.lines2)
lines2Resources.add(ss.getResource((Long)n.getData()));
ArrayList<Resource> terminals1Resources = new ArrayList<Resource>(srg.terminals1.size());
for(RouteTerminal n : srg.terminals1)
terminals1Resources.add(ss.getResource((Long)n.getData()));
ArrayList<Resource> terminals2Resources = new ArrayList<Resource>(srg.terminals2.size());
for(RouteTerminal n : srg.terminals2)
terminals2Resources.add(ss.getResource((Long)n.getData()));
ArrayList<Resource> terminals1Resources = toResources(srg.terminals1);
ArrayList<Resource> terminals2Resources = toResources(srg.terminals2);
boolean mustFlip = analyzePartInputs(graph, terminals1Resources, terminals2Resources);
ArrayList<Resource> interfaceNodes1 = toResources(mustFlip ? srg.interfaceNodes2 : srg.interfaceNodes1);
ArrayList<Resource> interfaceNodes2 = toResources(mustFlip ? srg.interfaceNodes1 : srg.interfaceNodes2);
ArrayList<Resource> lines2 = toResources(mustFlip ? srg.lines1 : srg.lines2);
ArrayList<Resource> terminals1 = mustFlip ? terminals2Resources : terminals1Resources;
ArrayList<Resource> terminals2 = mustFlip ? terminals1Resources : terminals2Resources;
doSplit(graph, connection,
interfaceNodes1Resources,
interfaceNodes2Resources,
lines2Resources,
terminals1Resources,
terminals2Resources,
interfaceNodes1,
interfaceNodes2,
lines2,
terminals1,
terminals2,
line.isHorizontal(),
mustFlip,
isectX, isectY);
modis.addModi(new RouteGraphModification.Split(
modis.toIds(interfaceNodes1Resources),
modis.toIds(interfaceNodes2Resources),
modis.toIds(lines2Resources),
modis.toIds(terminals1Resources),
modis.toIds(terminals2Resources),
modis.toIds(interfaceNodes1),
modis.toIds(interfaceNodes2),
modis.toIds(lines2),
modis.toIds(terminals1),
modis.toIds(terminals2),
line.isHorizontal(),
mustFlip,
isectX, isectY
));
}
private ArrayList<Resource> toResources(Collection<? extends RouteNode> nodes) throws DatabaseException {
ArrayList<Resource> 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 <code>true</code> 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<Resource> terminals1, List<Resource> 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<Resource> interfaceNodes1Resources,
......@@ -186,22 +250,28 @@ public class RouteGraphConnectionSplitter {
ArrayList<Resource> lines2Resources,
ArrayList<Resource> terminals1Resources,
ArrayList<Resource> terminals2Resources,
boolean isHorizontal,
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: " + NameUtils.getSafeName(graph, i, true));
System.out.println("i1: " + routeNodeDebugInfo(graph, i));
for (Resource i : interfaceNodes2Resources)
System.out.println("i2: " + NameUtils.getSafeName(graph, i, true));
System.out.println("i2: " + routeNodeDebugInfo(graph, i));
for (Resource l : lines2Resources)
System.out.println("l2r: " + NameUtils.getSafeName(graph, l, true));
System.out.println("l2r: " + routeNodeDebugInfo(graph, l));
for (Resource t : terminals1Resources)
System.out.println("t1: " + NameUtils.getSafeName(graph, t, true));
System.out.println("t1: " + routeNodeDebugInfo(graph, t));
for (Resource t : terminals2Resources)
System.out.println("t2: " + NameUtils.getSafeName(graph, t, true));
System.out.println("t2: " + routeNodeDebugInfo(graph, t));
System.out.println("is horizontal: " + isHorizontal);
System.out.println("@(x,y): " + isectX + ", " + isectY);
}
......@@ -209,15 +279,39 @@ public class RouteGraphConnectionSplitter {
ConnectionUtil cu = new ConnectionUtil(graph);
Resource diagram = OrderedSetUtils.getSingleOwnerList(graph, connection, DIA.Diagram);
Resource connectionType = graph.getSingleType(connection, DIA.Connection);
Resource diagramConnectionType = graph.getSingleType(connection, DIA.Connection);
Resource hasConnectionType = graph.getPossibleObject(connection, STR.HasConnectionType);
Resource newConnection = cu.newConnection(diagram, connectionType);
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.
......@@ -237,97 +331,31 @@ public class RouteGraphConnectionSplitter {
graph.claim(rn, predicate, newConnection);
}
// 1 = output, 2 = input
FlagClass.Type type1, type2;
FlagLabelingScheme scheme = DiagramFlagPreferences.getActiveFlagLabelingScheme(graph);
String commonLabel = scheme.generateLabel(graph, diagram);
// Create flags and connect both disconnected ends to them.
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);
}
// Chooses flag directions
{
@SuppressWarnings("unused")
int inputs1 = 0, outputs1 = 0;
for(Resource connector : terminals1Resources) {
if(graph.hasStatement(connector, DIA.IsHeadConnectorOf))
++inputs1;
else
++outputs1;
}
@SuppressWarnings("unused")
int inputs2 = 0, outputs2 = 0;
for(Resource connector : terminals2Resources) {
if(graph.hasStatement(connector, DIA.IsHeadConnectorOf))
++inputs2;
else
++outputs2;
}
if(outputs1 == 0) {
type1 = FlagClass.Type.In;
type2 = FlagClass.Type.Out;
theta += Math.PI;
}
else {
type1 = FlagClass.Type.Out;
type2 = FlagClass.Type.In;
}
if (DEBUG) {
System.out.println("inputs1: " + inputs1);
System.out.println("outputs1: " + outputs1);
System.out.println("=> type1: " + type1);
System.out.println("inputs2: " + inputs2);
System.out.println("outputs2: " + outputs2);
System.out.println("=> type2: " + type2);
}
}
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));
}
// System.out.println("conn1: " + NameUtils.getSafeLabel(graph, type1 == FlagClass.Type.In ? DIA.HasPlainConnector : DIA.HasArrowConnector));
// System.out.println("conn2: " + NameUtils.getSafeLabel(graph, type2 == FlagClass.Type.In ? DIA.HasPlainConnector : DIA.HasArrowConnector));
Resource flagConnector1 = cu.newConnector(connection,
type1 == FlagClass.Type.In ? DIA.HasPlainConnector : DIA.HasArrowConnector);
Resource flagConnector2 = cu.newConnector(newConnection,
type2 == FlagClass.Type.In ? DIA.HasPlainConnector : DIA.HasArrowConnector);
Resource flagConnector1 = cu.newConnector(connection, DIA.HasArrowConnector);