001/* KIELER Bend Points with Arc Connector.
002
003 Copyright (c) 1998-2018 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.vergil.modal;
029
030import java.awt.Shape;
031import java.awt.geom.Arc2D;
032import java.awt.geom.GeneralPath;
033import java.awt.geom.Point2D;
034import java.util.List;
035
036import javax.swing.SwingConstants;
037
038import diva.canvas.CanvasUtilities;
039import diva.canvas.Site;
040import diva.canvas.connector.ArcConnector;
041import diva.canvas.toolbox.LabelFigure;
042import ptolemy.kernel.Relation;
043import ptolemy.vergil.actor.KielerLayoutUtil;
044import ptolemy.vergil.actor.LayoutHint;
045import ptolemy.vergil.actor.LayoutHint.LayoutHintItem;
046import ptolemy.vergil.kernel.Link;
047
048///////////////////////////////////////////////////////////////////
049//// KielerLayoutArcConnector
050
051/**
052 * Extends the regular ArcConnector, allowing to draw spline
053 * paths, i.e. series of bezier curves.
054 *
055 * @version $Id$
056 * @author Ulf Rueegg
057 * @see ptolemy.vergil.actor.KielerLayoutConnector
058 * @since Ptolemy II 11.0
059 * @Pt.ProposedRating red (uru)
060 */
061
062public class KielerLayoutArcConnector extends ArcConnector {
063
064    ///////////////////////////////////////////////////////////////////
065    ////                         public methods                    ////
066
067    /**
068     * Construct a new connector with the given tail and head for the
069     * specified link. The connector is either drawn as a spline (in
070     * case KIELER layout information is available) or in the classic
071     * arc-style fashion as implemented by the super-class.
072     *
073     * @param tail The tail site.
074     * @param head The head site.
075     */
076    public KielerLayoutArcConnector(Site tail, Site head) {
077        super(tail, head);
078    }
079
080    /**
081     * Tell the connector to route itself between the current positions of the
082     * head and tail sites. If bend points are available, draw the line with
083     * these instead. Delete bend point information if modification detected
084     * (i.e., movement of one or the other end of a link).
085     */
086    @Override
087    public void route() {
088
089        // Parse the bend points if existing.
090        List<Point2D> bendPointList = null;
091        Object object = this.getUserObject();
092        Link link = null;
093        Relation relation = null;
094        LayoutHintItem layoutHintItem = null;
095        boolean considerBendPoints = false;
096        if (object instanceof Link) {
097            link = (Link) object;
098            relation = link.getRelation();
099            // relation may be null if a new link is currently dragged from some port
100            if (relation != null) {
101                LayoutHint layoutHint = (LayoutHint) relation
102                        .getAttribute("_layoutHint");
103                if (layoutHint != null) {
104                    layoutHintItem = layoutHint
105                            .getLayoutHintItem(link.getHead(), link.getTail());
106                    if (layoutHintItem != null) {
107                        // Bend points are always considered while a layout operation is
108                        // in progress to keep them from being removed. This is not quite
109                        // thread-safe, but should work since no more than one MomlChangeRequest
110                        // is executed at a given time, and those are the only things that
111                        // could trigger a problem with this code.
112                        considerBendPoints = _layoutInProgress
113                                || layoutHintItem.revalidate();
114                        if (considerBendPoints) {
115                            bendPointList = layoutHintItem.getBendPointList();
116                        } else {
117                            layoutHint.removeLayoutHintItem(layoutHintItem);
118                        }
119                    }
120                }
121            }
122        }
123
124        // Remember the original arc shape if we are going to replace it
125        // with a spline path
126        if (getShape() instanceof Arc2D) {
127            _originalShape = getShape();
128        }
129
130        if (!considerBendPoints) {
131            // In this case we have no bend point annotations available so
132            // we use the normal draw functionality.
133            setShape(_originalShape);
134            _labelLocation = null;
135            super.route();
136        }
137
138        if (considerBendPoints) {
139
140            repaint();
141
142            // The following code proceeds as follows:
143            // We drop the first and the last point of the curve that
144            //  klay calculated. This is due to the #_applyEdgeLayoutBendPointAnnotation
145            //  method ignoring the two anchor points of an edge.
146            // Here we determine two anchors that are placed on the
147            //  actual boundary of the figure (as opposed to the rectangular
148            //  bounding box) and add them to the curve. This alters the
149            //  intended shape of the curve but should be fine.
150
151            Site headSite = getHeadSite();
152            Site tailSite = getTailSite();
153
154            Point2D[] headTail = KielerLayoutUtil.getHeadTailPoints(this,
155                    bendPointList);
156            Point2D headCenter = headTail[0];
157            Point2D tailCenter = headTail[1];
158
159            // Rotate the edge's start/end decorator (if any).
160            double tailNormal = 0, headNormal = 0;
161            if (!bendPointList.isEmpty()) {
162                headNormal = KielerLayoutUtil.getNormal(headTail[0],
163                        bendPointList.get(0));
164                tailNormal = KielerLayoutUtil.getNormal(headTail[1],
165                        bendPointList.get(bendPointList.size() - 1));
166            }
167
168            tailSite.setNormal(tailNormal);
169            headSite.setNormal(headNormal);
170
171            // Adjust for decorations on the ends
172            if (getHeadEnd() != null) {
173                getHeadEnd().setNormal(headNormal);
174                getHeadEnd().setOrigin(headCenter.getX(), headCenter.getY());
175                getHeadEnd().getConnection(headCenter);
176            }
177
178            if (getTailEnd() != null) {
179                getTailEnd().setNormal(tailNormal);
180                getTailEnd().setOrigin(tailCenter.getX(), tailCenter.getY());
181                getTailEnd().getConnection(tailCenter);
182            }
183
184            // In case there are connector decorators
185            //  at the endpoints of the edge,
186            //  use the center point of the decorator as end/start point
187            //  of the spline
188            // Note that it is _not_ used as the origin of the actual
189            //  ConnectorEnds
190            if (getHeadEnd() != null) {
191                double x = getHeadEnd().getBounds().getWidth() / 2f;
192
193                Point2D connectorCenter = _rotatePoint(x, 0, headNormal);
194                bendPointList.add(0, _sub(headCenter, connectorCenter));
195            } else {
196                bendPointList.add(0, headCenter);
197            }
198
199            if (getTailEnd() != null) {
200                double x = getTailEnd().getBounds().getWidth() / 2f;
201
202                Point2D connectorCenter = _rotatePoint(x, 0, headNormal);
203                bendPointList.add(0, _sub(tailCenter, connectorCenter));
204            } else {
205                bendPointList.add(tailCenter);
206            }
207
208            // We can now create a path object for the spline points
209            Point2D[] pts = bendPointList
210                    .toArray(new Point2D[bendPointList.size()]);
211
212            GeneralPath path = _createSplinePath(null, pts);
213
214            // Now set the shape.
215            setShape(path);
216
217            // Move the label
218            if (layoutHintItem.getLabelLocation() != null) {
219                // either we got a position for the label from KIELER
220                Point2D.Double loc = layoutHintItem.getLabelLocation();
221                _labelLocation = new Point2D.Double(loc.x, loc.y);
222            } else {
223                // ... or we pick a location for the label in the middle of the connector.
224                int count = bendPointList.size();
225                Point2D point1 = bendPointList.get(count / 2 - 1);
226                Point2D point2 = bendPointList.get(count / 2);
227
228                _labelLocation = new Point2D.Double(
229                        (point1.getX() + point2.getX()) / 2,
230                        (point1.getY() + point2.getY()) / 2);
231            }
232
233            repositionLabel();
234
235            repaint();
236        }
237    }
238
239    /** Tell the connector to reposition its label if it has one.
240     * The label is currently only positioned at the center of the arc.
241     */
242    @Override
243    public void repositionLabel() {
244        LabelFigure label = getLabelFigure();
245
246        if (label != null) {
247
248            if (_labelLocation != null) {
249
250                Point2D pos = (Point2D) _labelLocation.clone();
251                CanvasUtilities.translate(pos, label.getPadding(),
252                        SwingConstants.NORTH_WEST);
253                label.translateTo(pos);
254
255                // the positions calculated by KIELER are always for the top-left corner of an element
256                label.setAnchor(SwingConstants.NORTH_WEST);
257
258            } else {
259                Point2D pt = getArcMidpoint();
260                label.translateTo(pt);
261
262                // FIXME: Need a way to override the positioning.
263                label.autoAnchor(getShape());
264            }
265
266        }
267    }
268
269    /**
270     * Notifies layout connections that a layout is in progress, which stops them
271     * from deciding to remove layout hints from relations. Without this mechanism,
272     * it can happen that layout hints get removed seemingly at random. This is
273     * caused by layout connectors thinking that one actor in a relation is moved
274     * during the application of the layout results. This in turn triggers the
275     * corresponding layout hint to be viewed as being invalid, and consequently to
276     * be removed.
277     * <p>
278     * A call to this method with the parameter value {@code true} must always be
279     * followed by a call with the parameter value {@code false}.</p>
280     * <p>
281     * <b>Note:</b> This mechanism is not thread-safe! However, since the problem
282     * only occurs while a layout result is being applied through a
283     * {@code MoMLChangeRequest} (of which only one is ever being executed at a
284     * given time), this shouldn't be a problem.</p>
285     *
286     * @param inProgress {@code true} if a layout result is currently being applied.
287     */
288    public static void setLayoutInProgress(boolean inProgress) {
289        _layoutInProgress = inProgress;
290    }
291
292    ///////////////////////////////////////////////////////////////////
293    ////                         private methods                   ////
294
295    /**
296     * Resets the given <code>thePath</code> and adds the required segments for drawing a spline.
297     * If <code>thePath</code> is <code>null</code> a new path object is created.
298     *
299     * Method is copied from KIELER's KLighD project, from class PolylineUtil.
300     *
301     * @param thePath
302     *            the path object to put the segments into, may be <code>null</code>
303     * @param points
304     *            an array of AWT Geometry {@link Point2D Point2Ds}
305     * @return the path object containing the required segments
306     */
307    private GeneralPath _createSplinePath(final GeneralPath thePath,
308            final Point2D[] points) {
309
310        final GeneralPath path = thePath != null ? thePath : new GeneralPath();
311
312        path.reset();
313        final int size = points.length;
314
315        if (size < 1) {
316            return path; // nothing to do
317        }
318
319        path.moveTo(points[0].getX(), points[0].getY());
320
321        // draw cubic sections
322        int i = 1;
323        for (; i < size - 2; i += 3) { // SUPPRESS CHECKSTYLE MagicNumber
324            path.curveTo(points[i].getX(), points[i].getY(),
325                    points[i + 1].getX(), points[i + 1].getY(),
326                    points[i + 2].getX(), points[i + 2].getY());
327        }
328
329        // in case there are not enough points for a final bezier curve,
330        // draw something reasonable
331        // size-1: one straight line
332        // size-2: one quadratic
333        switch (size - i) {
334        case 1:
335            path.lineTo(points[i].getX(), points[i].getY());
336            break;
337        case 2:
338            path.quadTo(points[i].getX(), points[i].getY(),
339                    points[i + 1].getX(), points[i + 1].getY());
340            break;
341        default:
342            // this should not happen
343            break;
344        }
345
346        return path;
347    }
348
349    /**
350     * Rotates the passed point (x,y) by the given angle.
351     *
352     * @param x coordinate
353     * @param y coordinate
354     * @param angle by which to rotate the point, in radians.
355     * @return a newly created point rotated by the given angle.
356     */
357    private Point2D _rotatePoint(double x, double y, double angle) {
358        double xnew = x * Math.cos(angle) - y * Math.sin(angle);
359        double ynew = x * Math.sin(angle) + y * Math.cos(angle);
360
361        return new Point2D.Double(xnew, ynew);
362    }
363
364    /**
365     * @param p1 first point
366     * @param p2 second point to be subtracted from p1
367     * @return a new point where point p2 is component-wise subtracted from p1.
368     */
369    private Point2D _sub(Point2D p1, Point2D p2) {
370        return new Point2D.Double(p1.getX() - p2.getX(), p1.getY() - p2.getY());
371    }
372
373    ///////////////////////////////////////////////////////////////////
374    ////                         private variables                 ////
375
376    /**
377     * Whether automatic layout is currently in progress. If so, no layout hints
378     * are removed.
379     */
380    private static boolean _layoutInProgress = false;
381
382    /**
383     * The original arc shape, cached, s.t. we can reuse it if no
384     * layout information is available.
385     */
386    private Shape _originalShape = null;
387
388    /**
389     * The location to be applied to the label if layout information is available.
390     */
391    private Point2D _labelLocation = new Point2D.Float();
392
393}