/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.g2d.diagram; import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import org.simantics.g2d.canvas.Hints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.connection.ConnectionEntity; import org.simantics.g2d.connection.EndKeyOf; import org.simantics.g2d.connection.TerminalKeyOf; import org.simantics.g2d.connection.handler.ConnectionHandler; import org.simantics.g2d.diagram.handler.PickContext; import org.simantics.g2d.diagram.handler.PickRequest; import org.simantics.g2d.diagram.handler.Topology; import org.simantics.g2d.diagram.handler.Topology.Connection; import org.simantics.g2d.diagram.handler.TransactionContext; import org.simantics.g2d.diagram.handler.TransactionContext.Transaction; import org.simantics.g2d.diagram.handler.TransactionContext.TransactionType; import org.simantics.g2d.diagram.impl.Diagram; import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil; import org.simantics.g2d.element.ElementHints; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.handler.BendsHandler; import org.simantics.g2d.element.handler.BendsHandler.Bend; import org.simantics.g2d.element.handler.Children; import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd; import org.simantics.g2d.element.handler.InternalSize; import org.simantics.g2d.element.handler.Transform; import org.simantics.g2d.element.impl.Element; import org.simantics.g2d.elementclass.BranchPoint; import org.simantics.g2d.elementclass.BranchPoint.Direction; import org.simantics.g2d.routing.ConnectionDirectionUtil; import org.simantics.g2d.routing.Constants; import org.simantics.g2d.routing.IConnection; import org.simantics.g2d.routing.IRouter2; import org.simantics.g2d.routing.TrivialRouter2; import org.simantics.scenegraph.utils.GeometryUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import gnu.trove.map.hash.THashMap; /** * @author Toni Kalajainen * @author Antti Villberg * @author Tuukka Lehtonen */ public class DiagramUtils { private static final Logger LOGGER = LoggerFactory.getLogger(DiagramUtils.class); /** * Get rectangle that contains all elements or null if there are no elements. * @param d * @return rectangle or null */ public static Rectangle2D getContentRect(IDiagram d) { return getContentRect(d.getElements()); } /** * Get rectangle that contains all elements or null if there are no elements. * @param d * @return rectangle or null */ public static Rectangle2D getContentRect(Collection elements) { Rectangle2D diagramRect = null; Rectangle2D elementRect = new Rectangle2D.Double(); for (IElement el : elements) { if (ElementUtils.isHidden(el)) continue; InternalSize size = el.getElementClass().getSingleItem(InternalSize.class); elementRect.setRect(Double.NaN, Double.NaN, Double.NaN, Double.NaN); size.getBounds(el, elementRect); if (!Double.isFinite(elementRect.getWidth()) || !Double.isFinite(elementRect.getHeight()) || !Double.isFinite(elementRect.getX()) || !Double.isFinite(elementRect.getY())) continue; Transform t = el.getElementClass().getSingleItem(Transform.class); AffineTransform at = t.getTransform(el); Rectangle2D transformedRect = GeometryUtils.transformRectangle(at, elementRect); if (diagramRect==null) diagramRect = new Rectangle2D.Double( transformedRect.getX(), transformedRect.getY(), transformedRect.getWidth(), transformedRect.getHeight() ); else diagramRect.add(transformedRect); } return diagramRect; } public static void pick( IDiagram d, PickRequest request, Collection result) { PickContext pc = d.getDiagramClass().getSingleItem(PickContext.class); pc.pick(d, request, result); } public static void invalidate(IDiagram d) { //Task task = ThreadLog.BEGIN("DiagramUtils.invalidate"); d.setHint(Hints.KEY_DIRTY, Hints.VALUE_SG_DIRTY); //task.end(); } private static final ThreadLocal> elements = new ThreadLocal>() { @Override protected java.util.List initialValue() { return new ArrayList(); } }; /** * @param d * @param context */ public static void validateAndFix(final IDiagram d, ICanvasContext context) { //Task task = ThreadLog.BEGIN("DU.validateAndFix"); validateAndFix(d, d.getElements()); //task.end(); } /** * @param d * @param elementsToFix */ public static void validateAndFix(final IDiagram d, Collection elementsToFix) { //Task task = ThreadLog.BEGIN("DU.validateAndFix(IDiagram, Set)"); IRouter2 defaultRouter = ElementUtils.getHintOrDefault(d, DiagramHints.ROUTE_ALGORITHM, TrivialRouter2.INSTANCE); final Topology topology = d.getDiagramClass().getSingleItem(Topology.class); // Validate-and-fix is single-threaded. List segments = elements.get(); final Collection unmodifiableSegments = Collections.unmodifiableList(segments); for (final IElement element : elementsToFix) { if (!d.containsElement(element)) { LOGGER.warn("Fixing element not contained by diagram " + d + ": " + element); continue; } ConnectionHandler ch = element.getElementClass().getAtMostOneItemOfClass(ConnectionHandler.class); if (ch == null) continue; segments.clear(); ch.getSegments(element, segments); if (segments.isEmpty()) continue; // Get connection-specific router or use diagram default. IRouter2 router = ElementUtils.getHintOrDefault(element, DiagramHints.ROUTE_ALGORITHM, defaultRouter); for (final IElement e : unmodifiableSegments) { if (e.getElementClass().containsClass(BendsHandler.class)) { router.route(new IConnection() { THashMap branchPoints = new THashMap(); @Override public Connector getBegin(Object seg) { IElement e = (IElement)seg; Connection begin = topology.getConnection(e, EdgeEnd.Begin); Connector connector = begin == null ? null : branchPoints.get(begin.node); if(connector != null) return connector; connector = new Connector(); if(begin==null) { BendsHandler bends = e.getElementClass().getAtMostOneItemOfClass(BendsHandler.class); List bs = new ArrayList(); bends.getBends(e, bs); Point2D p = new Point2D.Double(); if(bs.size() > 0) bends.getBendPosition(e, bs.get(0), p); else p.setLocation(0.0, 0.0); AffineTransform elementTransform = ElementUtils.getTransform(e); elementTransform.transform(p, p); connector.x = p.getX(); connector.y = p.getY(); connector.allowedDirections = 0xf; } else { AffineTransform at = TerminalUtil.getTerminalPosOnDiagram(begin.node, begin.terminal); connector.x = at.getTranslateX(); connector.y = at.getTranslateY(); connector.parentObstacle = getObstacleShape(begin.node); BranchPoint bph = begin.node.getElementClass().getAtMostOneItemOfClass(BranchPoint.class); if(bph != null) { branchPoints.put(begin.node, connector); connector.allowedDirections = toAllowedDirections( bph.getDirectionPreference(begin.node, Direction.Any) ); } else ConnectionDirectionUtil.determineAllowedDirections(connector); } return connector; } private int toAllowedDirections(BranchPoint.Direction direction) { switch (direction) { case Any: return 0xf; case Horizontal: return Constants.EAST_FLAG | Constants.WEST_FLAG; case Vertical: return Constants.NORTH_FLAG | Constants.SOUTH_FLAG; default: throw new IllegalArgumentException("unrecognized direction: " + direction); } } @Override public Connector getEnd(Object seg) { IElement e = (IElement)seg; Connection end = topology.getConnection(e, EdgeEnd.End); Connector connector = end == null ? null : branchPoints.get(end.node); if(connector != null) return connector; connector = new Connector(); if(end==null) { BendsHandler bends = e.getElementClass().getAtMostOneItemOfClass(BendsHandler.class); List bs = new ArrayList(); bends.getBends(e, bs); Point2D p = new Point2D.Double(); if(bs.size() > 0) bends.getBendPosition(e, bs.get(bs.size()-1), p); else p.setLocation(0.0, 0.0); AffineTransform elementTransform = ElementUtils.getTransform(e); elementTransform.transform(p, p); connector.x = p.getX(); connector.y = p.getY(); connector.allowedDirections = 0xf; } else { AffineTransform at = TerminalUtil.getTerminalPosOnDiagram(end.node, end.terminal); connector.x = at.getTranslateX(); connector.y = at.getTranslateY(); connector.parentObstacle = getObstacleShape(end.node); BranchPoint bph = end.node.getElementClass().getAtMostOneItemOfClass(BranchPoint.class); if(bph != null) { branchPoints.put(end.node, connector); connector.allowedDirections = toAllowedDirections( bph.getDirectionPreference(end.node, Direction.Any) ); } else ConnectionDirectionUtil.determineAllowedDirections(connector); } return connector; } @Override public Collection getSegments() { return unmodifiableSegments; } @Override public void setPath(Object seg, Path2D path) { IElement e = (IElement)seg; BendsHandler bends = e.getElementClass().getAtMostOneItemOfClass(BendsHandler.class); AffineTransform elementTransform = ElementUtils.getInvTransform(e); path = (Path2D)path.clone(); path.transform(elementTransform); bends.setPath(e, path); } }); //task2.end(); } } } // Don't leave dangling references behind. segments.clear(); //task.end(); } /** * Execute the specified {@link Runnable} with in a diagram transaction * using the {@link TransactionContext} handler available in the * {@link DiagramClass} of the specified {@link Diagram}. * * @param diagram the diagram to execute the transaction for * @param type read or write (exclusive) * @param r the runnable to execute * * @throws IllegalArgumentException if the specified diagram does not have a * {@link TransactionContext} handler */ public static void inDiagramTransaction(IDiagram diagram, TransactionType type, Runnable r) { TransactionContext ctx = diagram.getDiagramClass().getAtMostOneItemOfClass(TransactionContext.class); if (ctx == null) throw new IllegalArgumentException("Diagram does not have a TransactionContext handler: " + diagram + ". Cannot execute runnable " + r); Transaction txn = ctx.startTransaction(diagram, type); try { r.run(); } finally { ctx.finishTransaction(diagram, txn); } } /** * Execute the specified callback within a diagram write transaction * using the {@link TransactionContext} handler available in the * {@link DiagramClass} of the specified {@link Diagram}. The diagram must * contain a valid value for the {@link DiagramHints#KEY_MUTATOR} hint which * is passed to the specified callback as an argument. This utility takes * care of clearing the diagram mutator before callback invocation and * clearing/committing its modifications after callback invocation depending * on its success. * * @param diagram the diagram to execute the transaction for * @param callback the runnable to execute * * @throws IllegalArgumentException if the specified diagram does not have a * {@link TransactionContext} handler or if the diagram does not * have a valid value for the {@link DiagramHints#KEY_MUTATOR} hint */ public static void mutateDiagram(IDiagram diagram, Consumer callback) { DiagramMutator mutator = diagram.getHint(DiagramHints.KEY_MUTATOR); if (mutator == null) throw new IllegalArgumentException("Diagram does not have an associated DiagramMutator (see DiagramHints.KEY_MUTATOR)."); TransactionContext ctx = diagram.getDiagramClass().getAtMostOneItemOfClass(TransactionContext.class); if (ctx == null) throw new IllegalArgumentException("Diagram does not have a TransactionContext handler: " + diagram + ". Cannot execute callback " + callback); Transaction txn = ctx.startTransaction(diagram, TransactionType.WRITE); boolean committed = false; try { mutator.clear(); callback.accept(mutator); mutator.commit(); committed = true; } finally { if (!committed) mutator.clear(); ctx.finishTransaction(diagram, txn); } } /** * Invokes a diagram mutation that synchronizes the hints of all the * specified elements into the back-end. * * @param diagram the diagram to mutate * @param elements the elements to synchronize to the back-end */ public static void synchronizeHintsToBackend(IDiagram diagram, final IElement... elements) { synchronizeHintsToBackend(diagram, Arrays.asList(elements)); } /** * Invokes a diagram mutation that synchronizes the hints of all the * specified elements into the back-end. * * @param diagram the diagram to mutate * @param elements the elements to synchronize to the back-end */ public static void synchronizeHintsToBackend(IDiagram diagram, final Collection elements) { mutateDiagram(diagram, m -> { for (IElement e : elements) m.synchronizeHintsToBackend(e); }); } /** * @param elements * @return */ public static Collection withChildren(Collection elements) { ArrayList result = new ArrayList(elements.size()*2); result.addAll(elements); for (int pos = 0; pos < result.size(); ++pos) { IElement element = result.get(pos); Children children = element.getElementClass().getAtMostOneItemOfClass(Children.class); if (children != null) { children.getChildren(element, result); } } return result; } /** * @param elements * @return */ public static Collection withDirectChildren(Collection elements) { ArrayList result = new ArrayList(elements); return getDirectChildren(elements, result); } /** * @param elements * @param * @return */ public static Collection getDirectChildren(Collection elements, Collection result) { for (IElement element : elements) { Children children = element.getElementClass().getAtMostOneItemOfClass(Children.class); if (children != null) children.getChildren(element, result); } return result; } /** * @param diagram * @param e */ public static void testInclusion(IDiagram diagram, IElement e) { BendsHandler bh = e.getElementClass().getAtMostOneItemOfClass(BendsHandler.class); BranchPoint bp = e.getElementClass().getAtMostOneItemOfClass(BranchPoint.class); assertAndPrint(e,e instanceof Element); if(bh == null && bp == null) { assertAndPrint(e,diagram == e.peekDiagram()); } else { assertAndPrint(e,e.peekDiagram() == null); ConnectionEntity ce = e.getHint(ElementHints.KEY_CONNECTION_ENTITY); assertAndPrint(e,ce != null); assertAndPrint(e,diagram == ce.getConnection().getDiagram()); } } /** * @param diagram */ public static void testDiagram(IDiagram diagram) { if (!(diagram instanceof Diagram)) return; Collection es = withChildren(diagram.getSnapshot()); for (IElement e : es) { System.out.println("test element " + e + " " + e.getElementClass()); testInclusion(diagram, e); Set> entrySet = e.getHintsOfClass(TerminalKeyOf.class).entrySet(); for (Map.Entry entry : entrySet) { Connection c = (Connection) entry.getValue(); testInclusion(diagram, c.node); testInclusion(diagram, c.edge); } BendsHandler bh = e.getElementClass().getAtMostOneItemOfClass(BendsHandler.class); if (bh != null) { Collection values = e.getHintsOfClass(EndKeyOf.class).values(); assertAndPrint(e, values.size() == 2); Iterator it = values.iterator(); Connection e1 = (Connection)it.next(); Connection e2 = (Connection)it.next(); testInclusion(diagram, e1.node); testInclusion(diagram, e1.edge); testInclusion(diagram, e2.node); testInclusion(diagram, e2.edge); assertAndPrint(e, e1.end.equals(e2.end.other())); } } } public static void pruneDiagram(IDiagram diagram) { if (!(diagram instanceof Diagram)) return; Collection es = withChildren(diagram.getSnapshot()); for (IElement e : es) { BendsHandler bh = e.getElementClass().getAtMostOneItemOfClass(BendsHandler.class); if (bh != null) { Set> values = e.getHintsOfClass(EndKeyOf.class).entrySet(); if (values.size() == 2) { Iterator> it = values.iterator(); Map.Entry e1 = it.next(); Map.Entry e2 = it.next(); if (!(((Connection) e1.getValue()).node instanceof Element)) { e.removeHint(e1.getKey()); System.out.println("###################### PRUNED: " /*+ ((Connection)e1.getValue()).node*/); } if (!(((Connection) e2.getValue()).node instanceof Element)) { e.removeHint(e2.getKey()); System.out.println("###################### PRUNED: " /*+ ((Connection)e2.getValue()).node*/); } } } } } private static void assertAndPrint(IElement element, boolean condition) { if(!condition) { System.out.println("ASSERTION FAILED FOR"); System.out.println("-" + element); System.out.println("-" + element.getElementClass()); assert(condition); } } public static Rectangle2D getObstacleShape(IElement e) { Rectangle2D rect = ElementUtils.getElementBounds(e); AffineTransform at = ElementUtils.getTransform(e); Point2D p1 = new Point2D.Double(); Point2D p2 = new Point2D.Double(); p1.setLocation(rect.getMinX(), rect.getMinY()); at.transform(p1, p1); p2.setLocation(rect.getMaxX(), rect.getMaxY()); at.transform(p2, p2); double x0 = p1.getX(); double y0 = p1.getY(); double x1 = p2.getX(); double y1 = p2.getY(); if(x0 > x1) { double temp = x0; x0 = x1; x1 = temp; } if(y0 > y1) { double temp = y0; y0 = y1; y1 = temp; } double OBSTACLE_MARGINAL = 1.0; return new Rectangle2D.Double( x0-OBSTACLE_MARGINAL, y0-OBSTACLE_MARGINAL, (x1-x0)+OBSTACLE_MARGINAL*2, (y1-y0)+OBSTACLE_MARGINAL*2 ); } }