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}