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    public boolean getStyle(StyleAttribute attrib, boolean recursive) throws SVGException
583    {
584        return getStyle(attrib, recursive, true);
585    }
586    
587    /**
588     * Copies the current style into the passed style attribute. Checks for
589     * inline styles first, then internal and extranal style sheets, and finally
590     * checks for presentation attributes. Recursively checks parents.
591     *
592     * @param attrib - Attribute to write style data to. Must have it's name set
593     * to the name of the style being queried.
594     * @param recursive - If true and this object does not contain the named
595     * style attribute, checks attributes of parents back to root until one
596     * found.
597     */
598    public boolean getStyle(StyleAttribute attrib, boolean recursive, boolean evalAnimation)
599            throws SVGException
600    {
601        String styName = attrib.getName();
602
603        //Check for local inline styles
604        StyleAttribute styAttr = (StyleAttribute)inlineStyles.get(styName);
605
606        attrib.setStringValue(styAttr == null ? "" : styAttr.getStringValue());
607
608        //Evalutate coresponding track, if one exists
609        if (evalAnimation)
610        {
611            TrackBase track = trackManager.getTrack(styName, AnimationElement.AT_CSS);
612            if (track != null)
613            {
614                track.getValue(attrib, diagram.getUniverse().getCurTime());
615                return true;
616            }
617        }
618
619        //Return if we've found a non animated style
620        if (styAttr != null)
621        {
622            return true;
623        }
624
625
626        //Check for presentation attribute
627        StyleAttribute presAttr = (StyleAttribute)presAttribs.get(styName);
628
629        attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
630
631        //Evalutate coresponding track, if one exists
632        if (evalAnimation)
633        {
634            TrackBase track = trackManager.getTrack(styName, AnimationElement.AT_XML);
635            if (track != null)
636            {
637                track.getValue(attrib, diagram.getUniverse().getCurTime());
638                return true;
639            }
640        }
641
642        //Return if we've found a presentation attribute instead
643        if (presAttr != null)
644        {
645            return true;
646        }
647
648        //Check for style sheet
649        SVGRoot root = getRoot();
650        if (root != null)
651        {
652            StyleSheet ss = root.getStyleSheet();
653            if (ss != null)
654            {
655                return ss.getStyle(attrib, getTagName(), cssClass);
656            }
657        }
658
659        //If we're recursive, check parents
660        if (recursive)
661        {
662            SVGElement parentContext = getParentContext();
663            if (parentContext != null)
664            {
665                return parentContext.getStyle(attrib, true);
666            }
667            if (parent != null)
668            {
669                return parent.getStyle(attrib, true);
670            }
671        }
672
673        //Unsuccessful reading style attribute
674        return false;
675    }
676
677    /**
678     * @return the raw style value of this attribute. Does not take the
679     * presentation value or animation into consideration. Used by animations to
680     * determine the base to animate from.
681     */
682    public StyleAttribute getStyleAbsolute(String styName)
683    {
684        //Check for local inline styles
685        return (StyleAttribute) inlineStyles.get(styName);
686    }
687
688    /**
689     * Copies the presentation attribute into the passed one.
690     *
691     * @return - True if attribute was read successfully
692     */
693    public boolean getPres(StyleAttribute attrib) throws SVGException
694    {
695        String presName = attrib.getName();
696
697        //Make sure we have a coresponding presentation attribute
698        StyleAttribute presAttr = (StyleAttribute) presAttribs.get(presName);
699
700        //Copy presentation value directly
701        attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
702
703        //Evalutate coresponding track, if one exists
704        TrackBase track = trackManager.getTrack(presName, AnimationElement.AT_XML);
705        if (track != null)
706        {
707            track.getValue(attrib, diagram.getUniverse().getCurTime());
708            return true;
709        }
710
711        //Return if we found presentation attribute
712        if (presAttr != null)
713        {
714            return true;
715        }
716
717        return false;
718    }
719
720    /**
721     * @return the raw presentation value of this attribute. Ignores any
722     * modifications applied by style attributes or animation. Used by
723     * animations to determine the starting point to animate from
724     */
725    public StyleAttribute getPresAbsolute(String styName)
726    {
727        //Check for local inline styles
728        return (StyleAttribute) presAttribs.get(styName);
729    }
730
731    static protected AffineTransform parseTransform(String val) throws SVGException
732    {
733        final Matcher matchExpression = Pattern.compile("\\w+\\([^)]*\\)").matcher("");
734
735        AffineTransform retXform = new AffineTransform();
736
737        matchExpression.reset(val);
738        while (matchExpression.find())
739        {
740            retXform.concatenate(parseSingleTransform(matchExpression.group()));
741        }
742
743        return retXform;
744    }
745
746    static public AffineTransform parseSingleTransform(String val) throws SVGException
747    {
748        final Matcher matchWord = Pattern.compile("[-.\\w]+").matcher("");
749
750        AffineTransform retXform = new AffineTransform();
751
752        matchWord.reset(val);
753        if (!matchWord.find())
754        {
755            //Return identity transformation if no data present (eg, empty string)
756            return retXform;
757        }
758
759        String function = matchWord.group().toLowerCase();
760
761        LinkedList termList = new LinkedList();
762        while (matchWord.find())
763        {
764            termList.add(matchWord.group());
765        }
766
767
768        double[] terms = new double[termList.size()];
769        Iterator it = termList.iterator();
770        int count = 0;
771        while (it.hasNext())
772        {
773            terms[count++] = XMLParseUtil.parseDouble((String) it.next());
774        }
775
776        //Calculate transformation
777        if (function.equals("matrix"))
778        {
779            retXform.setTransform(terms[0], terms[1], terms[2], terms[3], terms[4], terms[5]);
780        } else if (function.equals("translate"))
781        {
782            if (terms.length == 1)
783            {
784                retXform.setToTranslation(terms[0], 0);
785            } else
786            {
787                retXform.setToTranslation(terms[0], terms[1]);
788            }
789        } else if (function.equals("scale"))
790        {
791            if (terms.length > 1)
792            {
793                retXform.setToScale(terms[0], terms[1]);
794            } else
795            {
796                retXform.setToScale(terms[0], terms[0]);
797            }
798        } else if (function.equals("rotate"))
799        {
800            if (terms.length > 2)
801            {
802                retXform.setToRotation(Math.toRadians(terms[0]), terms[1], terms[2]);
803            } else
804            {
805                retXform.setToRotation(Math.toRadians(terms[0]));
806            }
807        } else if (function.equals("skewx"))
808        {
809            retXform.setToShear(Math.toRadians(terms[0]), 0.0);
810        } else if (function.equals("skewy"))
811        {
812            retXform.setToShear(0.0, Math.toRadians(terms[0]));
813        } else
814        {
815            throw new SVGException("Unknown transform type");
816        }
817
818        return retXform;
819    }
820
821    static protected float nextFloat(LinkedList l)
822    {
823        String s = (String) l.removeFirst();
824        return Float.parseFloat(s);
825    }
826
827    static protected PathCommand[] parsePathList(String list)
828    {
829        final Matcher matchPathCmd = Pattern.compile("([MmLlHhVvAaQqTtCcSsZz])|([-+]?((\\d*\\.\\d+)|(\\d+))([eE][-+]?\\d+)?)").matcher(list);
830
831        //Tokenize
832        LinkedList tokens = new LinkedList();
833        while (matchPathCmd.find())
834        {
835            tokens.addLast(matchPathCmd.group());
836        }
837
838
839        boolean defaultRelative = false;
840        LinkedList cmdList = new LinkedList();
841        char curCmd = 'Z';
842        while (tokens.size() != 0)
843        {
844            String curToken = (String) tokens.removeFirst();
845            char initChar = curToken.charAt(0);
846            if ((initChar >= 'A' && initChar <= 'Z') || (initChar >= 'a' && initChar <= 'z'))
847            {
848                curCmd = initChar;
849            } else
850            {
851                tokens.addFirst(curToken);
852            }
853
854            PathCommand cmd = null;
855
856            switch (curCmd)
857            {
858                case 'M':
859                    cmd = new MoveTo(false, nextFloat(tokens), nextFloat(tokens));
860                    curCmd = 'L';
861                    break;
862                case 'm':
863                    cmd = new MoveTo(true, nextFloat(tokens), nextFloat(tokens));
864                    curCmd = 'l';
865                    break;
866                case 'L':
867                    cmd = new LineTo(false, nextFloat(tokens), nextFloat(tokens));
868                    break;
869                case 'l':
870                    cmd = new LineTo(true, nextFloat(tokens), nextFloat(tokens));
871                    break;
872                case 'H':
873                    cmd = new Horizontal(false, nextFloat(tokens));
874                    break;
875                case 'h':
876                    cmd = new Horizontal(true, nextFloat(tokens));
877                    break;
878                case 'V':
879                    cmd = new Vertical(false, nextFloat(tokens));
880                    break;
881                case 'v':
882                    cmd = new Vertical(true, nextFloat(tokens));
883                    break;
884                case 'A':
885                    cmd = new Arc(false, nextFloat(tokens), nextFloat(tokens),
886                        nextFloat(tokens),
887                        nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
888                        nextFloat(tokens), nextFloat(tokens));
889                    break;
890                case 'a':
891                    cmd = new Arc(true, nextFloat(tokens), nextFloat(tokens),
892                        nextFloat(tokens),
893                        nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
894                        nextFloat(tokens), nextFloat(tokens));
895                    break;
896                case 'Q':
897                    cmd = new Quadratic(false, nextFloat(tokens), nextFloat(tokens),
898                        nextFloat(tokens), nextFloat(tokens));
899                    break;
900                case 'q':
901                    cmd = new Quadratic(true, nextFloat(tokens), nextFloat(tokens),
902                        nextFloat(tokens), nextFloat(tokens));
903                    break;
904                case 'T':
905                    cmd = new QuadraticSmooth(false, nextFloat(tokens), nextFloat(tokens));
906                    break;
907                case 't':
908                    cmd = new QuadraticSmooth(true, nextFloat(tokens), nextFloat(tokens));
909                    break;
910                case 'C':
911                    cmd = new Cubic(false, nextFloat(tokens), nextFloat(tokens),
912                        nextFloat(tokens), nextFloat(tokens),
913                        nextFloat(tokens), nextFloat(tokens));
914                    break;
915                case 'c':
916                    cmd = new Cubic(true, nextFloat(tokens), nextFloat(tokens),
917                        nextFloat(tokens), nextFloat(tokens),
918                        nextFloat(tokens), nextFloat(tokens));
919                    break;
920                case 'S':
921                    cmd = new CubicSmooth(false, nextFloat(tokens), nextFloat(tokens),
922                        nextFloat(tokens), nextFloat(tokens));
923                    break;
924                case 's':
925                    cmd = new CubicSmooth(true, nextFloat(tokens), nextFloat(tokens),
926                        nextFloat(tokens), nextFloat(tokens));
927                    break;
928                case 'Z':
929                case 'z':
930                    cmd = new Terminal();
931                    break;
932                default:
933                    throw new RuntimeException("Invalid path element");
934            }
935
936            cmdList.add(cmd);
937            defaultRelative = cmd.isRelative;
938        }
939
940        PathCommand[] retArr = new PathCommand[cmdList.size()];
941        cmdList.toArray(retArr);
942        return retArr;
943    }
944
945    static protected GeneralPath buildPath(String text, int windingRule)
946    {
947        PathCommand[] commands = parsePathList(text);
948
949        int numKnots = 2;
950        for (int i = 0; i < commands.length; i++)
951        {
952            numKnots += commands[i].getNumKnotsAdded();
953        }
954
955
956        GeneralPath path = new GeneralPath(windingRule, numKnots);
957
958        BuildHistory hist = new BuildHistory();
959
960        for (int i = 0; i < commands.length; i++)
961        {
962            PathCommand cmd = commands[i];
963            cmd.appendPath(path, hist);
964        }
965
966        return path;
967    }
968
969    /**
970     * Updates all attributes in this diagram associated with a time event. Ie,
971     * all attributes with track information.
972     *
973     * @return - true if this node has changed state as a result of the time
974     * update
975     */
976    abstract public boolean updateTime(double curTime) throws SVGException;
977
978    public int getNumChildren()
979    {
980        return children.size();
981    }
982
983    public SVGElement getChild(int i)
984    {
985        return (SVGElement) children.get(i);
986    }
987
988    public double lerp(double t0, double t1, double alpha)
989    {
990        return (1 - alpha) * t0 + alpha * t1;
991    }
992}