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<SVGElement> children = new ArrayList<SVGElement>();
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<String, StyleAttribute> inlineStyles = new HashMap<String, StyleAttribute>();
094    /**
095     * Presentation attributes set for this element. Ie, any attribute other
096     * than the <b>style</b> attribute.
097     */
098    protected final HashMap<String, StyleAttribute> presAttribs = new HashMap<String, StyleAttribute>();
099    /**
100     * A list of presentation attributes to not include in the presentation
101     * attribute set.
102     */
103    protected static final Set<String> ignorePresAttrib;
104
105    static
106    {
107        HashSet<String> set = new HashSet<String>();
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     * @param retVec
165     * @return an ordered list of nodes from the root of the tree to this node
166     */
167    public List<SVGElement> getPath(List<SVGElement> retVec)
168    {
169        if (retVec == null)
170        {
171            retVec = new ArrayList<SVGElement>();
172        }
173
174        if (parent != null)
175        {
176            parent.getPath(retVec);
177        }
178        retVec.add(this);
179
180        return retVec;
181    }
182
183    /**
184     * @param retVec - A list to add all children to. If null, a new list is
185     * created and children of this group are added.
186     *
187     * @return The list containing the children of this group
188     */
189    public List<SVGElement> getChildren(List<SVGElement> retVec)
190    {
191        if (retVec == null)
192        {
193            retVec = new ArrayList<SVGElement>();
194        }
195
196        retVec.addAll(children);
197
198        return retVec;
199    }
200
201    /**
202     * @param id - Id of svg element to return
203     * @return the child of the given id, or null if no such child exists.
204     */
205    public SVGElement getChild(String id)
206    {
207        for (SVGElement ele : children) {
208            String eleId = ele.getId();
209            if (eleId != null && eleId.equals(id))
210            {
211                return ele;
212            }
213        }
214
215        return null;
216    }
217
218    /**
219     * Searches children for given element. If found, returns index of child.
220     * Otherwise returns -1.
221     * @param child
222     * @return index of child
223     */
224    public int indexOfChild(SVGElement child)
225    {
226        return children.indexOf(child);
227    }
228
229    /**
230     * Swaps 2 elements in children.
231     *
232     * @param i index of first child
233     * @param j index of second child
234     * @throws com.kitfox.svg.SVGException
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        SVGElement 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     * @param parent
257     * @throws org.xml.sax.SAXException
258     */
259    public void loaderStartElement(SVGLoaderHelper helper, Attributes attrs, SVGElement parent) throws SAXException
260    {
261        //Set identification info
262        this.parent = parent;
263        this.diagram = helper.diagram;
264
265        this.id = attrs.getValue("id");
266        if (this.id != null && !this.id.equals(""))
267        {
268            diagram.setElement(this.id, this);
269        }
270
271        String className = attrs.getValue("class");
272        this.cssClass = (className == null || className.equals("")) ? null : className;
273        //docRoot = helper.docRoot;
274        //universe = helper.universe;
275
276        //Parse style string, if any
277        String style = attrs.getValue("style");
278        if (style != null)
279        {
280            HashMap<?, ?> map = XMLParseUtil.parseStyle(style, inlineStyles);
281        }
282
283        String base = attrs.getValue("xml:base");
284        if (base != null && !base.equals(""))
285        {
286            try
287            {
288                xmlBase = new URI(base);
289            } catch (Exception e)
290            {
291                throw new SAXException(e);
292            }
293        }
294
295        //Place all other attributes into the presentation attribute list
296        int numAttrs = attrs.getLength();
297        for (int i = 0; i < numAttrs; i++)
298        {
299            String name = attrs.getQName(i);
300            if (ignorePresAttrib.contains(name))
301            {
302                continue;
303            }
304            String value = attrs.getValue(i);
305
306            presAttribs.put(name, new StyleAttribute(name, value));
307        }
308    }
309
310    public void removeAttribute(String name, int attribType)
311    {
312        switch (attribType)
313        {
314            case AnimationElement.AT_CSS:
315                inlineStyles.remove(name);
316                return;
317            case AnimationElement.AT_XML:
318                presAttribs.remove(name);
319                return;
320        }
321    }
322
323    public void addAttribute(String name, int attribType, String value) throws SVGElementException
324    {
325        if (hasAttribute(name, attribType))
326        {
327            throw new SVGElementException(this, "Attribute " + name + "(" + AnimationElement.animationElementToString(attribType) + ") already exists");
328        }
329
330        //Alter layout for id attribute
331        if ("id".equals(name))
332        {
333            if (diagram != null)
334            {
335                diagram.removeElement(id);
336                diagram.setElement(value, this);
337            }
338            this.id = value;
339        }
340
341        switch (attribType)
342        {
343            case AnimationElement.AT_CSS:
344                inlineStyles.put(name, new StyleAttribute(name, value));
345                return;
346            case AnimationElement.AT_XML:
347                presAttribs.put(name, new StyleAttribute(name, value));
348                return;
349        }
350
351        throw new SVGElementException(this, "Invalid attribute type " + attribType);
352    }
353
354    public boolean hasAttribute(String name, int attribType) throws SVGElementException
355    {
356        switch (attribType)
357        {
358            case AnimationElement.AT_CSS:
359                return inlineStyles.containsKey(name);
360            case AnimationElement.AT_XML:
361                return presAttribs.containsKey(name);
362            case AnimationElement.AT_AUTO:
363                return inlineStyles.containsKey(name) || presAttribs.containsKey(name);
364        }
365
366        throw new SVGElementException(this, "Invalid attribute type " + attribType);
367    }
368
369    /**
370     * @return a set of Strings that corespond to CSS attributes on this element
371     */
372    public Set<String> getInlineAttributes()
373    {
374        return inlineStyles.keySet();
375    }
376
377    /**
378     * @return a set of Strings that corespond to XML attributes on this element
379     */
380    public Set<String> getPresentationAttributes()
381    {
382        return presAttribs.keySet();
383    }
384
385    /**
386     * Called after the start element but before the end element to indicate
387     * each child tag that has been processed
388     * @param helper
389     * @param child
390     * @throws com.kitfox.svg.SVGElementException
391     */
392    public void loaderAddChild(SVGLoaderHelper helper, SVGElement child) throws SVGElementException
393    {
394        children.add(child);
395        child.parent = this;
396        child.setDiagram(diagram);
397
398        //Add info to track if we've scanned animation element
399        if (child instanceof AnimationElement)
400        {
401            trackManager.addTrackElement((AnimationElement) child);
402        }
403    }
404
405    protected void setDiagram(SVGDiagram diagram)
406    {
407        this.diagram = diagram;
408        diagram.setElement(id, this);
409        for (SVGElement ele : children) {
410            ele.setDiagram(diagram);
411        }
412    }
413
414    public void removeChild(SVGElement child) throws SVGElementException
415    {
416        if (!children.contains(child))
417        {
418            throw new SVGElementException(this, "Element does not contain child " + child);
419        }
420
421        children.remove(child);
422    }
423
424    /**
425     * Called during load process to add text scanned within a tag
426     * @param helper
427     * @param text
428     */
429    public void loaderAddText(SVGLoaderHelper helper, String text)
430    {
431    }
432
433    /**
434     * Called to indicate that this tag and the tags it contains have been
435     * completely processed, and that it should finish any load processes.
436     * @param helper
437     * @throws com.kitfox.svg.SVGParseException
438     */
439    public void loaderEndElement(SVGLoaderHelper helper) throws SVGParseException
440    {
441//        try
442//        {
443//            build();
444//        }
445//        catch (SVGException se)
446//        {
447//            throw new SVGParseException(se);
448//        }
449    }
450
451    /**
452     * Called by internal processes to rebuild the geometry of this node from
453     * it's presentation attributes, style attributes and animated tracks.
454     * @throws com.kitfox.svg.SVGException
455     */
456    protected void build() throws SVGException
457    {
458        StyleAttribute sty = new StyleAttribute();
459
460        if (getPres(sty.setName("id")))
461        {
462            String newId = sty.getStringValue();
463            if (!newId.equals(id))
464            {
465                diagram.removeElement(id);
466                id = newId;
467                diagram.setElement(this.id, this);
468            }
469        }
470        if (getPres(sty.setName("class")))
471        {
472            cssClass = sty.getStringValue();
473        }
474        if (getPres(sty.setName("xml:base")))
475        {
476            xmlBase = sty.getURIValue();
477        }
478
479        //Build children
480        for (int i = 0; i < children.size(); ++i)
481        {
482            SVGElement ele = (SVGElement) children.get(i);
483            ele.build();
484        }
485    }
486
487    public URI getXMLBase()
488    {
489        return xmlBase != null ? xmlBase
490            : (parent != null ? parent.getXMLBase() : diagram.getXMLBase());
491    }
492
493    /**
494     * @return the id assigned to this node. Null if no id explicitly set.
495     */
496    public String getId()
497    {
498        return id;
499    }
500    LinkedList<SVGElement> contexts = new LinkedList<SVGElement>();
501
502    /**
503     * Hack to allow nodes to temporarily change their parents. The Use tag will
504     * need this so it can alter the attributes that a particular node uses.
505     * @param context
506     */
507    protected void pushParentContext(SVGElement context)
508    {
509        contexts.addLast(context);
510    }
511
512    protected SVGElement popParentContext()
513    {
514        return (SVGElement) contexts.removeLast();
515    }
516
517    protected SVGElement getParentContext()
518    {
519        return contexts.isEmpty() ? null : (SVGElement) contexts.getLast();
520    }
521
522    public SVGRoot getRoot()
523    {
524        return parent == null ? null : parent.getRoot();
525    }
526
527    /*
528     * Returns the named style attribute.  Checks for inline styles first, then
529     * internal and extranal style sheets, and finally checks for presentation
530     * attributes.
531     * @param styleName - Name of attribute to return
532     * @param recursive - If true and this object does not contain the
533     * named style attribute, checks attributes of parents abck to root until
534     * one found.
535     */
536    public boolean getStyle(StyleAttribute attrib) throws SVGException
537    {
538        return getStyle(attrib, true);
539    }
540
541    public void setAttribute(String name, int attribType, String value) throws SVGElementException
542    {
543        StyleAttribute styAttr;
544
545
546        switch (attribType)
547        {
548            case AnimationElement.AT_CSS:
549            {
550                styAttr = (StyleAttribute) inlineStyles.get(name);
551                break;
552            }
553            case AnimationElement.AT_XML:
554            {
555                styAttr = (StyleAttribute) presAttribs.get(name);
556                break;
557            }
558            case AnimationElement.AT_AUTO:
559            {
560                styAttr = (StyleAttribute) inlineStyles.get(name);
561
562                if (styAttr == null)
563                {
564                    styAttr = (StyleAttribute) presAttribs.get(name);
565                }
566                break;
567            }
568            default:
569                throw new SVGElementException(this, "Invalid attribute type " + attribType);
570        }
571
572        if (styAttr == null)
573        {
574            throw new SVGElementException(this, "Could not find attribute " + name + "(" + AnimationElement.animationElementToString(attribType) + ").  Make sure to create attribute before setting it.");
575        }
576
577        //Alter layout for relevant attributes
578        if ("id".equals(styAttr.getName()))
579        {
580            if (diagram != null)
581            {
582                diagram.removeElement(this.id);
583                diagram.setElement(value, this);
584            }
585            this.id = value;
586        }
587
588        styAttr.setStringValue(value);
589    }
590
591    public boolean getStyle(StyleAttribute attrib, boolean recursive) throws SVGException
592    {
593        return getStyle(attrib, recursive, true);
594    }
595    
596    /**
597     * Copies the current style into the passed style attribute. Checks for
598     * inline styles first, then internal and extranal style sheets, and finally
599     * checks for presentation attributes. Recursively checks parents.
600     *
601     * @param attrib - Attribute to write style data to. Must have it's name set
602     * to the name of the style being queried.
603     * @param recursive - If true and this object does not contain the named
604     * style attribute, checks attributes of parents back to root until one
605     * found.
606     * @param evalAnimation
607     * @return 
608     */
609    public boolean getStyle(StyleAttribute attrib, boolean recursive, boolean evalAnimation)
610            throws SVGException
611    {
612        String styName = attrib.getName();
613
614        //Check for local inline styles
615        StyleAttribute styAttr = (StyleAttribute)inlineStyles.get(styName);
616
617        attrib.setStringValue(styAttr == null ? "" : styAttr.getStringValue());
618
619        //Evalutate coresponding track, if one exists
620        if (evalAnimation)
621        {
622            TrackBase track = trackManager.getTrack(styName, AnimationElement.AT_CSS);
623            if (track != null)
624            {
625                track.getValue(attrib, diagram.getUniverse().getCurTime());
626                return true;
627            }
628        }
629
630        //Return if we've found a non animated style
631        if (styAttr != null)
632        {
633            return true;
634        }
635
636
637        //Check for presentation attribute
638        StyleAttribute presAttr = (StyleAttribute)presAttribs.get(styName);
639
640        attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
641
642        //Evalutate coresponding track, if one exists
643        if (evalAnimation)
644        {
645            TrackBase track = trackManager.getTrack(styName, AnimationElement.AT_XML);
646            if (track != null)
647            {
648                track.getValue(attrib, diagram.getUniverse().getCurTime());
649                return true;
650            }
651        }
652
653        //Return if we've found a presentation attribute instead
654        if (presAttr != null)
655        {
656            return true;
657        }
658
659        //Check for style sheet
660        SVGRoot root = getRoot();
661        if (root != null)
662        {
663            StyleSheet ss = root.getStyleSheet();
664            if (ss != null)
665            {
666                return ss.getStyle(attrib, getTagName(), cssClass);
667            }
668        }
669
670        //If we're recursive, check parents
671        if (recursive)
672        {
673            SVGElement parentContext = getParentContext();
674            if (parentContext != null)
675            {
676                return parentContext.getStyle(attrib, true);
677            }
678            if (parent != null)
679            {
680                return parent.getStyle(attrib, true);
681            }
682        }
683
684        //Unsuccessful reading style attribute
685        return false;
686    }
687
688    /**
689     * @param styName
690     * @return the raw style value of this attribute. Does not take the
691     * presentation value or animation into consideration. Used by animations to
692     * determine the base to animate from.
693     */
694    public StyleAttribute getStyleAbsolute(String styName)
695    {
696        //Check for local inline styles
697        return (StyleAttribute) inlineStyles.get(styName);
698    }
699
700    /**
701     * Copies the presentation attribute into the passed one.
702     *
703     * @param attrib
704     * @return - True if attribute was read successfully
705     * @throws com.kitfox.svg.SVGException
706     */
707    public boolean getPres(StyleAttribute attrib) throws SVGException
708    {
709        String presName = attrib.getName();
710
711        //Make sure we have a coresponding presentation attribute
712        StyleAttribute presAttr = (StyleAttribute) presAttribs.get(presName);
713
714        //Copy presentation value directly
715        attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
716
717        //Evalutate coresponding track, if one exists
718        TrackBase track = trackManager.getTrack(presName, AnimationElement.AT_XML);
719        if (track != null)
720        {
721            track.getValue(attrib, diagram.getUniverse().getCurTime());
722            return true;
723        }
724
725        //Return if we found presentation attribute
726        if (presAttr != null)
727        {
728            return true;
729        }
730
731        return false;
732    }
733
734    /**
735     * @param styName
736     * @return the raw presentation value of this attribute. Ignores any
737     * modifications applied by style attributes or animation. Used by
738     * animations to determine the starting point to animate from
739     */
740    public StyleAttribute getPresAbsolute(String styName)
741    {
742        //Check for local inline styles
743        return (StyleAttribute) presAttribs.get(styName);
744    }
745
746    private static final Pattern TRANSFORM_PATTERN = Pattern.compile("\\w+\\([^)]*\\)");
747    static protected AffineTransform parseTransform(String val) throws SVGException
748    {
749        final Matcher matchExpression = TRANSFORM_PATTERN.matcher("");
750
751        AffineTransform retXform = new AffineTransform();
752
753        matchExpression.reset(val);
754        while (matchExpression.find())
755        {
756            retXform.concatenate(parseSingleTransform(matchExpression.group()));
757        }
758
759        return retXform;
760    }
761
762    private static final Pattern WORD_PATTERN = Pattern.compile("([a-zA-Z]+|-?\\d+(\\.\\d+)?(e-?\\d+)?|-?\\.\\d+(e-?\\d+)?)");
763    static public AffineTransform parseSingleTransform(String val) throws SVGException
764    {
765        final Matcher matchWord = WORD_PATTERN.matcher("");
766
767        AffineTransform retXform = new AffineTransform();
768
769        matchWord.reset(val);
770        if (!matchWord.find())
771        {
772            //Return identity transformation if no data present (eg, empty string)
773            return retXform;
774        }
775
776        String function = matchWord.group().toLowerCase();
777
778        LinkedList<String> termList = new LinkedList<String>();
779        while (matchWord.find())
780        {
781            termList.add(matchWord.group());
782        }
783
784
785        double[] terms = new double[termList.size()];
786        Iterator<String> it = termList.iterator();
787        int count = 0;
788        while (it.hasNext())
789        {
790            terms[count++] = XMLParseUtil.parseDouble(it.next());
791        }
792
793        //Calculate transformation
794        if (function.equals("matrix"))
795        {
796            retXform.setTransform(terms[0], terms[1], terms[2], terms[3], terms[4], terms[5]);
797        } else if (function.equals("translate"))
798        {
799            if (terms.length == 1)
800            {
801                retXform.setToTranslation(terms[0], 0);
802            } else
803            {
804                retXform.setToTranslation(terms[0], terms[1]);
805            }
806        } else if (function.equals("scale"))
807        {
808            if (terms.length > 1)
809            {
810                retXform.setToScale(terms[0], terms[1]);
811            } else
812            {
813                retXform.setToScale(terms[0], terms[0]);
814            }
815        } else if (function.equals("rotate"))
816        {
817            if (terms.length > 2)
818            {
819                retXform.setToRotation(Math.toRadians(terms[0]), terms[1], terms[2]);
820            } else
821            {
822                retXform.setToRotation(Math.toRadians(terms[0]));
823            }
824        } else if (function.equals("skewx"))
825        {
826            retXform.setToShear(Math.toRadians(terms[0]), 0.0);
827        } else if (function.equals("skewy"))
828        {
829            retXform.setToShear(0.0, Math.toRadians(terms[0]));
830        } else
831        {
832            throw new SVGException("Unknown transform type");
833        }
834
835        return retXform;
836    }
837
838    static protected float nextFloat(LinkedList<String> l)
839    {
840        String s = (String) l.removeFirst();
841        return Float.parseFloat(s);
842    }
843
844    private static final Pattern COMMAND_PATTERN = Pattern.compile("([MmLlHhVvAaQqTtCcSsZz])|([-+]?((\\d*\\.\\d+)|(\\d+))([eE][-+]?\\d+)?)");
845    static protected PathCommand[] parsePathList(String list)
846    {
847        final Matcher matchPathCmd = COMMAND_PATTERN.matcher(list);
848
849        //Tokenize
850        LinkedList<String> tokens = new LinkedList<String>();
851        while (matchPathCmd.find())
852        {
853            tokens.addLast(matchPathCmd.group());
854        }
855
856
857        boolean defaultRelative = false;
858        LinkedList<PathCommand> cmdList = new LinkedList<PathCommand>();
859        char curCmd = 'Z';
860        while (tokens.size() != 0)
861        {
862            String curToken = (String) tokens.removeFirst();
863            char initChar = curToken.charAt(0);
864            if ((initChar >= 'A' && initChar <= 'Z') || (initChar >= 'a' && initChar <= 'z'))
865            {
866                curCmd = initChar;
867            } else
868            {
869                tokens.addFirst(curToken);
870            }
871
872            PathCommand cmd = null;
873
874            switch (curCmd)
875            {
876                case 'M':
877                    cmd = new MoveTo(false, nextFloat(tokens), nextFloat(tokens));
878                    curCmd = 'L';
879                    break;
880                case 'm':
881                    cmd = new MoveTo(true, nextFloat(tokens), nextFloat(tokens));
882                    curCmd = 'l';
883                    break;
884                case 'L':
885                    cmd = new LineTo(false, nextFloat(tokens), nextFloat(tokens));
886                    break;
887                case 'l':
888                    cmd = new LineTo(true, nextFloat(tokens), nextFloat(tokens));
889                    break;
890                case 'H':
891                    cmd = new Horizontal(false, nextFloat(tokens));
892                    break;
893                case 'h':
894                    cmd = new Horizontal(true, nextFloat(tokens));
895                    break;
896                case 'V':
897                    cmd = new Vertical(false, nextFloat(tokens));
898                    break;
899                case 'v':
900                    cmd = new Vertical(true, nextFloat(tokens));
901                    break;
902                case 'A':
903                    cmd = new Arc(false, nextFloat(tokens), nextFloat(tokens),
904                        nextFloat(tokens),
905                        nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
906                        nextFloat(tokens), nextFloat(tokens));
907                    break;
908                case 'a':
909                    cmd = new Arc(true, nextFloat(tokens), nextFloat(tokens),
910                        nextFloat(tokens),
911                        nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
912                        nextFloat(tokens), nextFloat(tokens));
913                    break;
914                case 'Q':
915                    cmd = new Quadratic(false, nextFloat(tokens), nextFloat(tokens),
916                        nextFloat(tokens), nextFloat(tokens));
917                    break;
918                case 'q':
919                    cmd = new Quadratic(true, nextFloat(tokens), nextFloat(tokens),
920                        nextFloat(tokens), nextFloat(tokens));
921                    break;
922                case 'T':
923                    cmd = new QuadraticSmooth(false, nextFloat(tokens), nextFloat(tokens));
924                    break;
925                case 't':
926                    cmd = new QuadraticSmooth(true, nextFloat(tokens), nextFloat(tokens));
927                    break;
928                case 'C':
929                    cmd = new Cubic(false, nextFloat(tokens), nextFloat(tokens),
930                        nextFloat(tokens), nextFloat(tokens),
931                        nextFloat(tokens), nextFloat(tokens));
932                    break;
933                case 'c':
934                    cmd = new Cubic(true, nextFloat(tokens), nextFloat(tokens),
935                        nextFloat(tokens), nextFloat(tokens),
936                        nextFloat(tokens), nextFloat(tokens));
937                    break;
938                case 'S':
939                    cmd = new CubicSmooth(false, nextFloat(tokens), nextFloat(tokens),
940                        nextFloat(tokens), nextFloat(tokens));
941                    break;
942                case 's':
943                    cmd = new CubicSmooth(true, nextFloat(tokens), nextFloat(tokens),
944                        nextFloat(tokens), nextFloat(tokens));
945                    break;
946                case 'Z':
947                case 'z':
948                    cmd = new Terminal();
949                    break;
950                default:
951                    throw new RuntimeException("Invalid path element");
952            }
953
954            cmdList.add(cmd);
955            defaultRelative = cmd.isRelative;
956        }
957
958        PathCommand[] retArr = new PathCommand[cmdList.size()];
959        cmdList.toArray(retArr);
960        return retArr;
961    }
962
963    static protected GeneralPath buildPath(String text, int windingRule)
964    {
965        PathCommand[] commands = parsePathList(text);
966
967        int numKnots = 2;
968        for (int i = 0; i < commands.length; i++)
969        {
970            numKnots += commands[i].getNumKnotsAdded();
971        }
972
973
974        GeneralPath path = new GeneralPath(windingRule, numKnots);
975
976        BuildHistory hist = new BuildHistory();
977
978        for (int i = 0; i < commands.length; i++)
979        {
980            PathCommand cmd = commands[i];
981            cmd.appendPath(path, hist);
982        }
983
984        return path;
985    }
986
987    /**
988     * Updates all attributes in this diagram associated with a time event. Ie,
989     * all attributes with track information.
990     *
991     * @param curTime
992     * @return - true if this node has changed state as a result of the time
993     * update
994     * @throws com.kitfox.svg.SVGException
995     */
996    abstract public boolean updateTime(double curTime) throws SVGException;
997
998    public int getNumChildren()
999    {
1000        return children.size();
1001    }
1002
1003    public SVGElement getChild(int i)
1004    {
1005        return (SVGElement) children.get(i);
1006    }
1007
1008    public double lerp(double t0, double t1, double alpha)
1009    {
1010        return (1 - alpha) * t0 + alpha * t1;
1011    }
1012}