001/*
002 * SVG Salamander
003 * Copyright (c) 2004, Mark McKay
004 * All rights reserved.
005 *
006 * Redistribution and use in source and binary forms, with or 
007 * without modification, are permitted provided that the following
008 * conditions are met:
009 *
010 *   - Redistributions of source code must retain the above 
011 *     copyright notice, this list of conditions and the following
012 *     disclaimer.
013 *   - Redistributions in binary form must reproduce the above
014 *     copyright notice, this list of conditions and the following
015 *     disclaimer in the documentation and/or other materials 
016 *     provided with the distribution.
017 *
018 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
019 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
020 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
021 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
022 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
023 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
025 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
026 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
027 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
028 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
029 * OF THE POSSIBILITY OF SUCH DAMAGE. 
030 * 
031 * Mark McKay can be contacted at mark@kitfox.com.  Salamander and other
032 * projects can be found at http://www.kitfox.com
033 *
034 * Created on January 26, 2004, 1:59 AM
035 */
036package com.kitfox.svg;
037
038import com.kitfox.svg.animation.AnimationElement;
039import com.kitfox.svg.animation.TrackBase;
040import com.kitfox.svg.animation.TrackManager;
041import com.kitfox.svg.pathcmd.Arc;
042import com.kitfox.svg.pathcmd.BuildHistory;
043import com.kitfox.svg.pathcmd.Cubic;
044import com.kitfox.svg.pathcmd.CubicSmooth;
045import com.kitfox.svg.pathcmd.Horizontal;
046import com.kitfox.svg.pathcmd.LineTo;
047import com.kitfox.svg.pathcmd.MoveTo;
048import com.kitfox.svg.pathcmd.PathCommand;
049import com.kitfox.svg.pathcmd.Quadratic;
050import com.kitfox.svg.pathcmd.QuadraticSmooth;
051import com.kitfox.svg.pathcmd.Terminal;
052import com.kitfox.svg.pathcmd.Vertical;
053import com.kitfox.svg.xml.StyleAttribute;
054import com.kitfox.svg.xml.StyleSheet;
055import com.kitfox.svg.xml.XMLParseUtil;
056import java.awt.geom.AffineTransform;
057import java.awt.geom.GeneralPath;
058import java.io.Serializable;
059import java.net.URI;
060import java.util.ArrayList;
061import java.util.Collections;
062import java.util.HashMap;
063import java.util.HashSet;
064import java.util.Iterator;
065import java.util.LinkedList;
066import java.util.List;
067import java.util.Set;
068import java.util.regex.Matcher;
069import java.util.regex.Pattern;
070import org.xml.sax.Attributes;
071import org.xml.sax.SAXException;
072
073
074/**
075 * @author Mark McKay
076 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
077 */
078abstract public class SVGElement implements Serializable
079{
080
081    public static final long serialVersionUID = 0;
082    public static final String SVG_NS = "http://www.w3.org/2000/svg";
083    protected SVGElement parent = null;
084    protected final ArrayList children = new ArrayList();
085    protected String id = null;
086    /**
087     * CSS class. Used for applying style sheet information.
088     */
089    protected String cssClass = null;
090    /**
091     * Styles defined for this elemnt via the <b>style</b> attribute.
092     */
093    protected final HashMap inlineStyles = new HashMap();
094    /**
095     * Presentation attributes set for this element. Ie, any attribute other
096     * than the <b>style</b> attribute.
097     */
098    protected final HashMap presAttribs = new HashMap();
099    /**
100     * A list of presentation attributes to not include in the presentation
101     * attribute set.
102     */
103    protected static final Set ignorePresAttrib;
104
105    static
106    {
107        HashSet set = new HashSet();
108//        set.add("id");
109//        set.add("class");
110//        set.add("style");
111//        set.add("xml:base");
112
113        ignorePresAttrib = Collections.unmodifiableSet(set);
114    }
115    /**
116     * This element may override the URI we resolve against with an xml:base
117     * attribute. If so, a copy is placed here. Otherwise, we defer to our
118     * parent for the reolution base
119     */
120    protected URI xmlBase = null;
121    /**
122     * The diagram this element belongs to
123     */
124    protected SVGDiagram diagram;
125    /**
126     * Link to the universe we reside in
127     */
128    protected final TrackManager trackManager = new TrackManager();
129    boolean dirty = true;
130
131    /**
132     * Creates a new instance of SVGElement
133     */
134    public SVGElement()
135    {
136        this(null, null, null);
137    }
138
139    public SVGElement(String id, SVGElement parent)
140    {
141        this(id, null, parent);
142    }
143
144    public SVGElement(String id, String cssClass, SVGElement parent)
145    {
146        this.id = id;
147        this.cssClass = cssClass;
148        this.parent = parent;
149    }
150
151    abstract public String getTagName();
152
153    public SVGElement getParent()
154    {
155        return parent;
156    }
157
158    void setParent(SVGElement parent)
159    {
160        this.parent = parent;
161    }
162
163    /**
164     * @return an ordered list of nodes from the root of the tree to this node
165     */
166    public List getPath(List retVec)
167    {
168        if (retVec == null)
169        {
170            retVec = new ArrayList();
171        }
172
173        if (parent != null)
174        {
175            parent.getPath(retVec);
176        }
177        retVec.add(this);
178
179        return retVec;
180    }
181
182    /**
183     * @param retVec - A list to add all children to. If null, a new list is
184     * created and children of this group are added.
185     *
186     * @return The list containing the children of this group
187     */
188    public List getChildren(List retVec)
189    {
190        if (retVec == null)
191        {
192            retVec = new ArrayList();
193        }
194
195        retVec.addAll(children);
196
197        return retVec;
198    }
199
200    /**
201     * @param id - Id of svg element to return
202     * @return the child of the given id, or null if no such child exists.
203     */
204    public SVGElement getChild(String id)
205    {
206        for (Iterator it = children.iterator(); it.hasNext();)
207        {
208            SVGElement ele = (SVGElement) it.next();
209            String eleId = ele.getId();
210            if (eleId != null && eleId.equals(id))
211            {
212                return ele;
213            }
214        }
215
216        return null;
217    }
218
219    /**
220     * Searches children for given element. If found, returns index of child.
221     * Otherwise returns -1.
222     */
223    public int indexOfChild(SVGElement child)
224    {
225        return children.indexOf(child);
226    }
227
228    /**
229     * Swaps 2 elements in children.
230     *
231     * @i index of first
232     * @j index of second
233     *
234     * @return true if successful, false otherwise
235     */
236    public void swapChildren(int i, int j) throws SVGException
237    {
238        if ((children == null) || (i < 0) || (i >= children.size()) || (j < 0) || (j >= children.size()))
239        {
240            return;
241        }
242
243        Object temp = children.get(i);
244        children.set(i, children.get(j));
245        children.set(j, temp);
246        build();
247    }
248
249    /**
250     * Called during SAX load process to notify that this tag has begun the
251     * process of being loaded
252     *
253     * @param attrs - Attributes of this tag
254     * @param helper - An object passed to all SVG elements involved in this
255     * build process to aid in sharing information.
256     */
257    public void loaderStartElement(SVGLoaderHelper helper, Attributes attrs, SVGElement parent) throws SAXException
258    {
259        //Set identification info
260        this.parent = parent;
261        this.diagram = helper.diagram;
262
263        this.id = attrs.getValue("id");
264        if (this.id != null && !this.id.equals(""))
265        {
266            diagram.setElement(this.id, this);
267        }
268
269        String className = attrs.getValue("class");
270        this.cssClass = (className == null || className.equals("")) ? null : className;
271        //docRoot = helper.docRoot;
272        //universe = helper.universe;
273
274        //Parse style string, if any
275        String style = attrs.getValue("style");
276        if (style != null)
277        {
278            HashMap map = XMLParseUtil.parseStyle(style, inlineStyles);
279        }
280
281        String base = attrs.getValue("xml:base");
282        if (base != null && !base.equals(""))
283        {
284            try
285            {
286                xmlBase = new URI(base);
287            } catch (Exception e)
288            {
289                throw new SAXException(e);
290            }
291        }
292
293        //Place all other attributes into the presentation attribute list
294        int numAttrs = attrs.getLength();
295        for (int i = 0; i < numAttrs; i++)
296        {
297            String name = attrs.getQName(i);
298            if (ignorePresAttrib.contains(name))
299            {
300                continue;
301            }
302            String value = attrs.getValue(i);
303
304            presAttribs.put(name, new StyleAttribute(name, value));
305        }
306    }
307
308    public void removeAttribute(String name, int attribType)
309    {
310        switch (attribType)
311        {
312            case AnimationElement.AT_CSS:
313                inlineStyles.remove(name);
314                return;
315            case AnimationElement.AT_XML:
316                presAttribs.remove(name);
317                return;
318        }
319    }
320
321    public void addAttribute(String name, int attribType, String value) throws SVGElementException
322    {
323        if (hasAttribute(name, attribType))
324        {
325            throw new SVGElementException(this, "Attribute " + name + "(" + AnimationElement.animationElementToString(attribType) + ") already exists");
326        }
327
328        //Alter layout for id attribute
329        if ("id".equals(name))
330        {
331            if (diagram != null)
332            {
333                diagram.removeElement(id);
334                diagram.setElement(value, this);
335            }
336            this.id = value;
337        }
338
339        switch (attribType)
340        {
341            case AnimationElement.AT_CSS:
342                inlineStyles.put(name, new StyleAttribute(name, value));
343                return;
344            case AnimationElement.AT_XML:
345                presAttribs.put(name, new StyleAttribute(name, value));
346                return;
347        }
348
349        throw new SVGElementException(this, "Invalid attribute type " + attribType);
350    }
351
352    public boolean hasAttribute(String name, int attribType) throws SVGElementException
353    {
354        switch (attribType)
355        {
356            case AnimationElement.AT_CSS:
357                return inlineStyles.containsKey(name);
358            case AnimationElement.AT_XML:
359                return presAttribs.containsKey(name);
360            case AnimationElement.AT_AUTO:
361                return inlineStyles.containsKey(name) || presAttribs.containsKey(name);
362        }
363
364        throw new SVGElementException(this, "Invalid attribute type " + attribType);
365    }
366
367    /**
368     * @return a set of Strings that corespond to CSS attributes on this element
369     */
370    public Set getInlineAttributes()
371    {
372        return inlineStyles.keySet();
373    }
374
375    /**
376     * @return a set of Strings that corespond to XML attributes on this element
377     */
378    public Set getPresentationAttributes()
379    {
380        return presAttribs.keySet();
381    }
382
383    /**
384     * Called after the start element but before the end element to indicate
385     * each child tag that has been processed
386     */
387    public void loaderAddChild(SVGLoaderHelper helper, SVGElement child) throws SVGElementException
388    {
389        children.add(child);
390        child.parent = this;
391        child.setDiagram(diagram);
392
393        //Add info to track if we've scanned animation element
394        if (child instanceof AnimationElement)
395        {
396            trackManager.addTrackElement((AnimationElement) child);
397        }
398    }
399
400    protected void setDiagram(SVGDiagram diagram)
401    {
402        this.diagram = diagram;
403        diagram.setElement(id, this);
404        for (Iterator it = children.iterator(); it.hasNext();)
405        {
406            SVGElement ele = (SVGElement) it.next();
407            ele.setDiagram(diagram);
408        }
409    }
410
411    public void removeChild(SVGElement child) throws SVGElementException
412    {
413        if (!children.contains(child))
414        {
415            throw new SVGElementException(this, "Element does not contain child " + child);
416        }
417
418        children.remove(child);
419    }
420
421    /**
422     * Called during load process to add text scanned within a tag
423     */
424    public void loaderAddText(SVGLoaderHelper helper, String text)
425    {
426    }
427
428    /**
429     * Called to indicate that this tag and the tags it contains have been
430     * completely processed, and that it should finish any load processes.
431     */
432    public void loaderEndElement(SVGLoaderHelper helper) throws SVGParseException
433    {
434//        try
435//        {
436//            build();
437//        }
438//        catch (SVGException se)
439//        {
440//            throw new SVGParseException(se);
441//        }
442    }
443
444    /**
445     * Called by internal processes to rebuild the geometry of this node from
446     * it's presentation attributes, style attributes and animated tracks.
447     */
448    protected void build() throws SVGException
449    {
450        StyleAttribute sty = new StyleAttribute();
451
452        if (getPres(sty.setName("id")))
453        {
454            String newId = sty.getStringValue();
455            if (!newId.equals(id))
456            {
457                diagram.removeElement(id);
458                id = newId;
459                diagram.setElement(this.id, this);
460            }
461        }
462        if (getPres(sty.setName("class")))
463        {
464            cssClass = sty.getStringValue();
465        }
466        if (getPres(sty.setName("xml:base")))
467        {
468            xmlBase = sty.getURIValue();
469        }
470
471        //Build children
472        for (int i = 0; i < children.size(); ++i)
473        {
474            SVGElement ele = (SVGElement) children.get(i);
475            ele.build();
476        }
477    }
478
479    public URI getXMLBase()
480    {
481        return xmlBase != null ? xmlBase
482            : (parent != null ? parent.getXMLBase() : diagram.getXMLBase());
483    }
484
485    /**
486     * @return the id assigned to this node. Null if no id explicitly set.
487     */
488    public String getId()
489    {
490        return id;
491    }
492    LinkedList contexts = new LinkedList();
493
494    /**
495     * Hack to allow nodes to temporarily change their parents. The Use tag will
496     * need this so it can alter the attributes that a particular node uses.
497     */
498    protected void pushParentContext(SVGElement context)
499    {
500        contexts.addLast(context);
501    }
502
503    protected SVGElement popParentContext()
504    {
505        return (SVGElement) contexts.removeLast();
506    }
507
508    protected SVGElement getParentContext()
509    {
510        return contexts.isEmpty() ? null : (SVGElement) contexts.getLast();
511    }
512
513    public SVGRoot getRoot()
514    {
515        return parent == null ? null : parent.getRoot();
516    }
517
518    /*
519     * Returns the named style attribute.  Checks for inline styles first, then
520     * internal and extranal style sheets, and finally checks for presentation
521     * attributes.
522     * @param styleName - Name of attribute to return
523     * @param recursive - If true and this object does not contain the
524     * named style attribute, checks attributes of parents abck to root until
525     * one found.
526     */
527    public boolean getStyle(StyleAttribute attrib) throws SVGException
528    {
529        return getStyle(attrib, true);
530    }
531
532    public void setAttribute(String name, int attribType, String value) throws SVGElementException
533    {
534        StyleAttribute styAttr;
535
536
537        switch (attribType)
538        {
539            case AnimationElement.AT_CSS:
540            {
541                styAttr = (StyleAttribute) inlineStyles.get(name);
542                break;
543            }
544            case AnimationElement.AT_XML:
545            {
546                styAttr = (StyleAttribute) presAttribs.get(name);
547                break;
548            }
549            case AnimationElement.AT_AUTO:
550            {
551                styAttr = (StyleAttribute) inlineStyles.get(name);
552
553                if (styAttr == null)
554                {
555                    styAttr = (StyleAttribute) presAttribs.get(name);
556                }
557                break;
558            }
559            default:
560                throw new SVGElementException(this, "Invalid attribute type " + attribType);
561        }
562
563        if (styAttr == null)
564        {
565            throw new SVGElementException(this, "Could not find attribute " + name + "(" + AnimationElement.animationElementToString(attribType) + ").  Make sure to create attribute before setting it.");
566        }
567
568        //Alter layout for relevant attributes
569        if ("id".equals(styAttr.getName()))
570        {
571            if (diagram != null)
572            {
573                diagram.removeElement(this.id);
574                diagram.setElement(value, this);
575            }
576            this.id = value;
577        }
578
579        styAttr.setStringValue(value);
580    }
581
582    /**
583     * Copies the current style into the passed style attribute. Checks for
584     * inline styles first, then internal and extranal style sheets, and finally
585     * checks for presentation attributes. Recursively checks parents.
586     *
587     * @param attrib - Attribute to write style data to. Must have it's name set
588     * to the name of the style being queried.
589     * @param recursive - If true and this object does not contain the named
590     * style attribute, checks attributes of parents back to root until one
591     * found.
592     */
593    public boolean getStyle(StyleAttribute attrib, boolean recursive) throws SVGException
594    {
595        String styName = attrib.getName();
596
597        //Check for local inline styles
598        StyleAttribute styAttr = (StyleAttribute)inlineStyles.get(styName);
599
600        attrib.setStringValue(styAttr == null ? "" : styAttr.getStringValue());
601
602        //Evalutate coresponding track, if one exists
603        TrackBase track = trackManager.getTrack(styName, AnimationElement.AT_CSS);
604        if (track != null)
605        {
606            track.getValue(attrib, diagram.getUniverse().getCurTime());
607            return true;
608        }
609
610        //Return if we've found a non animated style
611        if (styAttr != null)
612        {
613            return true;
614        }
615
616
617        //Check for presentation attribute
618        StyleAttribute presAttr = (StyleAttribute)presAttribs.get(styName);
619
620        attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
621
622        //Evalutate coresponding track, if one exists
623        track = trackManager.getTrack(styName, AnimationElement.AT_XML);
624        if (track != null)
625        {
626            track.getValue(attrib, diagram.getUniverse().getCurTime());
627            return true;
628        }
629
630        //Return if we've found a presentation attribute instead
631        if (presAttr != null)
632        {
633            return true;
634        }
635
636        //Check for style sheet
637        SVGRoot root = getRoot();
638        if (root != null)
639        {
640            StyleSheet ss = root.getStyleSheet();
641            if (ss != null)
642            {
643                return ss.getStyle(attrib, getTagName(), cssClass);
644            }
645        }
646
647        //If we're recursive, check parents
648        if (recursive)
649        {
650            SVGElement parentContext = getParentContext();
651            if (parentContext != null)
652            {
653                return parentContext.getStyle(attrib, true);
654            }
655            if (parent != null)
656            {
657                return parent.getStyle(attrib, true);
658            }
659        }
660
661        //Unsuccessful reading style attribute
662        return false;
663    }
664
665    /**
666     * @return the raw style value of this attribute. Does not take the
667     * presentation value or animation into consideration. Used by animations to
668     * determine the base to animate from.
669     */
670    public StyleAttribute getStyleAbsolute(String styName)
671    {
672        //Check for local inline styles
673        return (StyleAttribute) inlineStyles.get(styName);
674    }
675
676    /**
677     * Copies the presentation attribute into the passed one.
678     *
679     * @return - True if attribute was read successfully
680     */
681    public boolean getPres(StyleAttribute attrib) throws SVGException
682    {
683        String presName = attrib.getName();
684
685        //Make sure we have a coresponding presentation attribute
686        StyleAttribute presAttr = (StyleAttribute) presAttribs.get(presName);
687
688        //Copy presentation value directly
689        attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
690
691        //Evalutate coresponding track, if one exists
692        TrackBase track = trackManager.getTrack(presName, AnimationElement.AT_XML);
693        if (track != null)
694        {
695            track.getValue(attrib, diagram.getUniverse().getCurTime());
696            return true;
697        }
698
699        //Return if we found presentation attribute
700        if (presAttr != null)
701        {
702            return true;
703        }
704
705        return false;
706    }
707
708    /**
709     * @return the raw presentation value of this attribute. Ignores any
710     * modifications applied by style attributes or animation. Used by
711     * animations to determine the starting point to animate from
712     */
713    public StyleAttribute getPresAbsolute(String styName)
714    {
715        //Check for local inline styles
716        return (StyleAttribute) presAttribs.get(styName);
717    }
718
719    static protected AffineTransform parseTransform(String val) throws SVGException
720    {
721        final Matcher matchExpression = Pattern.compile("\\w+\\([^)]*\\)").matcher("");
722
723        AffineTransform retXform = new AffineTransform();
724
725        matchExpression.reset(val);
726        while (matchExpression.find())
727        {
728            retXform.concatenate(parseSingleTransform(matchExpression.group()));
729        }
730
731        return retXform;
732    }
733
734    static public AffineTransform parseSingleTransform(String val) throws SVGException
735    {
736        final Matcher matchWord = Pattern.compile("[-.\\w]+").matcher("");
737
738        AffineTransform retXform = new AffineTransform();
739
740        matchWord.reset(val);
741        if (!matchWord.find())
742        {
743            //Return identity transformation if no data present (eg, empty string)
744            return retXform;
745        }
746
747        String function = matchWord.group().toLowerCase();
748
749        LinkedList termList = new LinkedList();
750        while (matchWord.find())
751        {
752            termList.add(matchWord.group());
753        }
754
755
756        double[] terms = new double[termList.size()];
757        Iterator it = termList.iterator();
758        int count = 0;
759        while (it.hasNext())
760        {
761            terms[count++] = XMLParseUtil.parseDouble((String) it.next());
762        }
763
764        //Calculate transformation
765        if (function.equals("matrix"))
766        {
767            retXform.setTransform(terms[0], terms[1], terms[2], terms[3], terms[4], terms[5]);
768        } else if (function.equals("translate"))
769        {
770            if (terms.length == 1)
771            {
772                retXform.setToTranslation(terms[0], 0);
773            } else
774            {
775                retXform.setToTranslation(terms[0], terms[1]);
776            }
777        } else if (function.equals("scale"))
778        {
779            if (terms.length > 1)
780            {
781                retXform.setToScale(terms[0], terms[1]);
782            } else
783            {
784                retXform.setToScale(terms[0], terms[0]);
785            }
786        } else if (function.equals("rotate"))
787        {
788            if (terms.length > 2)
789            {
790                retXform.setToRotation(Math.toRadians(terms[0]), terms[1], terms[2]);
791            } else
792            {
793                retXform.setToRotation(Math.toRadians(terms[0]));
794            }
795        } else if (function.equals("skewx"))
796        {
797            retXform.setToShear(Math.toRadians(terms[0]), 0.0);
798        } else if (function.equals("skewy"))
799        {
800            retXform.setToShear(0.0, Math.toRadians(terms[0]));
801        } else
802        {
803            throw new SVGException("Unknown transform type");
804        }
805
806        return retXform;
807    }
808
809    static protected float nextFloat(LinkedList l)
810    {
811        String s = (String) l.removeFirst();
812        return Float.parseFloat(s);
813    }
814
815    static protected PathCommand[] parsePathList(String list)
816    {
817        final Matcher matchPathCmd = Pattern.compile("([MmLlHhVvAaQqTtCcSsZz])|([-+]?((\\d*\\.\\d+)|(\\d+))([eE][-+]?\\d+)?)").matcher(list);
818
819        //Tokenize
820        LinkedList tokens = new LinkedList();
821        while (matchPathCmd.find())
822        {
823            tokens.addLast(matchPathCmd.group());
824        }
825
826
827        boolean defaultRelative = false;
828        LinkedList cmdList = new LinkedList();
829        char curCmd = 'Z';
830        while (tokens.size() != 0)
831        {
832            String curToken = (String) tokens.removeFirst();
833            char initChar = curToken.charAt(0);
834            if ((initChar >= 'A' && initChar <= 'Z') || (initChar >= 'a' && initChar <= 'z'))
835            {
836                curCmd = initChar;
837            } else
838            {
839                tokens.addFirst(curToken);
840            }
841
842            PathCommand cmd = null;
843
844            switch (curCmd)
845            {
846                case 'M':
847                    cmd = new MoveTo(false, nextFloat(tokens), nextFloat(tokens));
848                    curCmd = 'L';
849                    break;
850                case 'm':
851                    cmd = new MoveTo(true, nextFloat(tokens), nextFloat(tokens));
852                    curCmd = 'l';
853                    break;
854                case 'L':
855                    cmd = new LineTo(false, nextFloat(tokens), nextFloat(tokens));
856                    break;
857                case 'l':
858                    cmd = new LineTo(true, nextFloat(tokens), nextFloat(tokens));
859                    break;
860                case 'H':
861                    cmd = new Horizontal(false, nextFloat(tokens));
862                    break;
863                case 'h':
864                    cmd = new Horizontal(true, nextFloat(tokens));
865                    break;
866                case 'V':
867                    cmd = new Vertical(false, nextFloat(tokens));
868                    break;
869                case 'v':
870                    cmd = new Vertical(true, nextFloat(tokens));
871                    break;
872                case 'A':
873                    cmd = new Arc(false, nextFloat(tokens), nextFloat(tokens),
874                        nextFloat(tokens),
875                        nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
876                        nextFloat(tokens), nextFloat(tokens));
877                    break;
878                case 'a':
879                    cmd = new Arc(true, nextFloat(tokens), nextFloat(tokens),
880                        nextFloat(tokens),
881                        nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
882                        nextFloat(tokens), nextFloat(tokens));
883                    break;
884                case 'Q':
885                    cmd = new Quadratic(false, nextFloat(tokens), nextFloat(tokens),
886                        nextFloat(tokens), nextFloat(tokens));
887                    break;
888                case 'q':
889                    cmd = new Quadratic(true, nextFloat(tokens), nextFloat(tokens),
890                        nextFloat(tokens), nextFloat(tokens));
891                    break;
892                case 'T':
893                    cmd = new QuadraticSmooth(false, nextFloat(tokens), nextFloat(tokens));
894                    break;
895                case 't':
896                    cmd = new QuadraticSmooth(true, nextFloat(tokens), nextFloat(tokens));
897                    break;
898                case 'C':
899                    cmd = new Cubic(false, nextFloat(tokens), nextFloat(tokens),
900                        nextFloat(tokens), nextFloat(tokens),
901                        nextFloat(tokens), nextFloat(tokens));
902                    break;
903                case 'c':
904                    cmd = new Cubic(true, nextFloat(tokens), nextFloat(tokens),
905                        nextFloat(tokens), nextFloat(tokens),
906                        nextFloat(tokens), nextFloat(tokens));
907                    break;
908                case 'S':
909                    cmd = new CubicSmooth(false, nextFloat(tokens), nextFloat(tokens),
910                        nextFloat(tokens), nextFloat(tokens));
911                    break;
912                case 's':
913                    cmd = new CubicSmooth(true, nextFloat(tokens), nextFloat(tokens),
914                        nextFloat(tokens), nextFloat(tokens));
915                    break;
916                case 'Z':
917                case 'z':
918                    cmd = new Terminal();
919                    break;
920                default:
921                    throw new RuntimeException("Invalid path element");
922            }
923
924            cmdList.add(cmd);
925            defaultRelative = cmd.isRelative;
926        }
927
928        PathCommand[] retArr = new PathCommand[cmdList.size()];
929        cmdList.toArray(retArr);
930        return retArr;
931    }
932
933    static protected GeneralPath buildPath(String text, int windingRule)
934    {
935        PathCommand[] commands = parsePathList(text);
936
937        int numKnots = 2;
938        for (int i = 0; i < commands.length; i++)
939        {
940            numKnots += commands[i].getNumKnotsAdded();
941        }
942
943
944        GeneralPath path = new GeneralPath(windingRule, numKnots);
945
946        BuildHistory hist = new BuildHistory();
947
948        for (int i = 0; i < commands.length; i++)
949        {
950            PathCommand cmd = commands[i];
951            cmd.appendPath(path, hist);
952        }
953
954        return path;
955    }
956
957    /**
958     * Updates all attributes in this diagram associated with a time event. Ie,
959     * all attributes with track information.
960     *
961     * @return - true if this node has changed state as a result of the time
962     * update
963     */
964    abstract public boolean updateTime(double curTime) throws SVGException;
965
966    public int getNumChildren()
967    {
968        return children.size();
969    }
970
971    public SVGElement getChild(int i)
972    {
973        return (SVGElement) children.get(i);
974    }
975
976    public double lerp(double t0, double t1, double alpha)
977    {
978        return (1 - alpha) * t0 + alpha * t1;
979    }
980}