001/* An icon whose description is represented in SVG 002 003 Copyright (c) 2003-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.icon; 029 030import java.io.Reader; 031import java.io.StringReader; 032import java.net.URL; 033import java.util.Iterator; 034import java.util.Map; 035 036import diva.util.xml.XmlDocument; 037import diva.util.xml.XmlElement; 038import diva.util.xml.XmlReader; 039import ptolemy.kernel.util.Attribute; 040import ptolemy.kernel.util.ConfigurableAttribute; 041import ptolemy.kernel.util.IllegalActionException; 042import ptolemy.kernel.util.InternalErrorException; 043import ptolemy.kernel.util.KernelException; 044import ptolemy.kernel.util.Location; 045import ptolemy.kernel.util.NameDuplicationException; 046import ptolemy.kernel.util.Nameable; 047import ptolemy.kernel.util.NamedObj; 048import ptolemy.kernel.util.Settable; 049import ptolemy.kernel.util.ValueListener; 050import ptolemy.kernel.util.Workspace; 051import ptolemy.vergil.kernel.attributes.EllipseAttribute; 052import ptolemy.vergil.kernel.attributes.FilledShapeAttribute; 053import ptolemy.vergil.kernel.attributes.RectangleAttribute; 054import ptolemy.vergil.kernel.attributes.ShapeAttribute; 055 056/////////////////////////////////////////////////////////////////// 057//// SVGIcon 058 059/** 060 This class is intended to eventually replace XMLIcon, however, 061 the current version doesn't work very well, so it isn't used. 062 063 @author Edward A. Lee 064 @version $Id$ 065 @since Ptolemy II 4.0 066 @Pt.ProposedRating Yellow (eal) 067 @Pt.AcceptedRating Red (johnr) 068 */ 069public class SVGIcon extends EditorIcon implements ValueListener { 070 /** Construct an icon in the specified workspace and name. 071 * This constructor is typically used in conjunction with 072 * setContainerToBe() and createFigure() to create an icon 073 * and generate a figure without having to have write access 074 * to the workspace. 075 * If the workspace argument is null, then use the default workspace. 076 * The object is added to the directory of the workspace. 077 * @see #setContainerToBe(NamedObj) 078 * Increment the version number of the workspace. 079 * @param workspace The workspace that will list the attribute. 080 * @param name The name of this attribute. 081 * @exception IllegalActionException If the specified name contains 082 * a period. 083 */ 084 public SVGIcon(Workspace workspace, String name) 085 throws IllegalActionException { 086 super(workspace, name); 087 088 try { 089 setName(name); 090 } catch (NameDuplicationException ex) { 091 throw new InternalErrorException(ex); 092 } 093 } 094 095 /** Create a new icon with the given name in the given container. 096 * By default, the icon contains no graphic objects. 097 * @param container The container for this attribute. 098 * @param name The name of this attribute. 099 * @exception IllegalActionException If thrown by the parent 100 * class or while setting an attribute. 101 * @exception NameDuplicationException If the name coincides with 102 * an attribute already in the container. 103 */ 104 public SVGIcon(NamedObj container, String name) 105 throws NameDuplicationException, IllegalActionException { 106 super(container, name); 107 super.setContainer(container); 108 } 109 110 /////////////////////////////////////////////////////////////////// 111 //// public methods //// 112 113 /** Clone the object into the specified workspace. The new object is 114 * <i>not</i> added to the directory of that workspace (you must do this 115 * yourself if you want it there). 116 * The result is an object with no container. 117 * @param workspace The workspace for the cloned object. 118 * @exception CloneNotSupportedException Not thrown in this base class 119 * @return The new Attribute. 120 */ 121 @Override 122 public Object clone(Workspace workspace) throws CloneNotSupportedException { 123 SVGIcon newObject = (SVGIcon) super.clone(workspace); 124 newObject._description = null; 125 newObject._smallIconDescription = null; 126 return newObject; 127 } 128 129 /** Override the base class to establish this as a listener to 130 * icon descriptions in the container. 131 * @param container The container to attach this attribute to.. 132 * @exception IllegalActionException If this attribute is not of the 133 * expected class for the container, or it has no name, 134 * or the attribute and container are not in the same workspace, or 135 * the proposed container would result in recursive containment. 136 * @exception NameDuplicationException If the container already has 137 * an attribute with the name of this attribute. 138 */ 139 @Override 140 public void setContainer(NamedObj container) 141 throws IllegalActionException, NameDuplicationException { 142 super.setContainer(container); 143 _bindToContainer(container); 144 } 145 146 /** Indicate that the container of this icon will eventually 147 * be the specified object. This rather specialized method is 148 * used to create an icon and generate a figure without having 149 * to have write access to the workspace. To use it, use the 150 * constructor that takes a workspace and a name, then call 151 * this method to indicate what the container will be. You 152 * can then call createFigure() or createBackgroundFigure(), 153 * and the appropriate figure for the container specified here 154 * will be used. Then queue a ChangeRequest that sets the 155 * container to the same specified container. Once the container 156 * has been set by calling setContainer(), then the object 157 * specified to this method is no longer relevant. 158 * @param container The container that will eventually be set. 159 * @see #getContainerOrContainerToBe() 160 */ 161 @Override 162 public void setContainerToBe(NamedObj container) { 163 super.setContainerToBe(container); 164 _bindToContainer(container); 165 } 166 167 /** React to the fact that the value of an attribute named 168 * "_iconDescription" contained by the same container has changed 169 * value by redrawing the figure. 170 * @param settable The object that has changed value. 171 */ 172 @Override 173 public void valueChanged(Settable settable) { 174 String name = ((Nameable) settable).getName(); 175 176 if (name.equals("_iconDescription") 177 || name.equals("_smallIconDescription")) { 178 _recreateFigure(); 179 180 try { 181 _updateContents(); 182 } catch (Exception ex) { 183 // Regrettable, but how else to inform of error? 184 throw new InternalErrorException(ex); 185 } 186 } 187 } 188 189 /////////////////////////////////////////////////////////////////// 190 //// private methods //// 191 192 /** Establish this icon as a listener for changes in attributes 193 * named "_iconDescription" and "_smallIconDescription" in the 194 * specified container. 195 */ 196 private void _bindToContainer(NamedObj container) { 197 // Get the description. 198 ConfigurableAttribute description = (ConfigurableAttribute) container 199 .getAttribute("_iconDescription"); 200 201 // If the description has changed... 202 if (_description != description) { 203 if (_description != null) { 204 // Remove this as a listener if there 205 // was a previous description. 206 _description.removeValueListener(this); 207 } 208 209 // update the description. 210 _description = description; 211 212 if (_description != null) { 213 // Listen for changes in value to the icon description. 214 _description.addValueListener(this); 215 } 216 } 217 218 // Get the icon description. 219 description = (ConfigurableAttribute) container 220 .getAttribute("_smallIconDescription"); 221 222 // If the description has changed... 223 if (_smallIconDescription != description) { 224 if (_smallIconDescription != null) { 225 // Remove this as a listener if there 226 // was a previous description. 227 _smallIconDescription.removeValueListener(this); 228 } 229 230 // update the description. 231 _smallIconDescription = description; 232 233 if (_smallIconDescription != null) { 234 // Listen for changes in value to the icon description. 235 _smallIconDescription.addValueListener(this); 236 } 237 } 238 239 try { 240 _updateContents(); 241 } catch (Exception ex) { 242 // Regrettable, but how else to inform of error? 243 throw new InternalErrorException(ex); 244 } 245 246 // clear the caches 247 _recreateFigure(); 248 } 249 250 /** Create a new attribute and insert it in this icon. This method 251 * must be called at a time when this thread can get write access 252 * on the workspace. For example, it is safe to call it from 253 * within a change request, or from within attributeChanged(), or 254 * from within notification of change to a parameter. The first 255 * argument is a string representation of the SVG element type, 256 * the second is a hashtable containing attributes of the object, 257 * and the third is the PC data within the element, if there is 258 * any. Any attributes that are not recognized will be ignored. 259 */ 260 private void _createAttribute(String type, Map attributes, String content) { 261 try { 262 if (type.equals("rect")) { 263 RectangleAttribute attribute = new RectangleAttribute(this, 264 uniqueName("rect")); 265 266 _processFilledShapeAttributeAttributes(attribute, attributes); 267 } else if (type.equals("circle")) { 268 EllipseAttribute attribute = new EllipseAttribute(this, 269 uniqueName("rect")); 270 271 // Rename the attributes. 272 attributes.put("x", _getAttribute(attributes, "cx", "0.0")); 273 attributes.put("y", _getAttribute(attributes, "cy", "0.0")); 274 275 double r = _getDouble(attributes, "r", 10.0); 276 double width = r * 2.0; 277 String widthString = Double.toString(width); 278 attributes.put("width", widthString); 279 attributes.put("height", widthString); 280 281 _processFilledShapeAttributeAttributes(attribute, attributes); 282 283 /* FIXME 284 } else if (type.equals("ellipse")) { 285 double cx, cy, rx, ry; 286 cx = _getDouble(attributes, "cx", 0); 287 cy = _getDouble(attributes, "cy", 0); 288 rx = _getDouble(attributes, "rx"); 289 ry = _getDouble(attributes, "ry"); 290 291 PaintedShape ps = new PaintedShape(new Ellipse2D.Double( 292 cx - rx, cy - ry, 2 * rx, 2 * ry)); 293 processPaintedShapeAttributes(ps, attributes); 294 return ps; 295 296 } else if (type.equals("line")) { 297 double x1, y1, x2, y2; 298 x1 = _getDouble(attributes, "x1", 0); 299 y1 = _getDouble(attributes, "y1", 0); 300 x2 = _getDouble(attributes, "x2", 0); 301 y2 = _getDouble(attributes, "y2", 0); 302 Line2D line = new Line2D.Double(x1, y1, x2, y2); 303 PaintedPath pp = new PaintedPath(line); 304 processPaintedPathAttributes(pp, attributes); 305 return pp; 306 } else if (type.equals("polyline")) { 307 double coords[] = 308 parseCoordString((String)attributes.get("points")); 309 Polyline2D poly = new Polyline2D.Double(); 310 poly.moveTo(coords[0], coords[1]); 311 for (int i = 2; i < coords.length; i += 2) { 312 poly.lineTo(coords[i], coords[i+1]); 313 } 314 PaintedPath pp = new PaintedPath(poly); 315 processPaintedPathAttributes(pp, attributes); 316 return pp; 317 } else if (type.equals("polygon")) { 318 double coords[] = 319 parseCoordString((String)attributes.get("points")); 320 Polygon2D poly = new Polygon2D.Double(); 321 poly.moveTo(coords[0], coords[1]); 322 for (int i = 2; i < coords.length; i += 2) { 323 poly.lineTo(coords[i], coords[i+1]); 324 } 325 poly.closePath(); 326 327 PaintedShape ps = new PaintedShape(poly); 328 processPaintedShapeAttributes(ps, attributes); 329 return ps; 330 331 } else if (type.equals("text")) { 332 double x, y; 333 x = _getDouble(attributes, "x", 0); 334 y = _getDouble(attributes, "y", 0); 335 PaintedString string = new PaintedString(content); 336 processPaintedStringAttributes(string, attributes); 337 string.translate(x, y); 338 return string; 339 } else if (type.equals("image")) { 340 double x, y, width, height; 341 x = _getDouble(attributes, "x", 0); 342 y = _getDouble(attributes, "y", 0); 343 width = _getDouble(attributes, "width"); 344 height = _getDouble(attributes, "height"); 345 Rectangle2D bounds = new Rectangle2D.Double(x, y, width, height); 346 String link = (String)attributes.get("xlink:href"); 347 // First try as a system resource. 348 URL url = ClassLoader.getSystemResource(link); 349 try { 350 if (url == null) { 351 // Web Start needs this. 352 if (_refClass == null) { 353 try { 354 _refClass = 355 Class.forName("diva.canvas.toolbox.SVGParser"); 356 } catch (ClassNotFoundException ex) { 357 throw new RuntimeException("Could not find " + 358 "diva.canvas.toolbox.SVGParser"); 359 } 360 } 361 url = _refClass.getClassLoader().getResource(link); 362 } 363 364 // Try as a regular URL. 365 if (url == null) { 366 url = new URL(link); 367 } 368 Toolkit tk = Toolkit.getDefaultToolkit(); 369 Image img = tk.getImage(url); 370 PaintedImage image = new PaintedImage(img, bounds); 371 // Wait until the image has been completely loaded, 372 // unless an error occurred. 373 while (true) { 374 if (tk.prepareImage(img, -1, -1, image)) { 375 // The image was fully prepared, so return the 376 // created image. 377 break; 378 } 379 int bitflags = tk.checkImage(img, -1, -1, image); 380 if ((bitflags & 381 (ImageObserver.ABORT | ImageObserver.ERROR)) != 0) { 382 // There was an error if either flag is set, 383 // so return null. 384 return null; 385 } 386 Thread.yield(); 387 } 388 return image; 389 } catch (java.net.MalformedURLException ex) { 390 return null; 391 } 392 */ 393 } 394 } catch (KernelException e) { 395 throw new InternalErrorException(e); 396 } 397 } 398 399 /** Given the root of an XML tree, populate this icon with 400 * attributes for each graphical element. 401 */ 402 private void _generateContents(XmlElement root) { 403 String name = root.getType(); 404 405 if (!name.equals("svg")) { 406 throw new IllegalArgumentException("Input XML has a root" 407 + "name which is '" + name + "' instead of 'svg':" + root); 408 } 409 410 Iterator children = root.elements(); 411 412 while (children.hasNext()) { 413 XmlElement child = (XmlElement) children.next(); 414 _createAttribute(child.getType(), child.getAttributeMap(), 415 child.getPCData()); 416 } 417 } 418 419 /** Extract the named attribute from the attribute map and 420 * return the value as a string. If the named attribute is not 421 * present, then return the default. 422 * @param map The attribute map. 423 * @param name The element name. 424 * @param defaultValue The default value. 425 * @return The double specified by this attribute. 426 */ 427 private static String _getAttribute(Map map, String name, 428 String defaultValue) { 429 if (map.containsKey(name)) { 430 return (String) map.get(name); 431 } else { 432 return defaultValue; 433 } 434 } 435 436 /** Extract the named attribute from the attribute map and 437 * return the value as a double. If the named attribute is not 438 * present, then return the default. 439 * @param map The attribute map. 440 * @param name The element name. 441 * @param defaultValue The default value. 442 * @return The double specified by this attribute. 443 */ 444 private static double _getDouble(Map map, String name, 445 double defaultValue) { 446 if (map.containsKey(name)) { 447 return Double.parseDouble((String) map.get(name)); 448 } else { 449 return defaultValue; 450 } 451 } 452 453 /** Set the attributes of a FilledShapeAttribute from the specified 454 * map of SVG attribute values. 455 */ 456 private static void _processFilledShapeAttributeAttributes( 457 FilledShapeAttribute attribute, Map attributes) { 458 _processShapeAttributeAttributes(attribute, attributes); 459 460 String width = _getAttribute(attributes, "width", "10.0"); 461 String height = _getAttribute(attributes, "height", "10.0"); 462 attribute.width.setExpression(width); 463 attribute.height.setExpression(height); 464 465 //String style = (String) attributes.get("style"); 466 467 //if (style != null) { 468 //StringTokenizer t = new StringTokenizer(style, ";"); 469 470 //while (t.hasMoreTokens()) { 471 //String string = t.nextToken().trim(); 472 //int index = string.indexOf(":"); 473 474 // String name = string.substring(0, index); 475 // String value = string.substring(index + 1); 476 477 /* FIXME: Figure out how to do this. See SVGParser. 478 if (name.equals("fill")) { 479 ps.fillPaint = lookupColor(value); 480 } else if (name.equals("stroke")) { 481 ps.strokePaint = lookupColor(value); 482 } else if (name.equals("stroke-width")) { 483 ps.setLineWidth(Float.parseFloat(value)); 484 } 485 */ 486 //} 487 //} 488 } 489 490 /** Set the attributes of a ShapeAttribute from the specified 491 * map of SVG attribute values. 492 */ 493 private static void _processShapeAttributeAttributes( 494 ShapeAttribute attribute, Map attributes) { 495 // FIXME: set lineWidth and lineColor. 496 _processLocation(attribute, attributes); 497 498 //String style = (String) attributes.get("style"); 499 500 //if (style != null) { 501 //StringTokenizer t = new StringTokenizer(style, ";"); 502 503 //while (t.hasMoreTokens()) { 504 //String string = t.nextToken().trim(); 505 //int index = string.indexOf(":"); 506 507 // String name = string.substring(0, index); 508 // String value = string.substring(index + 1); 509 510 /* FIXME: Figure out how to do this. See SVGParser. 511 if (name.equals("fill")) { 512 ps.fillPaint = lookupColor(value); 513 } else if (name.equals("stroke")) { 514 ps.strokePaint = lookupColor(value); 515 } else if (name.equals("stroke-width")) { 516 ps.setLineWidth(Float.parseFloat(value)); 517 } 518 */ 519 //} 520 //} 521 } 522 523 /** Set the location of an Attribute from the specified 524 * map of SVG attribute values. 525 */ 526 private static void _processLocation(Attribute attribute, Map attributes) { 527 double[] locationValue = new double[2]; 528 locationValue[0] = _getDouble(attributes, "x", 0.0); 529 locationValue[1] = _getDouble(attributes, "y", 0.0); 530 531 try { 532 Location location = new Location(attribute, "_location"); 533 location.setLocation(locationValue); 534 535 // Since this isn't delegated to the MoML parser, 536 // we have to handle propagation here. 537 location.propagateExistence(); 538 } catch (KernelException e) { 539 throw new InternalErrorException(e); 540 } 541 } 542 543 /** Update the contents of the icon based on the SVG data 544 * in the associated "_iconDescription" parameter, if there is one. 545 */ 546 private void _updateContents() throws Exception { 547 if (_description == null) { 548 return; 549 } 550 551 String text = _description.value(); 552 Reader in = new StringReader(text); 553 XmlDocument document = new XmlDocument((URL) null); 554 XmlReader reader = new XmlReader(); 555 reader.parse(document, in); 556 557 XmlElement root = document.getRoot(); 558 559 _generateContents(root); 560 561 // FIXME: What to do about the _smallIconDescription? 562 } 563 564 /////////////////////////////////////////////////////////////////// 565 //// private members //// 566 // The description of this icon in XML. 567 private ConfigurableAttribute _description; 568 569 // The description of the small version of the icon in XML. 570 private ConfigurableAttribute _smallIconDescription; 571}