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