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}