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 August 15, 2004, 2:52 AM
035 */
036
037package com.kitfox.svg.animation;
038
039import com.kitfox.svg.SVGConst;
040import com.kitfox.svg.SVGElement;
041import com.kitfox.svg.SVGException;
042import com.kitfox.svg.SVGLoaderHelper;
043import com.kitfox.svg.animation.parser.AnimTimeParser;
044import com.kitfox.svg.animation.parser.ParseException;
045import com.kitfox.svg.xml.StyleAttribute;
046import java.io.StringReader;
047import java.util.logging.Level;
048import java.util.logging.Logger;
049import org.xml.sax.Attributes;
050import org.xml.sax.SAXException;
051
052
053/**
054 * @author Mark McKay
055 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
056 */
057public abstract class AnimationElement extends SVGElement
058{
059    protected String attribName;
060//    protected String attribType;
061    protected int attribType = AT_AUTO;
062
063    public static final int AT_CSS = 0;
064    public static final int AT_XML = 1;
065    public static final int AT_AUTO = 2;  //Check CSS first, then XML
066
067    protected TimeBase beginTime;
068    protected TimeBase durTime;
069    protected TimeBase endTime;
070    protected int fillType = FT_AUTO;
071
072    /** <a href="http://www.w3.org/TR/smil20/smil-timing.html#adef-fill">More about the <b>fill</b> attribute</a> */
073    public static final int FT_REMOVE = 0;
074    public static final int FT_FREEZE = 1;
075    public static final int FT_HOLD = 2;
076    public static final int FT_TRANSITION = 3;
077    public static final int FT_AUTO = 4;
078    public static final int FT_DEFAULT = 5;
079
080    /** Additive state of track */
081    public static final int AD_REPLACE = 0;
082    public static final int AD_SUM = 1;
083
084    int additiveType = AD_REPLACE;
085    
086    /** Accumlative state */
087    public static final int AC_REPLACE = 0;
088    public static final int AC_SUM = 1;
089
090    int accumulateType = AC_REPLACE;
091
092    /** Creates a new instance of AnimateEle */
093    public AnimationElement()
094    {
095    }
096
097    public static String animationElementToString(int attrValue)
098    {
099        switch (attrValue)
100        {
101            case AT_CSS:
102                return "CSS";
103            case AT_XML:
104                return "XML";
105            case AT_AUTO:
106                return "AUTO";
107            default:
108                throw new RuntimeException("Unknown element type");
109        }
110    }
111    
112    public void loaderStartElement(SVGLoaderHelper helper, Attributes attrs, SVGElement parent) throws SAXException
113    {
114                //Load style string
115        super.loaderStartElement(helper, attrs, parent);
116
117        attribName = attrs.getValue("attributeName");
118        String attribType = attrs.getValue("attributeType");
119        if (attribType != null)
120        {
121            attribType = attribType.toLowerCase();
122            if (attribType.equals("css")) this.attribType = AT_CSS;
123            else if (attribType.equals("xml")) this.attribType = AT_XML;
124        }
125
126        String beginTime = attrs.getValue("begin");
127        String durTime = attrs.getValue("dur");
128        String endTime = attrs.getValue("end");
129
130        try
131        {
132            if (beginTime != null)
133            {
134                helper.animTimeParser.ReInit(new StringReader(beginTime));
135                this.beginTime = helper.animTimeParser.Expr();
136                this.beginTime.setParentElement(this);
137            }
138
139            if (durTime != null)
140            {
141                helper.animTimeParser.ReInit(new StringReader(durTime));
142                this.durTime = helper.animTimeParser.Expr();
143                this.durTime.setParentElement(this);
144            }
145
146            if (endTime != null)
147            {
148                helper.animTimeParser.ReInit(new StringReader(endTime));
149                this.endTime = helper.animTimeParser.Expr();
150                this.endTime.setParentElement(this);
151            }
152        }
153        catch (Exception e)
154        {
155            throw new SAXException(e);
156        }
157        
158//        this.beginTime = TimeBase.parseTime(beginTime);
159//        this.durTime = TimeBase.parseTime(durTime);
160//        this.endTime = TimeBase.parseTime(endTime);
161
162        String fill = attrs.getValue("fill");
163
164        if (fill != null)
165        {
166            if (fill.equals("remove")) this.fillType = FT_REMOVE;
167            if (fill.equals("freeze")) this.fillType = FT_FREEZE;
168            if (fill.equals("hold")) this.fillType = FT_HOLD;
169            if (fill.equals("transiton")) this.fillType = FT_TRANSITION;
170            if (fill.equals("auto")) this.fillType = FT_AUTO;
171            if (fill.equals("default")) this.fillType = FT_DEFAULT;
172        }
173        
174        String additiveStrn = attrs.getValue("additive");
175        
176        if (additiveStrn != null)
177        {
178            if (additiveStrn.equals("replace")) this.additiveType = AD_REPLACE;
179            if (additiveStrn.equals("sum")) this.additiveType = AD_SUM;
180        }
181        
182        String accumulateStrn = attrs.getValue("accumulate");
183        
184        if (accumulateStrn != null)
185        {
186            if (accumulateStrn.equals("replace")) this.accumulateType = AC_REPLACE;
187            if (accumulateStrn.equals("sum")) this.accumulateType = AC_SUM;
188        }
189    }
190
191    public String getAttribName() { return attribName; }
192    public int getAttribType() { return attribType; }
193    public int getAdditiveType() { return additiveType; }
194    public int getAccumulateType() { return accumulateType; }
195
196    public void evalParametric(AnimationTimeEval state, double curTime)
197    {
198        evalParametric(state, curTime, Double.NaN, Double.NaN);
199    }
200
201    /**
202     * Compares current time to start and end times and determines what degree
203     * of time interpolation this track currently represents.  Returns
204     * Float.NaN if this track cannot be evaluated at the passed time (ie,
205     * it is before or past the end of the track, or it depends upon
206     * an unknown event)
207     * @param state - A structure that will be filled with information
208     * regarding the applicability of this animatoin element at the passed
209     * time.
210     * @param curTime - Current time in seconds
211     * @param repeatCount - Optional number of repetitions of length 'dur' to
212     * do.  Set to Double.NaN to not consider this in the calculation.
213     * @param repeatDur - Optional amoun tof time to repeat the animaiton.
214     * Set to Double.NaN to not consider this in the calculation.
215     */
216    protected void evalParametric(AnimationTimeEval state, double curTime, double repeatCount, double repeatDur)
217    {
218        double begin = (beginTime == null) ? 0 : beginTime.evalTime();
219        if (Double.isNaN(begin) || begin > curTime)
220        {
221            state.set(Double.NaN, 0);
222            return;
223        }
224
225        double dur = (durTime == null) ? Double.NaN : durTime.evalTime();
226        if (Double.isNaN(dur))
227        {
228            state.set(Double.NaN, 0);
229            return;
230        }
231
232        //Determine end point of this animation
233        double end = (endTime == null) ? Double.NaN : endTime.evalTime();
234        double repeat;
235//        if (Double.isNaN(repeatDur))
236//        {
237//            repeatDur = dur;
238//        }
239        if (Double.isNaN(repeatCount) && Double.isNaN(repeatDur))
240        {
241            repeat = Double.NaN;
242        }
243        else
244        {
245            repeat = Math.min(
246                Double.isNaN(repeatCount) ? Double.POSITIVE_INFINITY : dur * repeatCount,
247                Double.isNaN(repeatDur) ? Double.POSITIVE_INFINITY : repeatDur);
248        }
249        if (Double.isNaN(repeat) && Double.isNaN(end))
250        {
251            //If neither and end point nor a repeat is specified, end point is 
252            // implied by duration.
253            end = begin + dur;
254        }
255
256        double finishTime;
257        if (Double.isNaN(end))
258        {
259            finishTime = begin + repeat;
260        }
261        else if (Double.isNaN(repeat))
262        {
263            finishTime = end;
264        }
265        else
266        {
267            finishTime = Math.min(end, repeat);
268        }
269        
270        double evalTime = Math.min(curTime, finishTime);
271//        if (curTime > finishTime) evalTime = finishTime;
272        
273        
274//        double evalTime = curTime;
275
276//        boolean pastEnd = curTime > evalTime;
277        
278//        if (!Double.isNaN(end) && curTime > end) { pastEnd = true; evalTime = Math.min(evalTime, end); }
279//        if (!Double.isNaN(repeat) && curTime > repeat) { pastEnd = true; evalTime = Math.min(evalTime, repeat); }
280        
281        double ratio = (evalTime - begin) / dur;
282        int rep = (int)ratio;
283        double interp = ratio - rep;
284        
285        //Adjust for roundoff
286        if (interp < 0.00001) interp = 0;
287
288//        state.set(interp, rep);
289//        if (!pastEnd)
290//        {
291//            state.set(interp, rep, false);
292//            return;
293//        }
294
295        //If we are still within the clip, return value
296        if (curTime == evalTime)
297        {
298            state.set(interp, rep);
299            return;
300        }
301        
302        //We are past end of clip.  Determine to clamp or ignore.
303        switch (fillType)
304        {
305            default:
306            case FT_REMOVE:
307            case FT_AUTO:
308            case FT_DEFAULT:
309                state.set(Double.NaN, rep);
310                return;
311            case FT_FREEZE:
312            case FT_HOLD:
313            case FT_TRANSITION:
314                state.set(interp == 0 ? 1 : interp, rep);
315                return;
316        }
317
318    }
319
320    double evalStartTime()
321    {
322        return beginTime == null ? Double.NaN : beginTime.evalTime();
323    }
324
325    double evalDurTime()
326    {
327        return durTime == null ? Double.NaN : durTime.evalTime();
328    }
329
330    /**
331     * Evaluates the ending time of this element.  Returns 0 if not specified.
332     *
333     * @see hasEndTime
334     */
335    double evalEndTime()
336    {
337        return endTime == null ? Double.NaN : endTime.evalTime();
338    }
339
340    /**
341     * Checks to see if an end time has been specified for this element.
342     */
343    boolean hasEndTime() { return endTime != null; }
344
345    /**
346     * Updates all attributes in this diagram associated with a time event.
347     * Ie, all attributes with track information.
348     * @return - true if this node has changed state as a result of the time
349     * update
350     */
351    public boolean updateTime(double curTime)
352    {
353        //Animation elements to not change with time
354        return false;
355    }
356
357    public void rebuild() throws SVGException
358    {
359        AnimTimeParser animTimeParser = new AnimTimeParser(new StringReader(""));
360
361        rebuild(animTimeParser);
362    }
363
364    protected void rebuild(AnimTimeParser animTimeParser) throws SVGException
365    {
366        StyleAttribute sty = new StyleAttribute();
367
368        if (getPres(sty.setName("begin")))
369        {
370            String newVal = sty.getStringValue();
371            animTimeParser.ReInit(new StringReader(newVal));
372            try {
373                this.beginTime = animTimeParser.Expr();
374            } catch (ParseException ex) {
375                    Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 
376                        "Could not parse '" + newVal + "'", ex);
377            }
378        }
379
380        if (getPres(sty.setName("dur")))
381        {
382            String newVal = sty.getStringValue();
383            animTimeParser.ReInit(new StringReader(newVal));
384            try {
385                this.durTime = animTimeParser.Expr();
386            } catch (ParseException ex) {
387                    Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 
388                        "Could not parse '" + newVal + "'", ex);
389            }
390        }
391
392        if (getPres(sty.setName("end")))
393        {
394            String newVal = sty.getStringValue();
395            animTimeParser.ReInit(new StringReader(newVal));
396            try {
397                this.endTime = animTimeParser.Expr();
398            } catch (ParseException ex) {
399                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 
400                    "Could not parse '" + newVal + "'", ex);
401            }
402        }
403
404        if (getPres(sty.setName("fill")))
405        {
406            String newVal = sty.getStringValue();
407            if (newVal.equals("remove")) this.fillType = FT_REMOVE;
408            if (newVal.equals("freeze")) this.fillType = FT_FREEZE;
409            if (newVal.equals("hold")) this.fillType = FT_HOLD;
410            if (newVal.equals("transiton")) this.fillType = FT_TRANSITION;
411            if (newVal.equals("auto")) this.fillType = FT_AUTO;
412            if (newVal.equals("default")) this.fillType = FT_DEFAULT;
413        }
414
415        if (getPres(sty.setName("additive")))
416        {
417            String newVal = sty.getStringValue();
418            if (newVal.equals("replace")) this.additiveType = AD_REPLACE;
419            if (newVal.equals("sum")) this.additiveType = AD_SUM;
420        }
421
422        if (getPres(sty.setName("accumulate")))
423        {
424            String newVal = sty.getStringValue();
425            if (newVal.equals("replace")) this.accumulateType = AC_REPLACE;
426            if (newVal.equals("sum")) this.accumulateType = AC_SUM;
427        }
428
429    }
430}