/******************************************************************************* * 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.participant.pointertool; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.diagram.DiagramUtils; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.handler.PickRequest; import org.simantics.g2d.diagram.handler.Topology.Terminal; 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.TerminalLayout; import org.simantics.g2d.element.handler.TerminalTopology; import org.simantics.g2d.utils.GeometryUtils; import org.simantics.g2d.utils.geom.DirectionSet; /** * @author Toni Kalajainen */ public class TerminalUtil { /** * Thread local terminal list for keeping memory allocations down. */ private static final ThreadLocal> TERMINALS = new ThreadLocal>() { @Override protected ArrayList initialValue() { return new ArrayList<>(); } }; /** * Thread local element list for keeping memory allocations down. */ private static final ThreadLocal> ELEMENTS = new ThreadLocal>() { @Override protected ArrayList initialValue() { return new ArrayList<>(); } }; public static class TerminalInfo { public IElement e; public Terminal t; public AffineTransform posElem; // on element public AffineTransform posDia; // on diagram public Shape shape; // Shape or null public double distance; // Distance of terminal from pick point in millimeters @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append('[') .append("element=").append(e) .append(", terminal=").append(t) .append(", posDia=").append(posDia) .append(", shape=").append(shape) .append(", distance=").append(distance) .append(']'); return sb.toString(); } public static TerminalInfo create(Point2D p, IElement e, Terminal t, Shape terminalShape) { AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY()); TerminalInfo ti = new TerminalInfo(); ti.e = e; ti.t = t; ti.posElem = at; ti.posDia = at; ti.shape = terminalShape; return ti; } } private static final Rectangle2D POINT_PICK_SHAPE = new Rectangle2D.Double(0, 0, 0.001, 0.001); public static final Comparator ASCENDING_DISTANCE_ORDER = new Comparator() { @Override public int compare(TerminalInfo o1, TerminalInfo o2) { double d1 = o1.distance; double d2 = o2.distance; if (d1 < d2) return -1; if (d1 > d2) return 1; return 0; } }; public static class BendsInfo { public IElement e; public Bend b; } /** * Pick terminals * @param d diagram * @param pickShape pick area or null for the whole canvas (return all terminals) * @param pickPointTerminals pick terminals of a single point * @param pickAreaTerminals pick terminals that have a shape * @return terminals in z-order (bottom to top) */ public static List pickTerminals(ICanvasContext ctx, IDiagram d, Shape pickShape, boolean pickPointTerminals, boolean pickAreaTerminals) { boolean clearElements = false; List elements = null; // Pick if (pickShape != null) { elements = ELEMENTS.get(); elements.clear(); clearElements = true; PickRequest req = new PickRequest(pickShape).context(ctx); DiagramUtils.pick(d, req, elements); } else { // Select all terminals elements = d.getElements(); } if (elements.isEmpty()) return Collections.emptyList(); double pickCenterX = 0; double pickCenterY = 0; if (pickShape != null) { Rectangle2D bounds = pickShape.getBounds2D(); pickCenterX = bounds.getCenterX(); pickCenterY = bounds.getCenterY(); } List result = new ArrayList<>(); ArrayList terminals = TERMINALS.get(); for (IElement e : elements) { TerminalTopology tt = e.getElementClass().getAtMostOneItemOfClass(TerminalTopology.class); if (tt==null) continue; terminals.clear(); tt.getTerminals(e, terminals); if (terminals.isEmpty()) continue; List tls = e.getElementClass().getItemsByClass(TerminalLayout.class); for (Terminal t : terminals) { Shape terminalShape = getTerminalShape(tls, e, t); if ( terminalShape==null /* point terminal */ && !pickPointTerminals ) continue; if ( terminalShape!=null /* area terminal */ && !pickAreaTerminals ) continue; AffineTransform terminalToElement = getTerminalPosOnElement0(e, t); AffineTransform terminalToDiagram = concatenate(ElementUtils.getTransform(e), terminalToElement); // Pick distance will is set to 0 if there was no pick shape, // i.e. everything is picked. double pickDist = 0; if (pickShape != null) { Shape pickTargetShape = terminalShape != null ? terminalShape : POINT_PICK_SHAPE; // Point Terminal uses a very small box as pick shape pickTargetShape = GeometryUtils.transformShape(pickTargetShape, terminalToDiagram); if (!GeometryUtils.intersects(pickShape, pickTargetShape)) continue; pickDist = Point2D.distance(pickCenterX, pickCenterY, terminalToDiagram.getTranslateX(), terminalToDiagram.getTranslateY()); } TerminalInfo ti = new TerminalInfo(); ti.e = e; ti.posDia = terminalToDiagram; ti.posElem = terminalToElement != null ? new AffineTransform(terminalToElement) : new AffineTransform(); ti.t = t; ti.shape = terminalShape; ti.distance = pickDist; result.add(ti); } } if (clearElements) elements.clear(); terminals.clear(); return result; } /** * Pick terminals * @param d diagram * @param pickShape pick area (in diagram coordinate system) * @return terminals in z-order (bottom to top) */ public static TerminalInfo pickTerminal(ICanvasContext ctx, IDiagram diagram, Shape pickShape) { ArrayList elements = ELEMENTS.get(); elements.clear(); PickRequest req = new PickRequest(pickShape).context(ctx); DiagramUtils.pick(diagram, req, elements); if (elements.isEmpty()) return null; TerminalInfo result = new TerminalInfo(); double bestShortestDist = Double.MAX_VALUE; Rectangle2D bounds = pickShape.getBounds2D(); double pickCenterX = bounds.getCenterX(); double pickCenterY = bounds.getCenterY(); ArrayList terminals = TERMINALS.get(); for (IElement e : elements) { TerminalTopology tt = e.getElementClass().getAtMostOneItemOfClass(TerminalTopology.class); if (tt==null) continue; terminals.clear(); tt.getTerminals(e, terminals); for (Terminal t : terminals) { AffineTransform terminalToElement = getTerminalPosOnElement0(e, t); AffineTransform terminalToDiagram = concatenate(ElementUtils.getTransform(e), terminalToElement); Shape terminalShape = getTerminalShape(e, t); Shape pickTargetShape = terminalShape != null ? terminalShape : POINT_PICK_SHAPE; pickTargetShape = GeometryUtils.transformShape(pickTargetShape, terminalToDiagram); if (!GeometryUtils.intersects(pickShape, pickTargetShape)) continue; double pickDist = Point2D.distanceSq(pickCenterX, pickCenterY, terminalToDiagram.getTranslateX(), terminalToDiagram.getTranslateY()); if (pickDist>bestShortestDist) continue; result.e = e; result.posDia = terminalToDiagram; result.posElem = terminalToElement != null ? new AffineTransform(terminalToElement) : new AffineTransform(); result.t = t; result.shape = terminalShape; result.distance = Math.sqrt(pickDist); bestShortestDist = pickDist; } } elements.clear(); terminals.clear(); if (bestShortestDist==Double.MAX_VALUE) return null; return result; } /** * Get directions * @param e * @param t * @param directions null or direction set * @return */ public static DirectionSet getTerminalDirectionSet(IElement e, Terminal t, DirectionSet directions) { if (directions == null) directions = new DirectionSet(); for (TerminalLayout tl : e.getElementClass().getItemsByClass(TerminalLayout.class)) tl.getTerminalDirection(e, t, directions); return directions; } /** * Get directions * @param e * @param t * @param directions null or direction set * @return */ public static DirectionSet getTerminalPosition(IElement e, Terminal t, DirectionSet directions) { if (directions == null) directions = new DirectionSet(); for (TerminalLayout tl : e.getElementClass().getItemsByClass(TerminalLayout.class)) tl.getTerminalDirection(e, t, directions); return directions; } public static Point2D getTerminalCenterPosOnDiagram(IElement e, Terminal t) { Shape shape = getTerminalShape(e, t); Point2D terminalCenterPos = new Point2D.Double(); if (shape!=null) { Rectangle2D rect = shape.getBounds2D(); terminalCenterPos.setLocation(rect.getCenterX(), rect.getCenterY()); } // Transform to diagram AffineTransform at = getTerminalPosOnDiagram(e, t); at.transform(terminalCenterPos, terminalCenterPos); return terminalCenterPos; } /** * Get position of a terminal on diagram * @param e element * @param t terminal * @return position of a terminal on diagram */ public static AffineTransform getTerminalPosOnDiagram(IElement e, Terminal t) { AffineTransform pos = getTerminalPosOnElement0(e, t); return concatenate(ElementUtils.getTransform(e), pos); } /** * Get position of a terminal in element * @param e element * @param t terminal * @return Transform of a terminal */ public static AffineTransform getTerminalPosOnElement(IElement e, Terminal t) { AffineTransform tr = getTerminalPosOnElement0(e, t); return tr != null ? new AffineTransform(tr) : null; } /** * Get position of a terminal in element * @param e element * @param t terminal * @return Transform of a terminal */ private static AffineTransform getTerminalPosOnElement0(IElement e, Terminal t) { List tls = e.getElementClass().getItemsByClass(TerminalLayout.class); AffineTransform result = null; for (TerminalLayout tl : tls) { result = tl.getTerminalPosition(e, t); if (result!=null) return result; } return null; } /** * Get terminal shape * @param e element * @param t terminal * @return terminal shape or null */ public static Shape getTerminalShape(IElement e, Terminal t) { List tls = e.getElementClass().getItemsByClass(TerminalLayout.class); return getTerminalShape(tls, e, t); } private static Shape getTerminalShape(List tls, IElement e, Terminal t) { for (TerminalLayout tl : tls) { Shape result = tl.getTerminalShape(e, t); if (result != null) return result; } return null; } /** * * @param diagram * @param pickShape * @return bends or null */ public BendsInfo pickBends(ICanvasContext ctx, IDiagram diagram, Shape pickShape) { BendsInfo result = null; double bestShortestDist = Double.MAX_VALUE; Rectangle2D pickShapeBounds = pickShape.getBounds2D(); Point2D pickShapeCenter = new Point2D.Double(pickShapeBounds.getCenterX(), pickShapeBounds.getCenterY()); ArrayList elements = ELEMENTS.get(); elements.clear(); PickRequest req = new PickRequest(pickShape).context(ctx); DiagramUtils.pick(diagram, req, elements); ArrayList bends = new ArrayList<>(); Point2D bendPos = new Point2D.Double(); for (IElement e : diagram.getElements()) { AffineTransform elementToDiagram = ElementUtils.getTransform(e); BendsHandler bh = e.getElementClass().getSingleItem(BendsHandler.class); if (bh==null) continue; bends.clear(); bh.getBends(e, bends); for (Bend b : bends) { bh.getBendPosition(e, b, bendPos); elementToDiagram.transform(bendPos, bendPos); if (!pickShape.contains(bendPos)) continue; double dist = bendPos.distance(pickShapeCenter); if (dist>bestShortestDist) continue; dist = bestShortestDist; result = new BendsInfo(); result.e = e; result.b = b; } } elements.clear(); if (bestShortestDist==Double.MAX_VALUE) return null; return result; } /** * Checks whether the element/terminal information of the two specified * TerminalInfo structures match. * * @param t1 * @param t2 * @return true if the element and terminal instances of both * structures are the same, false otherwise */ public static boolean isSameTerminal(TerminalInfo t1, TerminalInfo t2) { if (t1 == null || t2 == null) return false; return t1.e.equals(t2.e) && t1.t.equals(t2.t); } /** * Finds those terminals among the specified set that are *
    *
  1. nearest and equal in distance (see TerminalInfo.distance)
  2. *
  3. have the same absolute diagram position
  4. *
* * @param tis the picked terminals to examine * @return the nearest position-wise overlapping terminals */ public static List findNearestOverlappingTerminals(List tis) { int len = tis.size(); if (len < 2) return tis; // Only gather the nearest terminals that are // directly on top of each other TerminalInfo nearest = null; for (int i = 0; i < len; ++i) { TerminalInfo ti = tis.get(i); if (nearest == null || ti.distance < nearest.distance) { nearest = ti; } } ArrayList result = new ArrayList<>(len); for (int i = 0; i < len; ++i) { TerminalInfo ti = tis.get(i); if (ti.distance == nearest.distance //&& ti.e.equals(nearest.e) && ti.posDia.equals(nearest.posDia)) { result.add(ti); } } return result; } private static AffineTransform concatenate(AffineTransform a, AffineTransform b) { AffineTransform result = new AffineTransform(a); if (b != null) result.concatenate(b); return result; } }