001/* Layout Hint Attribute for Ptolemy relations to specify bendpoints for an explicit routing for links. 002 003 @Copyright (c) 2011-2018 The Regents of the University of California. 004 All rights reserved. 005 006 Permission is hereby granted, without written agreement and without 007 license or royalty fees, to use, copy, modify, and distribute this 008 software and its documentation for any purpose, provided that the 009 above copyright notice and the following two paragraphs appear in all 010 copies of this software. 011 012 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 013 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 014 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 015 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 016 SUCH DAMAGE. 017 018 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 019 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 020 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 021 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 022 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 023 ENHANCEMENTS, OR MODIFICATIONS. 024 025 PT_COPYRIGHT_VERSION_2 026 COPYRIGHTENDKEY 027 */ 028 029package ptolemy.vergil.actor; 030 031import java.awt.geom.Point2D; 032import java.io.IOException; 033import java.io.Writer; 034import java.util.ArrayList; 035import java.util.Collection; 036import java.util.Iterator; 037import java.util.LinkedList; 038import java.util.List; 039import java.util.Map; 040 041import diva.canvas.connector.ManhattanConnector; 042import ptolemy.actor.CompositeActor; 043import ptolemy.actor.IOPort; 044import ptolemy.data.ArrayToken; 045import ptolemy.data.DoubleToken; 046import ptolemy.data.IntToken; 047import ptolemy.data.RecordToken; 048import ptolemy.data.ScalarToken; 049import ptolemy.data.StringToken; 050import ptolemy.data.Token; 051import ptolemy.data.expr.ASTPtRootNode; 052import ptolemy.data.expr.ParseTreeEvaluator; 053import ptolemy.data.expr.PtParser; 054import ptolemy.kernel.ComponentPort; 055import ptolemy.kernel.Port; 056import ptolemy.kernel.Relation; 057import ptolemy.kernel.util.Attribute; 058import ptolemy.kernel.util.ChangeRequest; 059import ptolemy.kernel.util.IllegalActionException; 060import ptolemy.kernel.util.Location; 061import ptolemy.kernel.util.NameDuplicationException; 062import ptolemy.kernel.util.NamedObj; 063import ptolemy.kernel.util.Settable; 064import ptolemy.kernel.util.SingletonAttribute; 065import ptolemy.kernel.util.ValueListener; 066import ptolemy.kernel.util.Workspace; 067import ptolemy.moml.Vertex; 068import ptolemy.util.StringUtilities; 069 070/////////////////////////////////////////////////////////////////// 071//// LayoutHint 072/** 073 * A LayoutHint is an Attribute for Ptolemy Relations that holds the 074 * specification of bend points for links. Its value field contains a list of 075 * {@link LayoutHintItem} objects because one Relation can correspond to 076 * multiple links, which are not real objects in the Ptolemy abstract syntax and 077 * therefore can not carry any attributes. Each item carries a list of 078 * bendpoints for a specific link as well as a location for a label 079 * of the link (if it exists). 080 * <p> 081 * The LayoutHint uses a Ptolemy Expression as its value in which the 082 * {@link LayoutHintItem} objects are encoded. Therefore the Expression is 083 * expected to contain an {@link ArrayToken} of {@link LayoutHintItem} objects.</p> 084 * 085 * <p> 086 * A complete LayoutHint with two {@link LayoutHintItem}s could look like this:</p> 087 * 088 * <pre> 089 * { 090 * { 091 * head={id="Discard.input",x=60.0,y=115.0,index=2}, 092 * tail={id="CompositeActor.port3",x=300.0,y=380.0,index=3}, 093 * points={105.0,235.0,105.0,190.0,265.0,190.0,265.0,135.0} 094 * }, 095 * { 096 * head={id="Ramp.output",x=320.0,y=225.0}, 097 * tail={id="CompositeActor.port2",x=580.0,y=200.0,index=3}, 098 * points={135.0,25.0,135.0,125.0}, 099 * labelLocation={x=340.0,y=210.0} 100 * } 101 * } 102 * </pre> 103 * 104 * <p>This storage works like a {@link Map} with always two keys. One 105 * {@link LayoutHintItem} is unambiguously identified by its head and tail, 106 * which are Ptolemy objects like {@link Port}s or {@link Relation}s. The 107 * methods to access this are {@link #getLayoutHintItem(Object, Object)}, 108 * {@link #setLayoutHintItem(NamedObj, NamedObj, double[], Point2D.Double)} and 109 * {@link #removeLayoutHintItem(LayoutHintItem)}.</p> 110 * 111 * <p> The class extends {@link SingletonAttribute} because every 112 * Relation is expected to have only one such Attribute, while one of 113 * these Attributes can carry multiple {@link LayoutHintItem}s as 114 * explained above. It is also {@link ptolemy.kernel.util.Settable} as 115 * it can be set by loading a MOML file or by setting it manually 116 * through the GUI. However, usually its visibility is set to EXPERT 117 * mode only.</p> 118 * 119 * <p> 120 * Some of the standard code for example for value listeners is copied from 121 * {@link Location}.</p> 122 * 123 * @author Hauke Fuhrmann, (kieler@informatik.uni-kiel.de), Ulf Rueegg 124 * @since Ptolemy II 11.0 125 * @Pt.ProposedRating Red (haf) 126 * @Pt.AcceptedRating Red (haf) 127 */ 128public class LayoutHint extends SingletonAttribute implements Settable { 129 130 /////////////////////////////////////////////////////////////////// 131 //// public methods //// 132 133 /** Construct an attribute with the given container and name. 134 * @param container The container. 135 * @param name The name of this attribute. 136 * @exception IllegalActionException If the attribute cannot be contained 137 * by the proposed container. 138 * @exception NameDuplicationException If the container already has an 139 * attribute with this name, and the class of that container is not 140 * SingletonAttribute. 141 * 142 * @see SingletonAttribute#SingletonAttribute(NamedObj, String) 143 */ 144 public LayoutHint(NamedObj container, String name) 145 throws IllegalActionException, NameDuplicationException { 146 super(container, name); 147 } 148 149 /** Construct a new attribute with 150 * no container and an empty string as a name. 151 * @param workspace The workspace that will list the attribute. 152 * @see SingletonAttribute#SingletonAttribute(Workspace) 153 */ 154 public LayoutHint(Workspace workspace) { 155 super(workspace); 156 } 157 158 /** 159 * Add a listener to be notified when the value of this attribute changes. 160 * If the listener is already on the list of listeners, then do nothing. 161 * 162 * @param listener The listener to add. 163 * @see #removeValueListener(ValueListener) 164 */ 165 @Override 166 public void addValueListener(ValueListener listener) { 167 if (_valueListeners == null) { 168 _valueListeners = new LinkedList(); 169 } 170 171 if (!_valueListeners.contains(listener)) { 172 _valueListeners.add(listener); 173 } 174 } 175 176 /** 177 * Write a MoML description of this object. MoML is an XML modeling markup 178 * language. In this class, the object is identified by the "property" 179 * element, with "name", "class", and "value" (XML) attributes. The body of 180 * the element, between the "<property>" and "</property>", is 181 * written using the _exportMoMLContents() protected method, so that derived 182 * classes can override that method alone to alter only how the contents of 183 * this object are described. The text that is written is indented according 184 * to the specified depth, with each line (including the last one) 185 * terminated with a newline. If this object is non-persistent, then nothing 186 * is written. 187 * 188 * @see Location#exportMoML(Writer, int, String) 189 * @param output The output writer to write to. 190 * @param depth The depth in the hierarchy, to determine indenting. 191 * @param name The name to use instead of the current name. 192 * @exception IOException If an I/O error occurs. 193 * @see #isPersistent() 194 */ 195 @Override 196 public void exportMoML(Writer output, int depth, String name) 197 throws IOException { 198 // If the object is not persistent, and we are not at level 0, do nothing. 199 if (_isMoMLSuppressed(depth)) { 200 return; 201 } 202 203 String value = getExpression(); 204 String valueTerm = ""; 205 206 if (value != null && !value.equals("")) { 207 valueTerm = " value=\"" + StringUtilities.escapeForXML(value) 208 + "\""; 209 } 210 211 // It might be better to use multiple writes here for performance. 212 output.write(_getIndentPrefix(depth) + "<" + _elementName + " name=\"" 213 + name + "\" class=\"" + getClassName() + "\"" + valueTerm 214 + ">\n"); 215 _exportMoMLContents(output, depth + 1); 216 output.write(_getIndentPrefix(depth) + "</" + _elementName + ">\n"); 217 } 218 219 /** 220 * A LayoutHint has no default expression. 221 * @return always null 222 */ 223 @Override 224 public String getDefaultExpression() { 225 return null; 226 } 227 228 /** 229 * Get the value that has been set by setExpression() or by 230 * setLayoutHintItem(), whichever was most recently called, or return an 231 * empty string if neither has been called. 232 * 233 * <p> 234 * If setExpression(String value) was called, then the return value is 235 * exactly what ever was passed in as the argument to setExpression. This 236 * means that there is no guarantee that the return value of getExpression() 237 * is a well formed Ptolemy array expression.</p> 238 * 239 * <p> 240 * If setLayoutHintItem(NamedObj, NamedObj, double[]) was called, then the 241 * return value is a well formed Ptolemy array expression that starts with 242 * "{" and ends with "}", and contains the expressions of 243 * {@link LayoutHintItem}s as array elements. Example:</p> 244 * 245 * <pre> 246 * { item1, item2 } 247 * </pre> 248 * 249 * @return The expression. 250 * @see Location#getExpression() 251 * @see #setExpression(String) 252 */ 253 @Override 254 public String getExpression() { 255 if (_expressionSet) { 256 // FIXME: If setExpression() was called with a string that does 257 // not begin and end with curly brackets, then getExpression() 258 // will not return something that is parseable by setExpression() 259 return _expression; 260 } 261 StringBuffer buffer = new StringBuffer(); 262 buffer.append("{ "); 263 int i = 0; 264 for (LayoutHintItem item : _layoutHintItems) { 265 if (i > 0) { 266 buffer.append(","); 267 } 268 buffer.append(item.getExpression()); 269 i++; 270 } 271 buffer.append(" }"); 272 return buffer.toString(); 273 } 274 275 /** 276 * Get the {@link LayoutHintItem} stored in this LayoutHint that is 277 * identified by the head and tail of the link for which it specifies bend 278 * points. If no {@link LayoutHintItem} is stored for the given head and 279 * tail, null is returned. It works like a map with two keys that have to 280 * match. As links in Ptolemy are not directed, it does not matter if head 281 * and tail get switched. However, for layout the direction does matter and 282 * the bendpoint list is directed from head to tail. So if there is an item 283 * available where head and tail are swapped, then this item will be 284 * returned but the entries get swapped again to guarantee that head and 285 * tail and the bendpoint order are correct. 286 * 287 * @param head The starting point of the link, e.g. a Ptolemy Port or 288 * Relation. 289 * @param tail The ending point of the link, e.g. a Ptolemy Port or 290 * Relation. 291 * @return the LayoutHintItem stored for this link or null 292 * @see #setLayoutHintItem(NamedObj, NamedObj, double[], Point2D.Double) 293 */ 294 public LayoutHintItem getLayoutHintItem(Object head, Object tail) { 295 for (LayoutHintItem item : _layoutHintItems) { 296 if (item.getHead() == head && item.getTail() == tail) { 297 return item; 298 } 299 // also return this hint if head and tail are switched 300 if (item.getHead() == tail && item.getTail() == head) { 301 item._reverse(); 302 return item; 303 } 304 } 305 return null; 306 } 307 308 /** 309 * Get the value of the attribute, which is the evaluated expression. 310 * @return The value. 311 * @see Settable#getValueAsString() 312 */ 313 @Override 314 public String getValueAsString() { 315 return getExpression(); 316 } 317 318 /** 319 * Get the visibility of this Settable, as set by setVisibility(). 320 * The returned value is one of the static 321 * instances of the {@link ptolemy.kernel.util.Settable.Visibility} inner class. 322 * @return The visibility of this Settable. 323 * @see #setVisibility(ptolemy.kernel.util.Settable.Visibility) 324 * @see ptolemy.kernel.util.Settable#getVisibility() 325 */ 326 @Override 327 public Visibility getVisibility() { 328 return _visibility; 329 } 330 331 /** 332 * Remove a {@link LayoutHintItem} from this storage. If that is the last 333 * item contained in this layout hint, then the layout hint itself is 334 * removed from its container. 335 * 336 * @param itemToRemove The layout hint item to remove 337 */ 338 public void removeLayoutHintItem(final LayoutHintItem itemToRemove) { 339 final NamedObj container = getContainer(); 340 if (container != null) { 341 container.requestChange( 342 new ChangeRequest(container, "Remove Layout Hint") { 343 @Override 344 protected void _execute() throws Exception { 345 _layoutHintItems.remove(itemToRemove); 346 if (_layoutHintItems.isEmpty()) { 347 setContainer(null); 348 } 349 } 350 }); 351 } 352 } 353 354 /** 355 * Remove a listener from the list of listeners that is notified when the 356 * value of this variable changes. If no such listener exists, do nothing. 357 * 358 * @param listener The listener to remove. 359 * @see Location#removeValueListener(ValueListener) 360 * @see #addValueListener(ValueListener) 361 */ 362 @Override 363 public void removeValueListener(ValueListener listener) { 364 if (_valueListeners != null) { 365 _valueListeners.remove(listener); 366 } 367 } 368 369 /** 370 * Set the value of the attribute by giving some expression. This expression 371 * is not parsed until validate() is called, and the container and value 372 * listeners are not notified until validate() is called. See the class 373 * comment for a description of the format. 374 * 375 * @param expression The value of the attribute. 376 * @see #getExpression() 377 */ 378 @Override 379 public void setExpression(String expression) { 380 _expression = expression; 381 _expressionSet = true; 382 } 383 384 /** 385 * Set a {@link LayoutHintItem} for a link which is specified by its head 386 * and tail, i.e. Ptolemy {@link Port}s or {@link Relation}s. For this link 387 * store the given list of bend points. Like in a {@link Map} with two keys, 388 * a possibly existing item for the given head and tail will be reused and 389 * updated with the bend points. If no such item yet exists, a new one is 390 * added. 391 * 392 * @param head the head object of the corresponding link 393 * @param tail the tail object of the corresponding link 394 * @param bendPoints an array of double coordinates, where always two 395 * correspond to a bend point 396 * @param labelLocation the location of a label if it exists, may be null 397 * @see #getLayoutHintItem(Object, Object) 398 */ 399 public void setLayoutHintItem(NamedObj head, NamedObj tail, 400 double[] bendPoints, Point2D.Double labelLocation) { 401 _expressionSet = false; 402 LayoutHintItem item = getLayoutHintItem(head, tail); 403 if (item == null) { 404 item = new LayoutHintItem(head, tail); 405 _layoutHintItems.add(item); 406 } 407 // make sure head and tail are in the right order 408 if (head == item.getTail() && tail == item.getHead()) { 409 // they are reversed, so reverse also bendpoints 410 _reverseCoordinateArray(bendPoints); 411 } 412 413 item.setBendpoints(bendPoints); 414 415 item.setLabelLocation(labelLocation); 416 417 if (_valueListeners != null) { 418 Iterator listeners = _valueListeners.iterator(); 419 420 while (listeners.hasNext()) { 421 ValueListener listener = (ValueListener) listeners.next(); 422 listener.valueChanged(this); 423 } 424 } 425 } 426 427 /** 428 * Set the visibility of this attribute. The argument should be one of the 429 * public static instances in Settable. 430 * 431 * @param visibility The visibility of this attribute. 432 * @see #getVisibility() 433 */ 434 @Override 435 public void setVisibility(Settable.Visibility visibility) { 436 _visibility = visibility; 437 } 438 439 /** 440 * Parse the layout hint specification given by setExpression(), if there 441 * has been one, and otherwise do nothing, i.e. keep the list of layout 442 * hints empty. Notify the container and any value listeners of the new 443 * location, if it has changed. See the class comment for a description of 444 * the format. 445 * 446 * @return Null, indicating that no other instances of Settable are 447 * validated. 448 * @exception IllegalActionException If the expression is invalid. 449 */ 450 @Override 451 public Collection validate() throws IllegalActionException { 452 _layoutHintItems = new ArrayList<LayoutHintItem>(); 453 if (_expression == null) { 454 return null; // don't parse anything if there is no expression 455 } 456 // parse the expression 457 Token result; 458 try { 459 PtParser parser = new PtParser(); 460 ASTPtRootNode parseTree = parser.generateParseTree(_expression); 461 ParseTreeEvaluator parseTreeEvaluator = new ParseTreeEvaluator(); 462 result = parseTreeEvaluator.evaluateParseTree(parseTree, null); 463 } catch (Throwable throwable) { 464 // Chain exceptions to get the actor that threw the exception. 465 // Note that if evaluateParseTree does a divide by zero, we 466 // need to catch an ArithmeticException here. 467 throw new IllegalActionException(this, throwable, 468 "Expression invalid."); 469 } 470 if (result == null) { 471 throw new IllegalActionException(this, 472 "Expression yields a null result: " + _expression); 473 } 474 _addLayoutHintItems(result); 475 return null; 476 } 477 478 /////////////////////////////////////////////////////////////////// 479 //// protected methods //// 480 481 /** 482 * Propagate the value of this object to the specified object. The specified 483 * object is required to be an instance of the same class as this one, or a 484 * ClassCastException will be thrown. 485 * 486 * @param destination Object to which to propagate the value. 487 * @exception IllegalActionException If the value cannot be propagated. 488 * @see Location#_propagateValue(NamedObj) 489 */ 490 @Override 491 protected void _propagateValue(NamedObj destination) 492 throws IllegalActionException { 493 ((LayoutHint) destination).setExpression(getExpression()); 494 } 495 496 /////////////////////////////////////////////////////////////////// 497 //// private methods //// 498 499 /** 500 * Create {@link LayoutHintItem}s from parsed Ptolemy Expression in form of 501 * a {@link Token}. The token is expected to have exactly a specific format 502 * as given in the class comment. 503 * 504 * @param hints a Token containing the required information about the 505 * LayoutHintItems 506 * @exception IllegalActionException thrown when the Token does not conform to 507 * the expected format. 508 */ 509 private void _addLayoutHintItems(Token hints) 510 throws IllegalActionException { 511 try { 512 // The token is expected to be an array of LayoutHintItems. 513 for (int i = 0; i < ((ArrayToken) hints).length(); i++) { 514 // Each LayoutHintItem is expected to be a Record. 515 RecordToken layoutItem = (RecordToken) ((ArrayToken) hints) 516 .getElement(i); 517 // A LayoutHintItem has a head and tail entry, which are 518 // records containing an identifying String, coordinates, 519 // and optionally the width of a multiport. 520 RecordToken headToken = (RecordToken) layoutItem.get("head"); 521 NamedObj head = _findNamedObj(this, 522 ((StringToken) headToken.get("id")).stringValue()); 523 Point2D.Double headLocation = new Point2D.Double(); 524 headLocation.x = ((DoubleToken) headToken.get("x")) 525 .doubleValue(); 526 headLocation.y = ((DoubleToken) headToken.get("y")) 527 .doubleValue(); 528 int headMultiportWidth = 1; 529 if (headToken.get("index") != null) { 530 headMultiportWidth = ((IntToken) headToken.get("index")) 531 .intValue(); 532 } 533 534 RecordToken tailToken = (RecordToken) layoutItem.get("tail"); 535 NamedObj tail = _findNamedObj(this, 536 ((StringToken) tailToken.get("id")).stringValue()); 537 Point2D.Double tailLocation = new Point2D.Double(); 538 tailLocation.x = ((DoubleToken) tailToken.get("x")) 539 .doubleValue(); 540 tailLocation.y = ((DoubleToken) tailToken.get("y")) 541 .doubleValue(); 542 int tailMultiportWidth = 1; 543 if (tailToken.get("index") != null) { 544 tailMultiportWidth = ((IntToken) tailToken.get("index")) 545 .intValue(); 546 } 547 548 // label location 549 Point2D.Double labelLocation = null; 550 Token labelLocationToken = layoutItem.get("labelLocation"); 551 if (labelLocationToken != null) { 552 labelLocation = new Point2D.Double(); 553 RecordToken labelRecord = (RecordToken) labelLocationToken; 554 labelLocation.x = ((DoubleToken) labelRecord.get("x")) 555 .doubleValue(); 556 labelLocation.y = ((DoubleToken) labelRecord.get("y")) 557 .doubleValue(); 558 } 559 560 // The LayoutHintItem record contains a points entry, containing 561 // an array of bend points. 562 ArrayToken bendPoints = (ArrayToken) layoutItem.get("points"); 563 // Only do if head and tail could be resolved. 564 // This can fail if you insert a new relation vertex into an 565 // existing link. Then first the attribute is copied and only 566 // then the relation is inserted into the diagram. Therefore the 567 // head and tail cannot be found. However, this LayoutHint will 568 // be invalid in such case anyways. 569 if (head != null && tail != null) { 570 // create new LayoutHintItem and add it to this LayoutHint 571 LayoutHintItem item = new LayoutHintItem(head, tail, 572 headLocation, tailLocation, headMultiportWidth, 573 tailMultiportWidth, labelLocation); 574 double[] primitiveBendPoints = new double[bendPoints 575 .length()]; 576 for (int ii = 0; ii < bendPoints.length(); ii++) { 577 primitiveBendPoints[ii] = ((ScalarToken) bendPoints 578 .getElement(ii)).doubleValue(); 579 } 580 item.setBendpoints(primitiveBendPoints); 581 _layoutHintItems.add(item); 582 } 583 } 584 } catch (Exception e) { 585 throw new IllegalActionException(this, e, e.getMessage() 586 + "\nExpression is expected to be an Array of layout hint Records. " 587 + "The following expression is of wrong format: \n" 588 + _expression 589 + "\nAn example for a layoutHint expression is\n" 590 + EXAMPLE_EXPRESSION); 591 } 592 } 593 594 /** 595 * Find the first CompositeActor in the parent hierarchy of the given 596 * NamedObj and find some other NamedObj with a given String name in there. 597 * 598 * @param start start object in the parent hierarchy 599 * @param name name of the object to find in the first CompositeActor found 600 * @return the object found or null if not found 601 */ 602 private static NamedObj _findNamedObj(NamedObj start, String name) { 603 NamedObj result = null; 604 NamedObj container = start.getContainer(); 605 while (container != null && !(container instanceof CompositeActor)) { 606 container = container.getContainer(); 607 } 608 if (container != null) { 609 result = ((CompositeActor) container).getPort(name); 610 if (result == null) { 611 result = ((CompositeActor) container).getEntity(name); 612 } 613 if (result == null) { 614 result = ((CompositeActor) container).getAttribute(name); 615 } 616 } 617 return result; 618 } 619 620 /** 621 * Reverse the order of an array of coordinates, i.e. every two entries are 622 * assumed to belong together and will be kept in right order. 623 * 624 * @param bendPoints the array to reverse, will be changed 625 * @return the changed array 626 */ 627 private static double[] _reverseCoordinateArray(double[] bendPoints) { 628 int size = bendPoints.length - bendPoints.length % 2; 629 // Make sure only to process even length. 630 // Don't do anything if the array has only one location. 631 if (size >= 4) { 632 double tempx, tempy; 633 int lastX, lastY; 634 int iterations = size / 4; 635 for (int i = 0; i < iterations; i += 1) { 636 int index = i * 2; 637 tempx = bendPoints[index]; 638 tempy = bendPoints[index + 1]; 639 lastY = size - 1 - index; 640 lastX = lastY - 1; 641 bendPoints[index] = bendPoints[lastX]; 642 bendPoints[index + 1] = bendPoints[lastY]; 643 bendPoints[lastX] = tempx; 644 bendPoints[lastY] = tempy; 645 } 646 } 647 return bendPoints; 648 } 649 650 /** A valid example expression to show in the GUI in case of errors. */ 651 private static final String EXAMPLE_EXPRESSION = "{ \n{head={id=\"a.out\",x=10,y=11}," 652 + "tail={id=\"relation1\",x=20,y=21},points={1,2,3,4,5,6}} ," 653 + " \n{head={id=\"b.out1\",x=10,y=11}," 654 + "tail={id=\"relation2\",x=20,y=21},points={1,2,3,4,5,6}} \n}"; 655 /** The expression given in setExpression(). */ 656 private String _expression; 657 /** Indicator that the expression is the most recent spec for the location. */ 658 private boolean _expressionSet = false; 659 /** List of layout hint items stored by this layout hint */ 660 private List<LayoutHintItem> _layoutHintItems = new ArrayList<LayoutHintItem>(); 661 /** Listeners for changes in value. */ 662 private List _valueListeners; 663 /** The visibility of this attribute, which defaults to EXPERT. */ 664 private Settable.Visibility _visibility = Settable.EXPERT; 665 666 /** 667 * A LayoutHintItem is the specification of layout information for one Link. 668 * As there are usually multiple links corresponding to one {@link Relation}, 669 * a {@link LayoutHint} is attached to a Relation and carries multiple of 670 * these LayoutHintItems corresponding to the links. As links are no 671 * persisted objects in Ptolemy, a link is identified by its head and tail 672 * objects, which are {@link Port}s or {@link Relation}s. 673 * <p> 674 * The most important information such item carries is a list of bend points 675 * that can be used to explicitly route a link along these bend points 676 * instead of using a simple routing strategy like the 677 * {@link ManhattanConnector}. A router that uses the bend point information 678 * for example is the {@link KielerLayoutConnector}. 679 * <p> 680 * Such item can be serialized to the String representation of a Ptolemy 681 * Expression by {@link #getExpression()}. This is used for persisting 682 * LayoutHintItems. However, the bend point data are absolute coordinates 683 * and therefore are only valid until the head and/or tail of the link are 684 * moved. Hence, the LayoutHintItem also stores the coordinates and 685 * optionally the multiport width of head and tail, which specify for which 686 * layout of nodes the bend point information is only valid. The 687 * {@link #revalidate()} method is used to check the validity of the 688 * LayoutHintItem by comparing the stored positions with the actual 689 * positions in the diagram, i.e. checking whether head and/or tail have 690 * been moved or the width of a multiport has changed. If the LayoutHintItem 691 * is not valid anymore, its bend points should not be used. 692 * <p> 693 * A special case is when head and tail moved relatively exactly the same, 694 * which happens, if multiple elements are selected and moved together. In 695 * such case the bend points are still valid relatively, but not absolutely. 696 * Therefore the {@link #revalidate()} method also checks this case and 697 * translates the bend point coordinates as well as the new head and tail 698 * locations making the LayoutHintItem valid again. This avoids invalidating 699 * bend points when whole model parts get moved. 700 * <p> 701 * An example for one LayoutHintItem's String representation is the 702 * following 703 * 704 * <pre> 705 * { head={"CompositeActor.port",20.0,200.0,2}, tail={"Discard.input",70.0,25.0}, points={135.0,25.0,135.0,125.0} } 706 * </pre> 707 * 708 * The head contains the object's name, its coordinates in x and y and the 709 * width, because the port is a multiport. The width defaults to 1 as can be 710 * seen at the tail where it is omitted. 711 */ 712 public static class LayoutHintItem { 713 714 /////////////////////////////////////////////////////////////////// 715 //// public methods //// 716 717 /** 718 * Simple constructor specifying only head and tail for this 719 * LayoutHintItem. The current layout of head and tail that is required 720 * for validity checking is obtained from these objects automatically. 721 * 722 * @param head the head object of the corresponding link 723 * @param tail the tail object of the corresponding link 724 */ 725 public LayoutHintItem(NamedObj head, NamedObj tail) { 726 this._head = head; 727 this._tail = tail; 728 _updateHeadTailLocations(); 729 } 730 731 /** 732 * Constructor passing not only head and tail but also all required 733 * layout information for the conditions under which this LayoutHintItem 734 * is only valid. 735 * 736 * @param head the head object of the corresponding link 737 * @param tail the tail object of the corresponding link 738 * @param locationHead the location of the head as vector 739 * @param locationTail the location of the tail as vector 740 * @param multiportWidthHead the width of the head, which is relevant 741 * for multiports, 1, if no multiport 742 * @param multiportWidthTail the width of the tail, which is relevant 743 * for multiports, 1, if no multiport 744 * @param labelPosition The position of the label. 745 */ 746 public LayoutHintItem(NamedObj head, NamedObj tail, 747 Point2D.Double locationHead, Point2D.Double locationTail, 748 int multiportWidthHead, int multiportWidthTail, 749 Point2D.Double labelPosition) { 750 this(head, tail); 751 this._headLocation = locationHead; 752 this._tailLocation = locationTail; 753 this._headMultiportIndex[1] = multiportWidthHead; 754 this._tailMultiportIndex[1] = multiportWidthTail; 755 this._labelLocation = labelPosition; 756 } 757 758 /** 759 * Get the bend points stored in this hint as an array of doubles, where 760 * each two entries correspond to x and y of one bend point. 761 * 762 * @return array containing bend point coordinates 763 */ 764 public double[] getBendPoints() { 765 return _bendPoints; 766 } 767 768 /** 769 * Get a list of {@link Point2D} corresponding to the bend points stored 770 * in this item. If by setting the bend points with 771 * {@link #setBendpoints(double[])} the list of bend point coordinates 772 * is odd, the last coordinate is discarded, and a list of points 773 * without the dangling coordinate is returned. 774 * 775 * @return list of bend points 776 */ 777 public List<Point2D> getBendPointList() { 778 int size = _bendPoints.length / 2; // integer arithmetics will cut 779 // off in odd cases 780 ArrayList<Point2D> list = new ArrayList<Point2D>(size); 781 for (int i = 0; i < size; i++) { 782 Point2D point = new Point2D.Double(_bendPoints[2 * i], 783 _bendPoints[2 * i + 1]); 784 list.add(point); 785 } 786 return list; 787 } 788 789 /** 790 * A {@link Point2D} representing the position where a label of an 791 * edge should be positioned. 792 * 793 * @return the label location. 794 * @see #setLabelLocation(Point2D.Double) 795 */ 796 public Point2D.Double getLabelLocation() { 797 return _labelLocation; 798 } 799 800 /** 801 * Get the String representation of the Ptolemy Expression by which this 802 * LayoutHint is persisted. See the class comment for the concrete 803 * specification. 804 * 805 * @return String representation of this LayoutHint 806 */ 807 public String getExpression() { 808 StringBuffer buffer = new StringBuffer(); 809 buffer.append("{ head={id=\"" + _getName(_head) + "\"" + ",x=" 810 + _headLocation.x + ",y=" + _headLocation.y); 811 if (_headMultiportIndex[1] != 1) { 812 buffer.append(",index=" + _headMultiportIndex[1]); 813 } 814 buffer.append("}, " + "tail={id=\"" + _getName(_tail) + "\"" + ",x=" 815 + _tailLocation.x + ",y=" + _tailLocation.y); 816 if (_tailMultiportIndex[1] != 1) { 817 buffer.append(",index=" + _tailMultiportIndex[1]); 818 } 819 buffer.append("}, " + "points={"); 820 for (int i = 0; i < _bendPoints.length - 1; i += 2) { 821 if (i > 0) { 822 buffer.append(","); 823 } 824 buffer.append(_bendPoints[i] + "," + _bendPoints[i + 1]); 825 } 826 if (_labelLocation != null) { 827 buffer.append("}, " + "labelLocation={"); 828 buffer.append("x=" + _labelLocation.x); 829 buffer.append(",y=" + _labelLocation.y); 830 } 831 buffer.append("} }"); 832 return buffer.toString(); 833 } 834 835 /** 836 * Get the head of this LayoutHint which is used to identify this hint. 837 * 838 * @return head object of this LayoutHint 839 */ 840 public NamedObj getHead() { 841 return _head; 842 } 843 844 /** 845 * Get the tail of this LayoutHint which is used to identify this hint. 846 * 847 * @return tail object of this LayoutHint 848 */ 849 public NamedObj getTail() { 850 return _tail; 851 } 852 853 /** 854 * Check if the head and tail objects have been moved. If this is the 855 * case but both have been moved while keeping the same relative 856 * position to each other, the bend points can be translated 857 * accordingly. In this case, update the bend points and the head and 858 * tail location. If they have been moved and the relative positions are 859 * different now, then return false. In that case the bend point list is 860 * no longer feasible and should not be used anymore, however, no update 861 * is done here. 862 * 863 * @return true if the relative positions of head and tail are the same 864 * as before, false otherwise 865 */ 866 public boolean revalidate() { 867 if (_bendPoints == null) { 868 return false; 869 } 870 if (_head instanceof Port) { 871 // check if the width of a multiport has changed 872 int width = _getChannelWidth(_head); 873 if (width != _headMultiportIndex[1]) { 874 return false; 875 } 876 } 877 if (_tail instanceof Port) { 878 int width = _getChannelWidth(_tail); 879 if (width != _tailMultiportIndex[1]) { 880 return false; 881 } 882 } 883 Point2D.Double newHeadLocation = _getEndpointLocation(_head); 884 Point2D.Double newTailLocation = _getEndpointLocation(_tail); 885 886 if (newHeadLocation.equals(_headLocation) 887 && newTailLocation.equals(_tailLocation)) { 888 // nothing has changed, we don't need to update anything 889 return true; 890 } 891 892 double oldDistanceX = _headLocation.x - _tailLocation.x; 893 double oldDistanceY = _headLocation.y - _tailLocation.y; 894 double newDistanceX = newHeadLocation.x - newTailLocation.x; 895 double newDistanceY = newHeadLocation.y - newTailLocation.y; 896 897 // do an integer compare. This is safe and enough. 898 if ((int) oldDistanceX != (int) newDistanceX 899 || (int) oldDistanceY != (int) newDistanceY) { 900 // in this case we cannot use the bend points anymore 901 return false; 902 } 903 // now we know the head and tail have been moved but the relative 904 // location is the same 905 // we can safely translate everything 906 _translate(newHeadLocation.x - _headLocation.x, 907 newHeadLocation.y - _headLocation.y); 908 return true; 909 } 910 911 /** 912 * Set a new list of bend points and update the current validation 913 * information such as the current location of head and tail and their 914 * port widths. Hence, this LayoutHint will be valid until the head and 915 * tail are moved again. 916 * 917 * @param bendPoints new bend points 918 */ 919 public void setBendpoints(double[] bendPoints) { 920 _bendPoints = bendPoints; 921 _updateHeadTailLocations(); 922 } 923 924 /** 925 * Sets the position a label should be placed at. 926 * 927 * @param labelLocation a {@link Point2D} with the position, may be null. 928 * @see #getLabelLocation() 929 */ 930 public void setLabelLocation(Point2D.Double labelLocation) { 931 _labelLocation = labelLocation; 932 } 933 934 /** 935 * Get a String representation of a LayoutHint which will be the same as 936 * {@link #getExpression()}. 937 * 938 * @see #getExpression() 939 * @return String representation of this LayoutHint 940 */ 941 @Override 942 public String toString() { 943 return getExpression(); 944 } 945 946 /** 947 * Reverse the list of bend points. This may be neces 948 */ 949 protected void _reverse() { 950 Object temp; 951 // swap head and tail 952 temp = _head; 953 _head = _tail; 954 _tail = (NamedObj) temp; 955 // swap location 956 temp = _headLocation; 957 _headLocation = _tailLocation; 958 _tailLocation = (Point2D.Double) temp; 959 // swap multiport stuff 960 temp = _headMultiportIndex; 961 _headMultiportIndex = _tailMultiportIndex; 962 _tailMultiportIndex = (int[]) temp; 963 // reverse bendpoints 964 _reverseCoordinateArray(_bendPoints); 965 } 966 967 /////////////////////////////////////////////////////////////////// 968 //// private methods //// 969 970 /** 971 * Translate all coordinates given in this hint item. Include the 972 * memorized head and tail location as well as all bend points. 973 * 974 * @param x amount on the x-axis to translate 975 * @param y amount on the x-axis to translate 976 */ 977 private void _translate(double x, double y) { 978 _headLocation.x += x; 979 _headLocation.y += y; 980 _tailLocation.x += x; 981 _tailLocation.y += y; 982 // iterate all bend points, make sure that in a faulty odd 983 // array, the last entry is ignored 984 for (int i = 0; i < _bendPoints.length - 1; i += 2) { 985 _bendPoints[i] += x; 986 _bendPoints[i + 1] += y; 987 } 988 if (_labelLocation != null) { 989 _labelLocation.x += x; 990 _labelLocation.y += y; 991 } 992 } 993 994 /** 995 * Update the layout information for head and tail of this hint that is 996 * required to check validity. This is the location and channel width 997 * get read from the current model objects. 998 */ 999 private void _updateHeadTailLocations() { 1000 _headLocation = _getEndpointLocation(_head); 1001 _tailLocation = _getEndpointLocation(_tail); 1002 _updateChannelIndex(_head, _headMultiportIndex); 1003 _updateChannelIndex(_tail, _tailMultiportIndex); 1004 } 1005 1006 /** 1007 * Get the width of a channel corresponding to a port. If no 1008 * {@link IOPort} is passed, return 0. 1009 * 1010 * @param port port for which to determine the channel width 1011 * @return channel width if applicable, else 0 1012 */ 1013 private static int _getChannelWidth(Object port) { 1014 if (port instanceof IOPort) { 1015 return ((IOPort) port).numLinks(); 1016 // return ((IOPort)port).getWidth(); 1017 } 1018 return 0; 1019 } 1020 1021 /** 1022 * Get a String name of an object that is sufficient to identify it on 1023 * one level of hierarchy. The name will be the name of the object 1024 * relative to some parent. E.g. a port "input" of a "Discard" actor 1025 * will result in the name "Discard.input". 1026 * 1027 * @param obj The object for which the name should be obtained 1028 * @return a String representation that is sufficient to identify the 1029 * object 1030 */ 1031 private static String _getName(NamedObj obj) { 1032 NamedObj parent = obj.getContainer(); 1033 if ((obj instanceof Port || obj instanceof Vertex 1034 || obj instanceof Attribute) && parent != null) { 1035 parent = parent.getContainer(); 1036 } 1037 return obj.getName(parent); 1038 } 1039 1040 /** 1041 * Determine the correct location of a link endpoint. 1042 * 1043 * @param obj an endpoint of a link 1044 * @return the location for the given endpoint 1045 */ 1046 private static Point2D.Double _getEndpointLocation(NamedObj obj) { 1047 // In case of a component port, the endpoint is the external port of 1048 // a composite actor, so take the actor's position instead of the port's 1049 // internal position. 1050 if (obj instanceof ComponentPort) { 1051 return (Point2D.Double) KielerLayoutUtil 1052 .getLocationPoint(obj.getContainer()); 1053 } 1054 return (Point2D.Double) KielerLayoutUtil.getLocationPoint(obj); 1055 } 1056 1057 /** 1058 * Update the channel width of a given object. For an IOPort, the actual 1059 * channel width is obtained and otherwise set it to default 1. 1060 * 1061 * @param port The port for which the update is required, may also be 1062 * some other type than port, e.g. a Relation 1063 * @param indexLocation array with at least 2 entries in where to store 1064 * the index locations 1065 */ 1066 private static void _updateChannelIndex(Object port, 1067 int[] indexLocation) { 1068 int width = 1; 1069 int index = 1; // currently the index is ignored 1070 if (port instanceof IOPort) { 1071 width = ((IOPort) port).numLinks(); 1072 } 1073 indexLocation[0] = index; 1074 indexLocation[1] = width; 1075 } 1076 1077 /** 1078 * Local storage of bend points, where every 2 doubles form x and y 1079 * coordinates. 1080 */ 1081 private double[] _bendPoints = {}; 1082 /** Head object to identify this item. */ 1083 private NamedObj _head = null; 1084 /** Coordinates for the head at which this item is only valid. */ 1085 private Point2D.Double _headLocation = new Point2D.Double(); 1086 /** Width and index of the multiport, if the head actually is a multiport. */ 1087 private int[] _headMultiportIndex = { 1, 1 }; 1088 /** Tail object to identify this item. */ 1089 private NamedObj _tail = null; 1090 /** Coordinates for the tail at which this item is only valid. */ 1091 private Point2D.Double _tailLocation = new Point2D.Double(); 1092 /** Width and index of the multiport, if the tail actually is a multiport. */ 1093 private int[] _tailMultiportIndex = { 1, 1 }; 1094 /** The position of a label if it exists. */ 1095 private Point2D.Double _labelLocation = null; 1096 } 1097}