import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import org.simantics.Simantics;
import org.simantics.db.ReadGraph;
import org.simantics.db.Resource;
+import org.simantics.db.Statement;
import org.simantics.db.UndoContext;
import org.simantics.db.WriteGraph;
import org.simantics.db.common.CommentMetadata;
import org.simantics.db.common.request.IndexRoot;
import org.simantics.db.common.request.WriteRequest;
+import org.simantics.db.common.utils.NameUtils;
import org.simantics.db.exception.DatabaseException;
import org.simantics.db.exception.ManyObjectsForFunctionalRelationException;
import org.simantics.db.exception.NoSingleResultException;
import org.simantics.db.exception.ServiceException;
import org.simantics.db.request.Write;
+import org.simantics.diagram.content.TerminalMap;
import org.simantics.diagram.query.DiagramRequests;
import org.simantics.diagram.stubs.DiagramResource;
+import org.simantics.diagram.synchronization.graph.BasicResources;
import org.simantics.diagram.synchronization.graph.DiagramGraphUtil;
import org.simantics.g2d.diagram.DiagramClass;
import org.simantics.g2d.diagram.IDiagram;
import org.simantics.g2d.utils.GeometryUtils;
import org.simantics.scl.commands.Command;
import org.simantics.scl.commands.Commands;
+import org.simantics.structural2.queries.Terminal;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import gnu.trove.set.hash.THashSet;
/**
* Tools to align, rotate, and flip diagram elements.
*/
public final class ElementTransforms {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ElementTransforms.class);
+
public static enum SIDE { LEFT, RIGHT, TOP, BOTTOM, VERT, HORIZ, VERT_BTW, HORIZ_BTW };
/**
return GeometryUtils.transformShape(bounds, at).getBounds2D();
}
+ @SuppressWarnings("unused")
public double getDeterminant() {
return transform[0] * transform[3] - transform[1] * transform[2];
}
}
}
+ // ------------------------------------------------------------------------
+
+ private interface TerminalFunction {
+ /**
+ * @param terminal
+ * @param rotationTheta in radians
+ * @return
+ */
+ AffineTransform transform(Resource connectionPoint, double rotationTheta);
+ default Point2D position(Resource connectionPoint, double rotationTheta) {
+ AffineTransform tr = transform(connectionPoint, rotationTheta);
+ return new Point2D.Double(tr.getTranslateX(), tr.getTranslateY());
+ }
+ Resource toConnectionPoint(Resource terminal);
+ Resource toTerminal(Resource connectionPoint);
+ Set<Resource> connectionPoints();
+ Set<Resource> terminals();
+ }
+
+ private static TerminalFunction terminalPositionFunction(ReadGraph graph, boolean ignoreElementRotation, Resource element) throws DatabaseException {
+ AffineTransform transform = DiagramGraphUtil.getAffineTransform(graph, element);
+ if (ignoreElementRotation) {
+ AffineTransform noRotation = AffineTransform.getTranslateInstance(transform.getTranslateX(), transform.getTranslateY());
+ Point2D scale = org.simantics.scenegraph.utils.GeometryUtils.getScale2D(transform);
+ noRotation.scale(scale.getX(), scale.getY());
+ transform = noRotation;
+ }
+ AffineTransform elementTransform = transform;
+
+ TerminalMap elementTerminals = DiagramGraphUtil.getElementTerminals(graph, element);
+ Map<Resource, AffineTransform> terminalTransforms = new HashMap<>();
+ for (Resource terminal : elementTerminals.getTerminals())
+ terminalTransforms.put(elementTerminals.getConnectionPoint(terminal), DiagramGraphUtil.getAffineTransform(graph, terminal));
+
+ return new TerminalFunction() {
+ @Override
+ public AffineTransform transform(Resource connectionPoint, double rotationTheta) {
+ AffineTransform tr = new AffineTransform(elementTransform);
+ AffineTransform terminalTr = terminalTransforms.get(connectionPoint);
+ if (rotationTheta != 0)
+ tr.rotate(rotationTheta);
+ if (terminalTr != null)
+ tr.concatenate(terminalTr);
+ return tr;
+ }
+ @Override
+ public Resource toConnectionPoint(Resource terminal) {
+ return elementTerminals.getConnectionPoint(terminal);
+ }
+ @Override
+ public Resource toTerminal(Resource connectionPoint) {
+ return elementTerminals.getTerminal(connectionPoint);
+ }
+ @Override
+ public Set<Resource> connectionPoints() {
+ return elementTerminals.getConnectionPoints();
+ }
+ @Override
+ public Set<Resource> terminals() {
+ return elementTerminals.getTerminals();
+ }
+ };
+ }
+
+ private static Set<Terminal> connectedTo(ReadGraph graph, Resource element, Resource connectionPoint) throws DatabaseException {
+ BasicResources BR = BasicResources.getInstance(graph);
+ Set<Terminal> result = new THashSet<>();
+ for (Resource connector : graph.getObjects(element, connectionPoint)) {
+ Resource connection = graph.getPossibleObject(connector, BR.DIA.IsConnectorOf);
+ if (connection == null)
+ continue;
+ for (Resource otherConnector : graph.getObjects(connection, BR.DIA.HasConnector)) {
+ if (!otherConnector.equals(connector)) {
+ for (Statement s : graph.getStatements(otherConnector, BR.STR.Connects)) {
+ if (!s.getObject().equals(connection)) {
+ result.add( new Terminal(s.getObject(), graph.getInverse(s.getPredicate())) );
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Rotates provided element so that its rotation is the same as the slope of a
+ * line drawn between the two other elements the element is connected to.
+ *
+ * <p>
+ * Note that this requires that the element is connected to exactly two other
+ * elements.
+ *
+ * @param graph
+ * @param element
+ * @return the chosen rotation in degrees
+ * @throws DatabaseException
+ */
+ public static double rotateToNeighborSlope(WriteGraph graph, Resource element) throws DatabaseException {
+
+ if (LOGGER.isDebugEnabled())
+ LOGGER.debug("rotateAccordingToNeighborPositions( {} )", graph.getPossibleURI(element));
+
+ TerminalFunction elementTerminals = terminalPositionFunction(graph, true, element);
+ Set<Resource> connectedComponents = new HashSet<>();
+ Map<Resource, TerminalFunction> componentTerminals = new HashMap<>();
+ Map<Resource, Terminal> connectedTerminals = new HashMap<>();
+
+ for (Resource connectionPoint : elementTerminals.connectionPoints()) {
+ Set<Terminal> connectedTo = connectedTo(graph, element, connectionPoint);
+ if (connectedTo.isEmpty())
+ continue;
+ if (connectedTo.size() > 1) {
+ if (LOGGER.isWarnEnabled())
+ LOGGER.warn("rotateToNeighbors only supports functional connection points, ignoring {}", NameUtils.getURIOrSafeNameInternal(graph, connectionPoint));
+ continue;
+ }
+
+ Terminal t = connectedTo.iterator().next();
+ TerminalFunction tf = terminalPositionFunction(graph, false, t.getComponent());
+ connectedComponents.add(t.getComponent());
+ componentTerminals.put(t.getComponent(), tf);
+ connectedTerminals.put(connectionPoint, t);
+
+ if (LOGGER.isDebugEnabled())
+ LOGGER.info("{} {} is connected to {}", NameUtils.getSafeName(graph, element), NameUtils.getSafeName(graph, connectionPoint), t.toString(graph));
+ }
+
+ if (connectedComponents.size() != 2) {
+ LOGGER.error("rotateToNeighborPositions only works for elements connected to exactly two other elements, {} is connected to {}", NameUtils.getURIOrSafeNameInternal(graph, element), connectedComponents.size());
+ return Double.NaN;
+ }
+
+ Resource[] ends = connectedComponents.toArray(Resource.NONE);
+ AffineTransform[] endTr = {
+ componentTerminals.get(ends[0]).transform(null, 0),
+ componentTerminals.get(ends[1]).transform(null, 0)
+ };
+
+ double slopeTheta = Math.atan2(endTr[1].getTranslateY() - endTr[0].getTranslateY(), endTr[1].getTranslateX() - endTr[0].getTranslateX());
+ double[] thetas = { slopeTheta, slopeTheta + Math.PI };
+ double selectedRotation = slopeTheta;
+ double minCost = Double.MAX_VALUE;
+ for (int i = 0; i < 2; ++i) {
+ double cost = 0;
+ for (Map.Entry<Resource, Terminal> e : connectedTerminals.entrySet()) {
+ Terminal t = e.getValue();
+ TerminalFunction tf = componentTerminals.get(t.getComponent());
+ Point2D otp = tf.position(t.getRelation(), 0);
+ Point2D etp = elementTerminals.position(e.getKey(), thetas[i]);
+ cost += otp.distance(etp);
+ }
+
+ if (LOGGER.isDebugEnabled())
+ LOGGER.info("total cost of theta {} is {}", Math.toDegrees(thetas[i]), cost);
+
+ if (cost < minCost) {
+ minCost = cost;
+ selectedRotation = thetas[i];
+ if (LOGGER.isDebugEnabled())
+ LOGGER.debug("new minimum cost {} found", cost);
+ }
+ }
+
+ AffineTransform newTr = elementTerminals.transform(null, 0);
+ newTr.rotate(selectedRotation);
+ DiagramGraphUtil.setTransform(graph, element, newTr);
+
+ if (LOGGER.isDebugEnabled())
+ LOGGER.debug("set rotation to {} degrees", Math.toDegrees(selectedRotation));
+
+ return Math.toDegrees(selectedRotation);
+ }
+
}