X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.diagram%2Fsrc%2Forg%2Fsimantics%2Fdiagram%2Felements%2FElementTransforms.java;h=1e141c63643cdb108458a2555f8d94359582cdb9;hb=HEAD;hp=a9cc2d40079cf49ea82b7010f99749bb595c6a78;hpb=969bd23cab98a79ca9101af33334000879fb60c5;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/elements/ElementTransforms.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/elements/ElementTransforms.java index a9cc2d400..1e141c636 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/elements/ElementTransforms.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/elements/ElementTransforms.java @@ -1,715 +1,904 @@ -package org.simantics.diagram.elements; - -import java.awt.geom.AffineTransform; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import org.simantics.db.ReadGraph; -import org.simantics.db.Resource; -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.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.query.DiagramRequests; -import org.simantics.diagram.stubs.DiagramResource; -import org.simantics.diagram.synchronization.graph.DiagramGraphUtil; -import org.simantics.g2d.diagram.DiagramClass; -import org.simantics.g2d.diagram.IDiagram; -import org.simantics.g2d.diagram.impl.Diagram; -import org.simantics.g2d.element.ElementClass; -import org.simantics.g2d.element.ElementUtils; -import org.simantics.g2d.element.IElement; -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.ui.SimanticsUI; - -/** - * Tools to align, rotate, and flip diagram elements. - * - * TODO : We need to add capability hints to elements to prevent rotating and mirroring elements that do not support that. - * Example: mirrored text does not make any sense. - * - * - * @author Marko Luukkainen (implementation) - * @author Tuukka Lehtonen (documentation) - */ -public final class ElementTransforms { - - public static enum SIDE { LEFT, RIGHT, TOP, BOTTOM, VERT, HORIZ, VERT_BTW, HORIZ_BTW }; - - /** - * Align the specified set of diagram element resources in line with each - * other calculated by the specified side. - * - *

- * Alignment requires at least two elements to do anything. - * - * @param resources diagram element resources to rotate - * @param side the side of each element to use for distancing. Does not support between aligments. - */ - public static void align(final Resource resources[], final SIDE side) { - if (resources.length < 2) - return; - if (side == SIDE.HORIZ_BTW || side == SIDE.VERT_BTW ) - return; - - SimanticsUI.getSession().asyncRequest(new WriteRequest() { - - @Override - public void perform(WriteGraph graph) throws DatabaseException { - graph.markUndoPoint(); - IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); - - List elements = new ArrayList(); - for (Resource r : resources) { - AlignElement e = create(graph, hints, r); - if (e != null) - elements.add(e); - } - if (elements.size() < 2) - return; - double mx = 0; - double my = 0; - for (AlignElement e : elements) { - mx += e.transform[4]; - my += e.transform[5]; - } - mx /= elements.size(); - my /= elements.size(); - - // prevent moving symbols into the same position - int count = 0; - for (AlignElement e : elements) { - if (side == SIDE.VERT || side == SIDE.LEFT || side == SIDE.RIGHT) { - if (Math.abs(e.transform[5] - my) < 0.1) { - count++; - } - } else { - if (Math.abs(e.transform[4] - mx) < 0.1) { - count++; - } - } - } - if (count > 1) - return; - - if (side == SIDE.HORIZ || side == SIDE.VERT) { - - - for (AlignElement e : elements) { - - if (side == SIDE.VERT) - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); - else if (side == SIDE.HORIZ) { - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); - } - } - } else { - double lx, rx; - double ty, by; - lx = elements.get(0).transform[4] + elements.get(0).rotatedBounds.getMinX(); - rx = elements.get(0).transform[4] + elements.get(0).rotatedBounds.getMaxX(); - - ty = elements.get(0).transform[5] + elements.get(0).rotatedBounds.getMinY(); - by = elements.get(0).transform[5] + elements.get(0).rotatedBounds.getMaxY(); - - for (int i = 1; i < elements.size(); i++) { - double tlx, trx; - double tty, tby; - tlx = elements.get(i).transform[4] + elements.get(i).rotatedBounds.getMinX(); - trx = elements.get(i).transform[4] + elements.get(i).rotatedBounds.getMaxX(); - - tty = elements.get(i).transform[5] + elements.get(i).rotatedBounds.getMinY(); - tby = elements.get(i).transform[5] + elements.get(i).rotatedBounds.getMaxY(); - if (tlx < lx) - lx = tlx; - if (trx > rx) - rx = trx; - if (tty < ty) - ty = tty; - if (tby > by) - by = tby; - - } - - for (AlignElement e : elements) { - mx = e.transform[4]; - my = e.transform[5]; - if (side == SIDE.LEFT) { - mx = lx - e.rotatedBounds.getMinX() ; - } else if (side == SIDE.RIGHT) { - mx = rx - e.rotatedBounds.getMaxX(); - } else if (side == SIDE.TOP) { - my = ty - e.rotatedBounds.getMinY(); - } else { - my = by - e.rotatedBounds.getMaxY(); - } - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,my}); - - } - - } - - } - }); - } - - /** - * Distance specified set of diagram element resources equally. Distancing - * is performed on the specified side of the each element. - * - *

- * Distancing requires at least three elements to work. - * - * @param resources diagram element resources to rotate - * @param side the side of each element to use for distancing - */ - public static void dist(final Resource resources[], final SIDE side) { - - if (resources.length < 3) - return; - - SimanticsUI.getSession().asyncRequest(new WriteRequest() { - - @Override - public void perform(WriteGraph graph) throws DatabaseException { - graph.markUndoPoint(); - IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); - - List elements = new ArrayList(); - for (Resource r : resources) { - //System.out.println(r + " " + GraphUtils.getReadableName(graph, r)); - AlignElement e = create(graph, hints, r); - if (e != null) - elements.add(e); - } - if (elements.size() < 3) - return; - switch (side) { - case LEFT: { - Collections.sort(elements, new XComparator()); - AlignElement left = elements.get(0); - AlignElement right = elements.get(elements.size() - 1); - - double leftEdge = left.transform[4] + left.rotatedBounds.getMinX(); - double rightEdge = right.transform[4] + right.rotatedBounds.getMinX(); - - double totalDist = rightEdge - leftEdge; - double dist = totalDist / (elements.size() - 1); - double d = leftEdge; - - for (int i = 1; i < elements.size() -1; i++) { - d += dist; - AlignElement e = elements.get(i); - - double mx = d - e.rotatedBounds.getMinX(); - - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); - } - - break; - } - case VERT: { - Collections.sort(elements, new XComparator()); - AlignElement left = elements.get(0); - AlignElement right = elements.get(elements.size() - 1); - - double leftEdge = left.transform[4]; - double rightEdge = right.transform[4]; - - double totalDist = rightEdge - leftEdge; - double dist = totalDist / (elements.size() - 1); - double d = leftEdge; - - for (int i = 1; i < elements.size() -1; i++) { - d += dist; - AlignElement e = elements.get(i); - - double mx = d; - - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); - } - - break; - } - case RIGHT:{ - Collections.sort(elements, new XComparator()); - AlignElement left = elements.get(0); - AlignElement right = elements.get(elements.size() - 1); - - double leftEdge = left.transform[4] + left.rotatedBounds.getMaxX(); - double rightEdge = right.transform[4] + right.rotatedBounds.getMaxX(); - - double totalDist = rightEdge - leftEdge; - double dist = totalDist / (elements.size() - 1); - double d = leftEdge; - - for (int i = 1; i < elements.size() -1; i++) { - d += dist; - AlignElement e = elements.get(i); - - double mx = d - e.rotatedBounds.getMaxX(); - - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); - } - break; - } - case VERT_BTW:{ - Collections.sort(elements, new XComparator()); - AlignElement left = elements.get(0); - AlignElement right = elements.get(elements.size() - 1); - - double leftEdge = left.transform[4] + left.rotatedBounds.getMaxX(); - double rightEdge = right.transform[4] + right.rotatedBounds.getMinX(); - - double totalDist = rightEdge - leftEdge; - double totalElementSize = 0; - for (int i = 1; i < elements.size() -1; i++) { - totalElementSize += elements.get(i).rotatedBounds.getWidth(); - } - double totalAvail = totalDist - totalElementSize; - double dist = totalAvail / (elements.size() - 1); - double d = leftEdge; - - for (int i = 1; i < elements.size() -1; i++) { - d += dist; - AlignElement e = elements.get(i); - - double mx = d - e.rotatedBounds.getMinX(); - d += e.bounds.getWidth(); - - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); - } - - break; - } - case BOTTOM: { - Collections.sort(elements, new YComparator()); - AlignElement top = elements.get(0); - AlignElement bottom = elements.get(elements.size() - 1); - - double topEdge = top.transform[5] + top.rotatedBounds.getMaxY(); - double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMaxY(); - - double totalDist = bottomEdge - topEdge; - double dist = totalDist / (elements.size() - 1); - double d = topEdge; - - for (int i = 1; i < elements.size() -1; i++) { - d += dist; - AlignElement e = elements.get(i); - - double my = d - e.rotatedBounds.getMaxY(); - - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); - } - - break; - } - case TOP: { - Collections.sort(elements, new YComparator()); - AlignElement top = elements.get(0); - AlignElement bottom = elements.get(elements.size() - 1); - - double topEdge = top.transform[5] + top.rotatedBounds.getMinY(); - double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMinY(); - - double totalDist = bottomEdge - topEdge; - double dist = totalDist / (elements.size() - 1); - double d = topEdge; - - for (int i = 1; i < elements.size() -1; i++) { - d += dist; - AlignElement e = elements.get(i); - - double my = d - e.rotatedBounds.getMinY(); - - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); - } - - break; - } - case HORIZ: { - Collections.sort(elements, new YComparator()); - AlignElement top = elements.get(0); - AlignElement bottom = elements.get(elements.size() - 1); - - double topEdge = top.transform[5]; - double bottomEdge = bottom.transform[5]; - - double totalDist = bottomEdge - topEdge; - double dist = totalDist / (elements.size() - 1); - double d = topEdge; - - for (int i = 1; i < elements.size() -1; i++) { - d += dist; - AlignElement e = elements.get(i); - - double my = d; - - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); - } - - break; - } - case HORIZ_BTW: { - Collections.sort(elements, new YComparator()); - AlignElement top = elements.get(0); - AlignElement bottom = elements.get(elements.size() - 1); - - double topEdge = top.transform[5] + top.rotatedBounds.getMaxY(); - double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMinY(); - - double totalDist = bottomEdge - topEdge; - double totalElementSize = 0; - for (int i = 1; i < elements.size() -1; i++) { - totalElementSize += elements.get(i).rotatedBounds.getHeight(); - } - double totalAvail = totalDist - totalElementSize; - double dist = totalAvail / (elements.size() - 1); - double d = topEdge; - - for (int i = 1; i < elements.size() -1; i++) { - d += dist; - AlignElement e = elements.get(i); - - double my = d - e.rotatedBounds.getMinY(); - d += e.rotatedBounds.getHeight(); - - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); - } - - break; - } - - } - - - } - }); - - } - - /** - * Rotate specified set of diagram element resources around the center of - * mass of the specified element selection. - * - * @param resources diagram element resources to rotate - * @param clockwise true to rotate 90 degrees clockwise, - * false to rotate 90 degrees counter-clockwise - */ - public static void rotate(final Resource resources[], final boolean clockwise) { - SimanticsUI.getSession().asyncRequest(new WriteRequest() { - @Override - public void perform(WriteGraph graph) throws DatabaseException { - graph.markUndoPoint(); - IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); - - DiagramResource DIA = DiagramResource.getInstance(graph); - - List elements = new ArrayList(); - List connections = new ArrayList(); - for (Resource r : resources) { - AlignElement e = create(graph, hints, r); - if (e != null) - elements.add(e); - else if(graph.isInstanceOf(r, DIA.RouteGraphConnection)) - connections.add(r); - } - if (elements.size() < 1) - return; - - // Add comment to change set. - CommentMetadata cm = graph.getMetadata(CommentMetadata.class); - graph.addMetadata( cm.add("Rotate " + elements.size() + " elements " + (clockwise ? "clockwise" : "counter-clockwise")) ); - - AffineTransform at = clockwise ? AffineTransform.getQuadrantRotateInstance(1) - : AffineTransform.getQuadrantRotateInstance(3); - - if (elements.size() == 1 && connections.isEmpty()) { - for (AlignElement e : elements) { - AffineTransform eat = new AffineTransform(e.transform[0], e.transform[1], e.transform[2], e.transform[3], 0, 0); - eat.preConcatenate(at); - - DiagramGraphUtil.changeTransform(graph, e.element, - new double[]{eat.getScaleX(),eat.getShearY(),eat.getShearX(),eat.getScaleY(),e.transform[4],e.transform[5]}); - } - } else { - Rectangle2D selectionBounds = null; - for (AlignElement e : elements) { - if (selectionBounds != null) { - selectionBounds.add(e.transform[4], e.transform[5]); - } else { - selectionBounds = new Rectangle2D.Double(e.transform[4], e.transform[5], 0, 0); - } - } - - double cx = selectionBounds.getCenterX(); - double cy = selectionBounds.getCenterY(); - - for (AlignElement e : elements) { - double x = e.transform[4]; - double y = e.transform[5]; - double dx = x - cx; - double dy = y - cy; - Point2D r = at.transform(new Point2D.Double(dx, dy), null); - double mx = r.getX() + cx; - double my = r.getY() + cy; - AffineTransform eat = new AffineTransform(e.transform[0], e.transform[1], e.transform[2], e.transform[3], 0, 0); - eat.preConcatenate(at); - - DiagramGraphUtil.changeTransform(graph, e.element, - new double[]{eat.getScaleX(),eat.getShearY(),eat.getShearX(),eat.getScaleY(),mx,my}); - } - - if(!connections.isEmpty()) { - Command rotateConnection = Commands.get(graph, "Simantics/Diagram/rotateConnection"); - Resource model = graph.syncRequest(new IndexRoot(connections.get(0))); - for(Resource r : connections) - rotateConnection.execute(graph, model, r, cx, cy, clockwise); - } - } - } - }); - } - - /** - * Flip specified set of diagram element resources around either the x or - * y-axis specified by the mass center of the selection bounding box. - * Each element is considered to weigh an equal amount. - * - * @param resources diagram element resources to flip - * @param xAxis true to flip around x-axis, false - * for y-axis - */ - public static void flip(final Resource resources[], final boolean xAxis) { - SimanticsUI.getSession().asyncRequest(new WriteRequest() { - @Override - public void perform(WriteGraph graph) throws DatabaseException { - graph.markUndoPoint(); - IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); - - DiagramResource DIA = DiagramResource.getInstance(graph); - - List elements = new ArrayList(); - List connections = new ArrayList(); - for (Resource r : resources) { - AlignElement e = create(graph, hints, r); - if (e != null) - elements.add(e); - else if(graph.isInstanceOf(r, DIA.RouteGraphConnection)) - connections.add(r); - } - if (elements.size() < 1) - return; - - // Add comment to change set. - CommentMetadata cm = graph.getMetadata(CommentMetadata.class); - graph.addMetadata( cm.add("Flip " + elements.size() + " elements " + (xAxis ? "vertically" : "horizontally")) ); - - if (elements.size() == 1 && connections.isEmpty()) { - for (AlignElement e : elements) { - if (xAxis) { - AffineTransform at = new AffineTransform(e.transform); - AffineTransform at2 = AffineTransform.getScaleInstance(1, -1); - at.preConcatenate(at2); - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],e.transform[5]}); - } else { - AffineTransform at = new AffineTransform(e.transform); - AffineTransform at2 = AffineTransform.getScaleInstance(-1, 1); - at.preConcatenate(at2); - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],e.transform[5]}); - } - } - } else { - - Rectangle2D selectionBounds = null; - for (AlignElement e : elements) { - if (selectionBounds != null) { - selectionBounds.add(e.transform[4], e.transform[5]); - } else { - selectionBounds = new Rectangle2D.Double(e.transform[4], e.transform[5], 0, 0); - } - } - - for (AlignElement e : elements) { - if (xAxis) { - double y = e.transform[5]; - double cy = selectionBounds.getCenterY(); - double my = cy + cy - y; - AffineTransform at = new AffineTransform(e.transform); - AffineTransform at2 = AffineTransform.getScaleInstance(1, -1); - at.preConcatenate(at2); - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],my}); - } else { - double x = e.transform[4]; - double cx = selectionBounds.getCenterX(); - double mx = cx + cx - x; - - AffineTransform at = new AffineTransform(e.transform); - AffineTransform at2 = AffineTransform.getScaleInstance(-1, 1); - at.preConcatenate(at2); - DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),mx,e.transform[5]}); - } - } - - if(!connections.isEmpty()) { - Command flipConnection = Commands.get(graph, "Simantics/Diagram/flipConnection"); - Resource model = graph.syncRequest(new IndexRoot(connections.get(0))); - for(Resource r : connections) - flipConnection.execute(graph, model, r, xAxis, - xAxis ? selectionBounds.getCenterY() - : selectionBounds.getCenterX()); - } - } - - } - }); - } - - private static AlignElement create(ReadGraph graph, IDiagram hints, Resource r) throws ManyObjectsForFunctionalRelationException, NoSingleResultException, ServiceException, DatabaseException { - DiagramResource dr = DiagramResource.getInstance(graph); - - if (graph.isInstanceOf(r, dr.Element) && !graph.isInstanceOf(r, dr.Connection) /*&& !graph.isInstanceOf(r, dr.Monitor)*/) { - double transform[] = graph.getPossibleRelatedValue(r, dr.HasTransform); - ElementClass ec = graph.syncRequest(DiagramRequests.getElementClass(graph.getSingleType(r, dr.Element), hints)); - IElement e = Element.spawnNew(ec); - Rectangle2D bounds = ElementUtils.getElementBounds(e); - if (transform != null && bounds != null) { - return new AlignElement(r, transform, bounds); - } - } - return null; - } - - - private static class AlignElement { - public Resource element; - public double[] transform; - public Rectangle2D bounds; - public Rectangle2D rotatedBounds; -// public Rectangle2D transformedBounds; - - public AlignElement(Resource element, double[] transform, Rectangle2D bounds) { - this.element = element; - this.transform = transform; - this.bounds = bounds; -// this.transformedBounds = getBounds(); - this.rotatedBounds = getRotatedBounds(); - } - -// public Rectangle2D getBounds() { -// AffineTransform at = new AffineTransform(transform[0], transform[1], transform[2], transform[3], transform[4], transform[5]); -// return GeometryUtils.transformShape(bounds, at).getBounds2D(); -// } - - public Rectangle2D getRotatedBounds() { - AffineTransform at = new AffineTransform(transform[0], transform[1], transform[2], transform[3], 0.0, 0.0); - return GeometryUtils.transformShape(bounds, at).getBounds2D(); - } - - public double getDeterminant() { - return transform[0] * transform[3] - transform[1] * transform[2]; - } - - } - - private static class XComparator implements Comparator { - @Override - public int compare(AlignElement o1, AlignElement o2) { - if (o1.transform[4] < o2.transform[4]) - return -1; - if (o1.transform[4] > o2.transform[4]) - return 1; - return 0; - - } - } - - private static class YComparator implements Comparator { - @Override - public int compare(AlignElement o1, AlignElement o2) { - if (o1.transform[5] < o2.transform[5]) - return -1; - if (o1.transform[5] > o2.transform[5]) - return 1; - return 0; - - } - } - - /** - * Set 2D affine transforms for the listed diagram element resources. - * - * @param elements diagram element resources to set transforms for - * @param transforms transforms for each element - */ - public static Write setTransformRequest(final Collection elements) - { - return new WriteRequest() { - @Override - public void perform(WriteGraph graph) throws DatabaseException { - for (TransformedObject element : elements) - DiagramGraphUtil.changeTransform(graph, element.element, element.transform); - } - }; - } - - /** - * Set 2D affine transforms for the listed diagram element resources. - * - * @param undoContext the database undo context to use for the returned - * request - * @param elements diagram element resources to set transforms for - * @param transforms transforms for each element - * @param preConcatenate true to pre-concatenate the - * transforms, false to concatenate - */ - public static Write concatenateTransformRequest( - UndoContext undoContext, - final Collection elements, - final boolean preConcatenate) - { - return new WriteRequest() { - @Override - public void perform(WriteGraph graph) throws DatabaseException { - for (TransformedObject element : elements) { - AffineTransform at = DiagramGraphUtil.getTransform(graph, element.element); - if (preConcatenate) - at.preConcatenate(element.transform); - else - at.concatenate(element.transform); - DiagramGraphUtil.setTransform(graph, element.element, at); - } - } - }; - } - - public static class TransformedObject { - public final Resource element; - public final AffineTransform transform; - - public TransformedObject(Resource element) { - this.element = element; - this.transform = new AffineTransform(); - } - public TransformedObject(Resource element, AffineTransform transform) { - this.element = element; - this.transform = transform; - } - } - -} +package org.simantics.diagram.elements; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +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; +import org.simantics.g2d.diagram.impl.Diagram; +import org.simantics.g2d.element.ElementClass; +import org.simantics.g2d.element.ElementUtils; +import org.simantics.g2d.element.IElement; +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. + * + * TODO : We need to add capability hints to elements to prevent rotating and mirroring elements that do not support that. + * Example: mirrored text does not make any sense. + * + * + * @author Marko Luukkainen (implementation) + * @author Tuukka Lehtonen (documentation) + */ +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 }; + + /** + * Align the specified set of diagram element resources in line with each + * other calculated by the specified side. + * + *

+ * Alignment requires at least two elements to do anything. + * + * @param resources diagram element resources to rotate + * @param side the side of each element to use for distancing. Does not support between aligments. + */ + public static void align(final Resource resources[], final SIDE side) { + if (resources.length < 2) + return; + if (side == SIDE.HORIZ_BTW || side == SIDE.VERT_BTW ) + return; + + Simantics.getSession().asyncRequest(new WriteRequest() { + + @Override + public void perform(WriteGraph graph) throws DatabaseException { + graph.markUndoPoint(); + IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); + + List elements = new ArrayList(); + for (Resource r : resources) { + AlignElement e = create(graph, hints, r); + if (e != null) + elements.add(e); + } + if (elements.size() < 2) + return; + double mx = 0; + double my = 0; + for (AlignElement e : elements) { + mx += e.transform[4]; + my += e.transform[5]; + } + mx /= elements.size(); + my /= elements.size(); + + // prevent moving symbols into the same position + int count = 0; + for (AlignElement e : elements) { + if (side == SIDE.VERT || side == SIDE.LEFT || side == SIDE.RIGHT) { + if (Math.abs(e.transform[5] - my) < 0.1) { + count++; + } + } else { + if (Math.abs(e.transform[4] - mx) < 0.1) { + count++; + } + } + } + if (count > 1) + return; + + if (side == SIDE.HORIZ || side == SIDE.VERT) { + + + for (AlignElement e : elements) { + + if (side == SIDE.VERT) + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); + else if (side == SIDE.HORIZ) { + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); + } + } + } else { + double lx, rx; + double ty, by; + lx = elements.get(0).transform[4] + elements.get(0).rotatedBounds.getMinX(); + rx = elements.get(0).transform[4] + elements.get(0).rotatedBounds.getMaxX(); + + ty = elements.get(0).transform[5] + elements.get(0).rotatedBounds.getMinY(); + by = elements.get(0).transform[5] + elements.get(0).rotatedBounds.getMaxY(); + + for (int i = 1; i < elements.size(); i++) { + double tlx, trx; + double tty, tby; + tlx = elements.get(i).transform[4] + elements.get(i).rotatedBounds.getMinX(); + trx = elements.get(i).transform[4] + elements.get(i).rotatedBounds.getMaxX(); + + tty = elements.get(i).transform[5] + elements.get(i).rotatedBounds.getMinY(); + tby = elements.get(i).transform[5] + elements.get(i).rotatedBounds.getMaxY(); + if (tlx < lx) + lx = tlx; + if (trx > rx) + rx = trx; + if (tty < ty) + ty = tty; + if (tby > by) + by = tby; + + } + + for (AlignElement e : elements) { + mx = e.transform[4]; + my = e.transform[5]; + if (side == SIDE.LEFT) { + mx = lx - e.rotatedBounds.getMinX() ; + } else if (side == SIDE.RIGHT) { + mx = rx - e.rotatedBounds.getMaxX(); + } else if (side == SIDE.TOP) { + my = ty - e.rotatedBounds.getMinY(); + } else { + my = by - e.rotatedBounds.getMaxY(); + } + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,my}); + + } + + } + + } + }); + } + + /** + * Distance specified set of diagram element resources equally. Distancing + * is performed on the specified side of the each element. + * + *

+ * Distancing requires at least three elements to work. + * + * @param resources diagram element resources to rotate + * @param side the side of each element to use for distancing + */ + public static void dist(final Resource resources[], final SIDE side) { + + if (resources.length < 3) + return; + + Simantics.getSession().asyncRequest(new WriteRequest() { + + @Override + public void perform(WriteGraph graph) throws DatabaseException { + graph.markUndoPoint(); + IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); + + List elements = new ArrayList(); + for (Resource r : resources) { + //System.out.println(r + " " + GraphUtils.getReadableName(graph, r)); + AlignElement e = create(graph, hints, r); + if (e != null) + elements.add(e); + } + if (elements.size() < 3) + return; + switch (side) { + case LEFT: { + Collections.sort(elements, new XComparator()); + AlignElement left = elements.get(0); + AlignElement right = elements.get(elements.size() - 1); + + double leftEdge = left.transform[4] + left.rotatedBounds.getMinX(); + double rightEdge = right.transform[4] + right.rotatedBounds.getMinX(); + + double totalDist = rightEdge - leftEdge; + double dist = totalDist / (elements.size() - 1); + double d = leftEdge; + + for (int i = 1; i < elements.size() -1; i++) { + d += dist; + AlignElement e = elements.get(i); + + double mx = d - e.rotatedBounds.getMinX(); + + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); + } + + break; + } + case VERT: { + Collections.sort(elements, new XComparator()); + AlignElement left = elements.get(0); + AlignElement right = elements.get(elements.size() - 1); + + double leftEdge = left.transform[4]; + double rightEdge = right.transform[4]; + + double totalDist = rightEdge - leftEdge; + double dist = totalDist / (elements.size() - 1); + double d = leftEdge; + + for (int i = 1; i < elements.size() -1; i++) { + d += dist; + AlignElement e = elements.get(i); + + double mx = d; + + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); + } + + break; + } + case RIGHT:{ + Collections.sort(elements, new XComparator()); + AlignElement left = elements.get(0); + AlignElement right = elements.get(elements.size() - 1); + + double leftEdge = left.transform[4] + left.rotatedBounds.getMaxX(); + double rightEdge = right.transform[4] + right.rotatedBounds.getMaxX(); + + double totalDist = rightEdge - leftEdge; + double dist = totalDist / (elements.size() - 1); + double d = leftEdge; + + for (int i = 1; i < elements.size() -1; i++) { + d += dist; + AlignElement e = elements.get(i); + + double mx = d - e.rotatedBounds.getMaxX(); + + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); + } + break; + } + case VERT_BTW:{ + Collections.sort(elements, new XComparator()); + AlignElement left = elements.get(0); + AlignElement right = elements.get(elements.size() - 1); + + double leftEdge = left.transform[4] + left.rotatedBounds.getMaxX(); + double rightEdge = right.transform[4] + right.rotatedBounds.getMinX(); + + double totalDist = rightEdge - leftEdge; + double totalElementSize = 0; + for (int i = 1; i < elements.size() -1; i++) { + totalElementSize += elements.get(i).rotatedBounds.getWidth(); + } + double totalAvail = totalDist - totalElementSize; + double dist = totalAvail / (elements.size() - 1); + double d = leftEdge; + + for (int i = 1; i < elements.size() -1; i++) { + d += dist; + AlignElement e = elements.get(i); + + double mx = d - e.rotatedBounds.getMinX(); + d += e.bounds.getWidth(); + + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); + } + + break; + } + case BOTTOM: { + Collections.sort(elements, new YComparator()); + AlignElement top = elements.get(0); + AlignElement bottom = elements.get(elements.size() - 1); + + double topEdge = top.transform[5] + top.rotatedBounds.getMaxY(); + double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMaxY(); + + double totalDist = bottomEdge - topEdge; + double dist = totalDist / (elements.size() - 1); + double d = topEdge; + + for (int i = 1; i < elements.size() -1; i++) { + d += dist; + AlignElement e = elements.get(i); + + double my = d - e.rotatedBounds.getMaxY(); + + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); + } + + break; + } + case TOP: { + Collections.sort(elements, new YComparator()); + AlignElement top = elements.get(0); + AlignElement bottom = elements.get(elements.size() - 1); + + double topEdge = top.transform[5] + top.rotatedBounds.getMinY(); + double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMinY(); + + double totalDist = bottomEdge - topEdge; + double dist = totalDist / (elements.size() - 1); + double d = topEdge; + + for (int i = 1; i < elements.size() -1; i++) { + d += dist; + AlignElement e = elements.get(i); + + double my = d - e.rotatedBounds.getMinY(); + + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); + } + + break; + } + case HORIZ: { + Collections.sort(elements, new YComparator()); + AlignElement top = elements.get(0); + AlignElement bottom = elements.get(elements.size() - 1); + + double topEdge = top.transform[5]; + double bottomEdge = bottom.transform[5]; + + double totalDist = bottomEdge - topEdge; + double dist = totalDist / (elements.size() - 1); + double d = topEdge; + + for (int i = 1; i < elements.size() -1; i++) { + d += dist; + AlignElement e = elements.get(i); + + double my = d; + + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); + } + + break; + } + case HORIZ_BTW: { + Collections.sort(elements, new YComparator()); + AlignElement top = elements.get(0); + AlignElement bottom = elements.get(elements.size() - 1); + + double topEdge = top.transform[5] + top.rotatedBounds.getMaxY(); + double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMinY(); + + double totalDist = bottomEdge - topEdge; + double totalElementSize = 0; + for (int i = 1; i < elements.size() -1; i++) { + totalElementSize += elements.get(i).rotatedBounds.getHeight(); + } + double totalAvail = totalDist - totalElementSize; + double dist = totalAvail / (elements.size() - 1); + double d = topEdge; + + for (int i = 1; i < elements.size() -1; i++) { + d += dist; + AlignElement e = elements.get(i); + + double my = d - e.rotatedBounds.getMinY(); + d += e.rotatedBounds.getHeight(); + + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); + } + + break; + } + + } + + + } + }); + + } + + /** + * Rotate specified set of diagram element resources around the center of + * mass of the specified element selection. + * + * @param resources diagram element resources to rotate + * @param clockwise true to rotate 90 degrees clockwise, + * false to rotate 90 degrees counter-clockwise + */ + public static void rotate(final Resource resources[], final boolean clockwise) { + Simantics.getSession().asyncRequest(new WriteRequest() { + @Override + public void perform(WriteGraph graph) throws DatabaseException { + graph.markUndoPoint(); + IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); + + DiagramResource DIA = DiagramResource.getInstance(graph); + + List elements = new ArrayList(); + List connections = new ArrayList(); + for (Resource r : resources) { + AlignElement e = create(graph, hints, r); + if (e != null) + elements.add(e); + else if(graph.isInstanceOf(r, DIA.RouteGraphConnection)) + connections.add(r); + } + if (elements.size() < 1) + return; + + // Add comment to change set. + CommentMetadata cm = graph.getMetadata(CommentMetadata.class); + graph.addMetadata( cm.add("Rotate " + elements.size() + " elements " + (clockwise ? "clockwise" : "counter-clockwise")) ); + + AffineTransform at = clockwise ? AffineTransform.getQuadrantRotateInstance(1) + : AffineTransform.getQuadrantRotateInstance(3); + + if (elements.size() == 1 && connections.isEmpty()) { + for (AlignElement e : elements) { + AffineTransform eat = new AffineTransform(e.transform[0], e.transform[1], e.transform[2], e.transform[3], 0, 0); + eat.preConcatenate(at); + + DiagramGraphUtil.changeTransform(graph, e.element, + new double[]{eat.getScaleX(),eat.getShearY(),eat.getShearX(),eat.getScaleY(),e.transform[4],e.transform[5]}); + } + } else { + Rectangle2D selectionBounds = null; + for (AlignElement e : elements) { + if (selectionBounds != null) { + selectionBounds.add(e.transform[4], e.transform[5]); + } else { + selectionBounds = new Rectangle2D.Double(e.transform[4], e.transform[5], 0, 0); + } + } + + double cx = selectionBounds.getCenterX(); + double cy = selectionBounds.getCenterY(); + + for (AlignElement e : elements) { + double x = e.transform[4]; + double y = e.transform[5]; + double dx = x - cx; + double dy = y - cy; + Point2D r = at.transform(new Point2D.Double(dx, dy), null); + double mx = r.getX() + cx; + double my = r.getY() + cy; + AffineTransform eat = new AffineTransform(e.transform[0], e.transform[1], e.transform[2], e.transform[3], 0, 0); + eat.preConcatenate(at); + + DiagramGraphUtil.changeTransform(graph, e.element, + new double[]{eat.getScaleX(),eat.getShearY(),eat.getShearX(),eat.getScaleY(),mx,my}); + } + + if(!connections.isEmpty()) { + Command rotateConnection = Commands.get(graph, "Simantics/Diagram/rotateConnection"); + Resource model = graph.syncRequest(new IndexRoot(connections.get(0))); + for(Resource r : connections) + rotateConnection.execute(graph, model, r, cx, cy, clockwise); + } + } + } + }); + } + + /** + * Flip specified set of diagram element resources around either the x or + * y-axis specified by the mass center of the selection bounding box. + * Each element is considered to weigh an equal amount. + * + * @param resources diagram element resources to flip + * @param xAxis true to flip around x-axis, false + * for y-axis + */ + public static void flip(final Resource resources[], final boolean xAxis) { + Simantics.getSession().asyncRequest(new WriteRequest() { + @Override + public void perform(WriteGraph graph) throws DatabaseException { + graph.markUndoPoint(); + IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); + + DiagramResource DIA = DiagramResource.getInstance(graph); + + List elements = new ArrayList(); + List connections = new ArrayList(); + for (Resource r : resources) { + AlignElement e = create(graph, hints, r); + if (e != null) + elements.add(e); + else if(graph.isInstanceOf(r, DIA.RouteGraphConnection)) + connections.add(r); + } + if (elements.size() < 1) + return; + + // Add comment to change set. + CommentMetadata cm = graph.getMetadata(CommentMetadata.class); + graph.addMetadata( cm.add("Flip " + elements.size() + " elements " + (xAxis ? "vertically" : "horizontally")) ); + + if (elements.size() == 1 && connections.isEmpty()) { + for (AlignElement e : elements) { + if (xAxis) { + AffineTransform at = new AffineTransform(e.transform); + AffineTransform at2 = AffineTransform.getScaleInstance(1, -1); + at.preConcatenate(at2); + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],e.transform[5]}); + } else { + AffineTransform at = new AffineTransform(e.transform); + AffineTransform at2 = AffineTransform.getScaleInstance(-1, 1); + at.preConcatenate(at2); + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],e.transform[5]}); + } + } + } else { + + Rectangle2D selectionBounds = null; + for (AlignElement e : elements) { + if (selectionBounds != null) { + selectionBounds.add(e.transform[4], e.transform[5]); + } else { + selectionBounds = new Rectangle2D.Double(e.transform[4], e.transform[5], 0, 0); + } + } + + for (AlignElement e : elements) { + if (xAxis) { + double y = e.transform[5]; + double cy = selectionBounds.getCenterY(); + double my = cy + cy - y; + AffineTransform at = new AffineTransform(e.transform); + AffineTransform at2 = AffineTransform.getScaleInstance(1, -1); + at.preConcatenate(at2); + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],my}); + } else { + double x = e.transform[4]; + double cx = selectionBounds.getCenterX(); + double mx = cx + cx - x; + + AffineTransform at = new AffineTransform(e.transform); + AffineTransform at2 = AffineTransform.getScaleInstance(-1, 1); + at.preConcatenate(at2); + DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),mx,e.transform[5]}); + } + } + + if(!connections.isEmpty()) { + Command flipConnection = Commands.get(graph, "Simantics/Diagram/flipConnection"); + Resource model = graph.syncRequest(new IndexRoot(connections.get(0))); + for(Resource r : connections) + flipConnection.execute(graph, model, r, xAxis, + xAxis ? selectionBounds.getCenterY() + : selectionBounds.getCenterX()); + } + } + + } + }); + } + + private static AlignElement create(ReadGraph graph, IDiagram hints, Resource r) throws ManyObjectsForFunctionalRelationException, NoSingleResultException, ServiceException, DatabaseException { + DiagramResource dr = DiagramResource.getInstance(graph); + + if (graph.isInstanceOf(r, dr.Element) && !graph.isInstanceOf(r, dr.Connection) /*&& !graph.isInstanceOf(r, dr.Monitor)*/) { + double transform[] = graph.getPossibleRelatedValue(r, dr.HasTransform); + ElementClass ec = graph.syncRequest(DiagramRequests.getElementClass(graph.getSingleType(r, dr.Element), hints)); + IElement e = Element.spawnNew(ec); + Rectangle2D bounds = ElementUtils.getElementBounds(e); + if (transform != null && bounds != null) { + return new AlignElement(r, transform, bounds); + } + } + return null; + } + + + private static class AlignElement { + public Resource element; + public double[] transform; + public Rectangle2D bounds; + public Rectangle2D rotatedBounds; +// public Rectangle2D transformedBounds; + + public AlignElement(Resource element, double[] transform, Rectangle2D bounds) { + this.element = element; + this.transform = transform; + this.bounds = bounds; +// this.transformedBounds = getBounds(); + this.rotatedBounds = getRotatedBounds(); + } + +// public Rectangle2D getBounds() { +// AffineTransform at = new AffineTransform(transform[0], transform[1], transform[2], transform[3], transform[4], transform[5]); +// return GeometryUtils.transformShape(bounds, at).getBounds2D(); +// } + + public Rectangle2D getRotatedBounds() { + AffineTransform at = new AffineTransform(transform[0], transform[1], transform[2], transform[3], 0.0, 0.0); + return GeometryUtils.transformShape(bounds, at).getBounds2D(); + } + + @SuppressWarnings("unused") + public double getDeterminant() { + return transform[0] * transform[3] - transform[1] * transform[2]; + } + + } + + private static class XComparator implements Comparator { + @Override + public int compare(AlignElement o1, AlignElement o2) { + if (o1.transform[4] < o2.transform[4]) + return -1; + if (o1.transform[4] > o2.transform[4]) + return 1; + return 0; + + } + } + + private static class YComparator implements Comparator { + @Override + public int compare(AlignElement o1, AlignElement o2) { + if (o1.transform[5] < o2.transform[5]) + return -1; + if (o1.transform[5] > o2.transform[5]) + return 1; + return 0; + + } + } + + /** + * Set 2D affine transforms for the listed diagram element resources. + * + * @param elements diagram element resources to set transforms for + * @param transforms transforms for each element + */ + public static Write setTransformRequest(final Collection elements) + { + return new WriteRequest() { + @Override + public void perform(WriteGraph graph) throws DatabaseException { + for (TransformedObject element : elements) + DiagramGraphUtil.changeTransform(graph, element.element, element.transform); + } + }; + } + + /** + * Set 2D affine transforms for the listed diagram element resources. + * + * @param undoContext the database undo context to use for the returned + * request + * @param elements diagram element resources to set transforms for + * @param transforms transforms for each element + * @param preConcatenate true to pre-concatenate the + * transforms, false to concatenate + */ + public static Write concatenateTransformRequest( + UndoContext undoContext, + final Collection elements, + final boolean preConcatenate) + { + return new WriteRequest() { + @Override + public void perform(WriteGraph graph) throws DatabaseException { + for (TransformedObject element : elements) { + AffineTransform at = DiagramGraphUtil.getTransform(graph, element.element); + if (preConcatenate) + at.preConcatenate(element.transform); + else + at.concatenate(element.transform); + DiagramGraphUtil.setTransform(graph, element.element, at); + } + } + }; + } + + public static class TransformedObject { + public final Resource element; + public final AffineTransform transform; + + public TransformedObject(Resource element) { + this.element = element; + this.transform = new AffineTransform(); + } + public TransformedObject(Resource element, AffineTransform transform) { + this.element = element; + this.transform = transform; + } + } + + // ------------------------------------------------------------------------ + + 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 connectionPoints(); + Set 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 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 connectionPoints() { + return elementTerminals.getConnectionPoints(); + } + @Override + public Set terminals() { + return elementTerminals.getTerminals(); + } + }; + } + + private static Set connectedTo(ReadGraph graph, Resource element, Resource connectionPoint) throws DatabaseException { + BasicResources BR = BasicResources.getInstance(graph); + Set 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. + * + *

+ * 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 connectedComponents = new HashSet<>(); + Map componentTerminals = new HashMap<>(); + Map connectedTerminals = new HashMap<>(); + + for (Resource connectionPoint : elementTerminals.connectionPoints()) { + Set 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 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); + } + +}