]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.diagram/src/org/simantics/diagram/elements/ElementTransforms.java
Added new diagram element transformation function rotateToNeighborSlope
[simantics/platform.git] / bundles / org.simantics.diagram / src / org / simantics / diagram / elements / ElementTransforms.java
index 916c274886e2b40a3474f32db1c4af838235fb4f..1e141c63643cdb108458a2555f8d94359582cdb9 100644 (file)
@@ -7,23 +7,31 @@ import java.util.ArrayList;
 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;
@@ -35,6 +43,11 @@ import org.simantics.g2d.element.impl.Element;
 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.
@@ -48,6 +61,8 @@ import org.simantics.scl.commands.Commands;
  */
 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 };
 
     /**
@@ -621,6 +636,7 @@ public final class ElementTransforms {
             return GeometryUtils.transformShape(bounds, at).getBounds2D();
         }
 
+        @SuppressWarnings("unused")
         public double getDeterminant() {
             return transform[0] * transform[3] - transform[1] * transform[2];
         }
@@ -712,4 +728,177 @@ public final class ElementTransforms {
         }
     }
 
+    // ------------------------------------------------------------------------
+
+    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);
+    }
+
 }