FileDocCategorySizeDatePackage
TimedElementSupport.javaAPI DocphoneME MR2 API (J2ME)84160Wed May 02 18:00:36 BST 2007com.sun.perseus.model

TimedElementSupport.java

/*
 *
 *
 * Copyright  1990-2007 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt).
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions.
 */
package com.sun.perseus.model;

import java.util.Vector;

/**
 * The <code>TimedElementSupport</code> class is the main abstraction of the
 * SMIL timing model implementation.
 *
 * <p>It is responsible for computing the animation's state (inactive, active,
 * post-acitve, frozen) and, when the application is active, what the current
 * simple time is. That simple time is then used by the animation engine to
 * compute animated values. </p>
 * 
 * <p>SMIL has a notion of time containers. A <code>TimedElementSupport</code>
 * is the child of a time container. The relationship is that the element's
 * current time is the container's simple time. In otherwords, sampling a
 * container for a given time (in its own container), yields a simple time for
 * the container. That simple time is the timed element's current time, used to
 * compute the timed element's own simple time. Refer to the SMIL 2
 * specification for an explanation of what the various time semantics are. </p>
 *
 * @version $Id: TimedElementSupport.java,v 1.7 2006/07/13 00:55:58 st125089 Exp $
 */
public class TimedElementSupport {
    /**
     * Last Simple Duration End event type.
     */
    public static final String LAST_DUR_END_EVENT_TYPE = "lastDurEndEvent";

    /**
     * Seek End event type.
     */
    public static final String SEEK_END_EVENT_TYPE = "seekEndEvent";

    /**
     * Seek End event type.
     */
    public static final String SEEK_BEGIN_EVENT_TYPE = "seekBeginEvent";

    /**
     * End event type.
     */
    public static final String END_EVENT_TYPE = "endEvent";

    /**
     * Begin event type.
     */
    public static final String BEGIN_EVENT_TYPE = "beginEvent";

    /**
     * Repeat event type
     */
    public static final String REPEAT_EVENT_TYPE = "repeat";

    /**
     * Used when this <code>TimedElement</code> can be restarted under
     * any circumstance.
     */
    public static final int RESTART_ALWAYS = 1;

    /**
     * Used when this <code>TimedElement</code> can only be restarted when
     * it is not active (i.e., it does not have a current interval.
     */
    public static final int RESTART_WHEN_NOT_ACTIVE = 2;

    /**
     * Used when this <code>TimedElement</code> cannot be restarted under
     * any circumstance
     */
    public static final int RESTART_NEVER = 3;

    /**
     * Used when this <code>TimedElement</code> should not modify the
     * animated value when it is post active.
     */
    public static final int FILL_BEHAVIOR_REMOVE = 1;

    /**
     * Used when this <code>TimedElement</code> should maintain the
     * last value of its simple interval after it becomes inactive.
     */
    public static final int FILL_BEHAVIOR_FREEZE = 2;

    /**
     * State before the element has been initialized
     */
    protected static final int STATE_PRE_INIT = 1;

    /**
     * State where there has never been a current interval
     * (since the last init).
     */
    protected static final int STATE_NO_INTERVAL = 2;

    /**
     * State while the element is waiting to begin
     * the first interval.
     */
    protected static final int STATE_WAITING_INTERVAL_0 = 3;

    /**
     * State while the element is waiting to begin
     * an interval other than the first one
     */
    protected static final int STATE_WAITING_INTERVAL_N = 4;

    /**
     * State while the element is playing the current
     * interval.
     */
    protected static final int STATE_PLAYING = 5;

    /**
     * State when the element performs any fill on the previous
     * interval.
     */
    protected static final int STATE_FILL = 6;

    /**
     * This time value is used to avoid creating Time instances
     * over and over again in animation loops, when dispatching
     * events.
     */
    protected final Time eventTime = new Time(0);

    /**
     * The element's state. One of the STATE_XXX constants.
     */
    int state = STATE_PRE_INIT;

    /**
     * Controls whether the element is in this rare state
     * where it is playing but its last simple duration 
     * is finished. In that state, the element is 'playing',
     * but acting as if frozen.
     *
     * There are situations where the min attribute may cause the 
     * active duration to go beyond the time of the last simple
     * duration instance. In that case, the element is still
     * in the playing state, but we mark it as 'playFill', which
     * means that it should behave almost as if in the fill state.
     */
    boolean playFill = false;

    /**
     * The current iteration (only used while playing
     */
    int curIter = 0;

    /**
     * This element's simple duration. This corresponds to the 'dur'
     * attribute. Note that this is only one of the parameters that
     * defines the actual simple duration.
     *
     * @see #simpleDur
     * @see #computeSimpleDuration
     */
    Time dur = Time.INDEFINITE;

    /**
     * The number of times this element repeats the simple duration.
     * NaN means unspecified.
     */
    float repeatCount = Float.NaN;

    /**
     * repeatDur set a limit on active duration length.
     */
    Time repeatDur = null;

    /**
     * This element's implicit duration
     */
    Time implicitDuration = Time.UNRESOLVED;

    /**
     * min sets a lower limit on the active duration.
     */
    Time min = new Time(0);

    /**
     * max sets an upper limit on the active duration
     */
    Time max = Time.INDEFINITE;

    /**
     * Defines the restart behavior for this element. This should be
     * one of RESTART_ALWAYS, RESTART_WHEN_NOT_ACTIVE, RESTART_NEVER.
     */
    int restart = RESTART_ALWAYS;

    /**
     * Defines the behavior after this element is no longer active.
     * If FILL_BEHAVIOR_FILL, it means the element will keep sampling at the 
     * last simple duration time. If FILL_BEHAVIOR_REMOVE (the default),
     * the effect of the animation is removed when the element is 
     * inactive (i.e., when it no longer has a current interval.
     */
    int fillBehavior = FILL_BEHAVIOR_REMOVE;

    /**
     * A reference to this element's time container. This is initialized
     * anytime the parent is set. This element's current time is the 
     * container's simple time.
     * 
     * @see #setParent
     */
    protected TimeContainerSupport timeContainer;

    /**
     * Keeps a reference to the current <code>TimeInterval</code>
     */
    TimeInterval currentInterval;

    /**
     * Last local time (i.e., within the simple duration) this element
     * was sampled at.
     */
    long lastSampleTime = -1;

    /**
     * The simple duration for the current interval
     * @see #computeSimpleDuration
     */
    Time simpleDur = Time.UNRESOLVED;

    /**
     * Keeps a reference to the interval preceding the current one.
     */
    TimeInterval previousInterval;

    /**
     * Keeps are reference to the last interval that was actually
     * run.
     */
    
    /**
     * List of begin <code>TimeInstance</code>s.
     */
    Vector beginInstances = new Vector(1);

    /**
     * List of end <code>TimeInstance</code>s.
     */
    Vector endInstances = new Vector(1);

    /**
     * Begin <code>TimeCondition</code>s. Should _never_ be null.
     */
    Vector beginConditions = new Vector(1);

    /**
     * End <code>TimeCondition</code>s. Should _never_ be null.
     */
    Vector endConditions = new Vector(1);

    /**
     * List of <code>SyncBaseCondition</code>s depedent on this
     * element's begin condition.
     */
    Vector beginDependents;

    /**
     * List of <code>SyncBaseCondition</code>s dependent on this 
     * element's end condition.
     */
    Vector endDependents;

    /**
     * The associated animation element.
     */
    ModelNode animationElement;

    /**
     * Time dependency cycles detector. This flag is set to true
     * when a timing update starts. There should only be one at
     * a time, and it should always be set back to true after
     * and timing update is complete.
     */
    boolean timingUpdate = false;

    /**
     * The seeking flag is used when seeking to a particular time
     * to prevent generation of beginEvent, endEvent and repeat
     * event.
     */
    boolean seeking = false;

    /**
     * Sets the repeat duration. 
     *
     * @param repeatDur the new repeat duration.
     * @throws IllegalStateException if the element is not in the
     *         STATE_PRE_INIT state.
     */
    public void setRepeatDur(final Time repeatDur) {
        checkPreInit();
        this.repeatDur = repeatDur;
    }

    /**
     * Sets the repeat count.
     *
     * @param repeatCount the new repeat count. Must be greater than
     *        zero.
     * @throws IllegalStateException if the element is not in the
     *         STATE_PRE_INIT state.
     */
    public void setRepeatCount(final float repeatCount) {
        checkPreInit();

        if (repeatCount <= 0) {
            throw new IllegalArgumentException();
        }

        this.repeatCount = repeatCount;
    }

    /**
     * Sets the restart strategy.
     *
     * @param restart the new restart strategy, one of
     *         RESTART_ALWAYS, RESTART_NEVER or RESTART_WHEN_NOT_ACTIVE.
     * @throws IllegalStateException if the element is not in the
     *         STATE_PRE_INIT state.
     */
    public void setRestart(final int restart) {
        checkPreInit();
        switch (restart) {
        case RESTART_ALWAYS:
        case RESTART_NEVER:
        case RESTART_WHEN_NOT_ACTIVE:
            this.restart = restart;
            break;
        default:
            throw new IllegalArgumentException();
        }
    }

    /**
     * Sets the maximun active duration for any timed element
     * interval.
     *
     * @param max the new maximum active duration. Should not be
     *        null.
     * @throws IllegalStateException if the element is not in the
     *         STATE_PRE_INIT state.
     */
    public void setMax(final Time max) {
        checkPreInit();
        if (max == null) {
            throw new IllegalArgumentException();
        }
        this.max = max;
    }

    /**
     * Sets the minimum active duration for any timed element interval.
     *
     * @param min the new minimum active duration. Should not be null.
     * @throws IllegalStateException if the element is not in the
     *         STATE_PRE_INIT state.
     */
    public void setMin(final Time min) {
        checkPreInit();
        if (min == null) {
            throw new IllegalArgumentException();
        }
        this.min = min;
    }

    /**
     * Sets the duration for this timed element
     *
     * @param dur the new element duration.
     * @throws IllegalStateException if the element is not in the
     *         STATE_PRE_INIT state.
     */
    public void setDur(final Time dur) {
        checkPreInit();
        this.dur = dur;
    }

    /**
     * Sets the fill state behavior.
     *
     * @param fillBehavior the new fill strategy, one of FILL_BEHAVIOR_FREEZE or
     *        FILL_BEHAVIOR_REMOVE
     * @throws IllegalStateException if the element is not in the 
     *         STATE_PRE_INIT state.
     */
    public void setFillBehavior(final int fillBehavior) {
        checkPreInit();
        switch (fillBehavior) {
        case FILL_BEHAVIOR_FREEZE:
        case FILL_BEHAVIOR_REMOVE:
            this.fillBehavior = fillBehavior;
            break;
        default:
            throw new IllegalArgumentException();
        }
    }

    /**
     * Implementation helper. Throws an exception if the element is
     * not in the STATE_PRE_INIT state.
     *
     * @throws IllegalStateException if the element is not in the
     *         STATE_PRE_INIT state.
     */
    public void checkPreInit() {
        if (state != STATE_PRE_INIT) {
            throw new IllegalStateException();
        }
    }

    /**
     * Calling this method causes a new <code>TimeInstance</code> to
     * be inserted in the begin instance list. The behavior depends
     * on the restart setting.
     */
    public void begin() {
        beginAt(0);
    }

    /**
     * Calling this method causes a new <code>TimeInstance</code> to
     * be inserted in the end instance list. This typically causes
     * the current interval (if any), to be ended.
     */
    public void end() {
        endAt(0);
    }

    /**
     * Calling this method causes a new <code>TimeInstance</code> to
     * be inserted in the begin instance list. The behavior depends
     * on the restart setting.
     *
     * @param offset the begin time inserted into the instance list
     *        is offset by 'offset' milliseconds from the current
     *        time.
     */
    public void beginAt(final long offset) {
        addInstance(true, offset);
    }
   
    /**
     * Calling this method causes a new <code>TimeInstance</code> to
     * be inserted in the end instance list. This typically causes
     * the current interval (if any), to be ended.
     *
     * @param offset the end time inserted into the instance list
     *        is offset by 'offset' milliseconds from the current
     *        time.
     */
    public void endAt(final long offset) {
        addInstance(false, offset);
    }

    /**
     * Adds a new <code>TimedInstance</code> with the specified offset
     * to the begin or end instance list (depending on <code>isBegin</code>
     *
     * @param isBegin if true, the instance is a begin instance
     * @param offset the instance offset from 0.
     */
    void addInstance(final boolean isBegin,
                     final long offset) {
        Time currentTime = getCurrentTime();
        if (!currentTime.isResolved()) {
            // If the current time is not resolved, it means the time
            // container is not active. Therefore, there is no way
            // we can add a new time instance that would be meaningful. 
            // So we do nothing.
            return;
        }

        Time newTime = new Time(currentTime.value + offset);

        TimeInstance newInstance 
            = new TimeInstance(this, newTime, true, isBegin);
    }

    /**
     * Sets this timed element's time container
     *
     * @param timeContainer time container
     */
    protected void setTimeContainer(final TimeContainerSupport timeContainer) {
        if (this.timeContainer != null) {
            this.timeContainer.timedElementChildren.removeElement(this);
        }

        this.timeContainer = timeContainer;
        if (timeContainer != null) {
            timeContainer.timedElementChildren.addElement(this);
            if (timeContainer.state != STATE_PRE_INIT) {
                // This is a live addition of an animation to a container.
                // We need to initialize this timed element support.
                initialize();
            }
        }
    }

    /**
     * @return this TimedElement's container.
     */
    protected TimeContainerSupport getTimeContainer() {
        return timeContainer;
    }

    /**
     * This method is called by the element's time container when it
     * resets, i.e., when the container starts it's simple time again.
     *
     * This has the effect of:
     * 1. clearing all the instance times which have the clearOnReset
     *    flag set to true.
     * 2. compute the first interval.
     */
    protected void initialize() {
        // Clear instances with clearOnReset == true
        reset();

        // Compute the first interval
        if (!checkNewInterval(computeFirstInterval())) {
            state = STATE_NO_INTERVAL;
        }
    }

    /**
     * Calls all the registered <code>TimeDependent</code>s so that they
     * are notified of a new TimeInterval creation.
     */
    void dispatchOnNewInterval() {
        int n = beginDependents == null ? 0 : beginDependents.size();
        for (int i = 0; i < n; i++) {
            ((TimeDependent) beginDependents.elementAt(i)).onNewInterval(this);
        }

        n = endDependents == null ? 0 : endDependents.size();
        for (int i = 0; i < n; i++) {
            ((TimeDependent) endDependents.elementAt(i)).onNewInterval(this);
        }
    }

    /**
     * @param after we are looking for a time greater than or equal
     *        to after.
     * @param instances the Vector containing the TimeInstances to
     *        look up.
     * @return the first value in the given instance list that
     *         starts after the input time.
     */
    Time getTimeAfter(final Time after,
                      final Vector instances) {
        int n = instances.size();
        
        // NOTE: The following _assumes_ that the instances vector
        //       is sorted in increasing time order.
        for (int i = 0; i < n; i++) {
            TimeInstance timeInstance = (TimeInstance) instances.elementAt(i);
            if (timeInstance.time.greaterThan(after)) {
                return timeInstance.time;
            }
        }
        return null;
    }

    /**
     * @param after we are looking for a time greater than or equal
     *        to after.
     * @param instances the Vector containing the TimeInstances to
     *        look up.
     * @return the first value in the given instance list that
     *         starts strictly after the input time.
     */
    Time getTimeAfterStrict(final Time after,
                            final Vector instances) {
        int n = instances.size();
        
        // NOTE: The following _assumes_ that the instances vector
        //       is sorted in increasing time order.
        for (int i = 0; i < n; i++) {
            TimeInstance timeInstance = (TimeInstance) instances.elementAt(i);
            if (!after.greaterThan(timeInstance.time)) {
                return timeInstance.time;
            }
        }
        return null;
    }

    /**
     * Computes the active end of this <code>TimedElement</code> for
     * the given begin and end time constraints. This accounts for 
     * the other attributes which control or constrain the 
     * active duration.
     *
     * @param begin the input begin time. Should not be unresolved or null.
     * @param end the proposed end time. May be null.
     * @return the computed, constrained end time.
     *
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-ComputingActiveDur">
     * SMIL 2: Computing the active duration</a>
     */
    Time calculateActiveEnd(final Time begin, final Time end) {
        Time result = end;
        Time pad = null;

        if (begin == null) {
            throw new NullPointerException();
        }
        
        if (!begin.isResolved()) {
            throw new IllegalArgumentException();
        }

        //
        // In the context of this method, end and begin are the time instances
        // passed to this method, and _not_ the begin and end attributes.
        //
        // If endTime is not null, and none of dur, repeatDur, and repeatCount
        // are specified, then the simple duration is indefinite from the simple
        // duration table above (see SMIL 2 spec), and the active duration is
        // defined by the end value, according to the following cases:
        //
        // If end is resolved to a value, then pad = end - B,
        // 
        // else, if end is indefinite, then pad = indefinite,
        // 
        // else, if end is unresolved, then pad is unresolved, and needs to be
        // recomputed when more information becomes available.
        //
        if (end != null 
            && dur == null 
            && repeatDur == null 
            && Float.isNaN(repeatCount)) {
            if (end.isResolved()) {
                pad = new Time(end.value - begin.value);
            } else if (end == Time.INDEFINITE) {
                pad = Time.INDEFINITE;
            } else {
                pad = Time.UNRESOLVED;
            }
        }


        // Else, if no end value is specified, or the end value is specified as
        // indefinite, then the active duration is determined from the
        // Intermediate Active Duration computation given below:
        // 
        // pad = Result from Intermediate Active Duration Computation  
        else if (end == null || end == Time.INDEFINITE) {
            pad = 
                calculateIntermediateActiveDuration(computeSimpleDuration(end));
        }
        
        // Otherwise, an end value not equal to indefinite is specified along
        // with at least one of dur, repeatDur, and repeatCount. Then the pad is
        // the minimum of the result from the Intermediate Active Duration
        // Computation given below and duration between end and the element
        // begin:
        //
        // pad = MIN( Result from Intermediate Active Duration Computation, 
        //            end - B)
        else {
            pad = 
                calculateIntermediateActiveDuration(computeSimpleDuration(end));
            Time pad2 = Time.UNRESOLVED;
            if (end.isResolved()) {
                pad2 = new Time(end.value - begin.value);
            }
            if (pad.greaterThan(pad2)) {
                pad = pad2;
            }
        }

        // Finally, the computed active duration ad is obtained by applying min
        // and max semantics to the preliminary active duration pad. In the
        // following expression, if there is no min value, substitute a value of
        // 0, and if there is no max value, substitute a value of "indefinite":
        // 
        // ad = MIN( max, MAX( min, pad )) 

        // Only do min/max constraint if min < max
        boolean doMinMax = true;
        if (min != null && max != null && !max.greaterThan(min)) {
            doMinMax = false;
        }

        Time ad = null;
        if (pad.isResolved()) {
            ad = new Time(pad.value);
        } else {
            ad = pad;
        }
            
        if (doMinMax) {
            // MAX( min, pad )
            if (min != null && min.greaterThan(ad)) {
                if (min.isResolved()) {
                    ad.value = min.value;
                } else {
                    ad = min;
                }
            }
            
            // MIN( max, MAX( min, pad ))
            if (max != null && !max.greaterThan(ad)) {
                if (max == Time.INDEFINITE) {
                    ad = Time.INDEFINITE;
                } else {
                    if (ad.isResolved()) {
                        ad.value = max.value;
                    } else {
                        ad = new Time(max.value);
                    }
                }
            }
        }

        // We have computed the active duration. Now, offset that
        // duration with the begin time to yield the computed end time.
        if (ad.isResolved()) {
            ad.value += begin.value;
        }

        // Finally, apply the restart behavior. If restart is always
        // we check if there is any end time that is before active
        // end time.
        if (restart == RESTART_ALWAYS) {
            Time restartBegin = getTimeAfterStrict(begin, beginInstances);
            if (restartBegin != null && ad.greaterThan(restartBegin)) {
                ad.value = restartBegin.value;
            }
        }

        return ad;
    }

    /**
     * Intermediate Active Duration Computation, as defined in the 
     * SMIL 2 specifications. 
     *
     * @param p0 the element's simple duration. Should not be null
     * @return the intermediate active duration.
     * @see <a href="http://www.w3.org/TR/smil20/smil-timing.html#q81">
     *                  Intermediate Active Duration Computation</a>
     */
    final Time calculateIntermediateActiveDuration(final Time p0) {
        if (p0 == null) {
            throw new NullPointerException();
        }

        if (p0.isResolved() && p0.value == 0) {
            return new Time(0);
        } else if (repeatDur == null && Float.isNaN(repeatCount)) {
            return p0;
        } else {
            Time p1 = Time.INDEFINITE;
            Time p2 = repeatDur;
            Time iad = Time.UNRESOLVED;
            
            if (!Float.isNaN(repeatCount)) {
                if (p0.isResolved()) {
                    if (repeatCount == Float.MAX_VALUE) {
                        p1 = Time.INDEFINITE;
                    } else {
                        p1 = new Time((long) (p0.value * repeatCount));
                    }
                } else {
                    p1 = p0; // INDEFINITE or UNRESOLVED
                }
            }
            
            if (p2 == null) {
                p2 = Time.INDEFINITE;
            }
            
            iad = Time.INDEFINITE;
            if (!p2.greaterThan(iad)) {
                iad = p2;
            }
            if (!p1.greaterThan(iad)) {
                iad = p1;
            }
            return iad;
        }
    }

    /**
     * Computes the element's simple duration, as defined in the 
     * SMIL 2 specification except that the 'media' value is
     * not supported.
     *
     * For the purpose of computing the simple duration, an unspecified
     * dur or an UNRESOLVED dur yield the same computed simple duration.
     *
     * @param end the interval end time. Can be null or have any Time
     *        value.
     * @return the computed simple duration.
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-DefiningSimpleDur">
     * Defining the simple duration</a>
     */
    final Time computeSimpleDuration(final Time end) {
        Time implicitDur = getImplicitElementDuration();

        //    dur          | implicit    | repeatDur and  | simpleDuration
        //                 | duration    | repeatCount    |
        //    -------------+-------------+----------------+--------------------
        // 1. unspecified  | ignored     | unspecified,   | indefinite
        //                 |             | end specified  |
        // 2. Clock-value  | ignored     | ignored        | dur or Clock-value
        // 3. indefinite   | ignored     | ignored        | indefinite
        // 4. unspecified  | resolved    | ignored        | implicit duration
        // 5. unspecified  | unresolved  | ignored        | unresolved
        //    -------------+-------------+----------------+--------------------
        
        // First line case
        if (dur == null && repeatDur == null
            && Float.isNaN(repeatCount) && end != Time.UNRESOLVED) {
            return Time.INDEFINITE;
        }

        // Second & Third line case
        if (dur != null && dur != Time.UNRESOLVED) {
            return dur;
        }

        // Fourth line case. If we got to this point, we know 
        // dur is UNRESOLVED or null
        if (implicitDur != Time.UNRESOLVED) {
            return implicitDur;
        } else {
            // Fifth line
            return Time.UNRESOLVED;
        }
    }

    /**
     * @return this element's implicit duration. Can be used for media such
     *         as audio and video where there is a natural, implicit duration.
     */
    protected Time getImplicitElementDuration() {
        return implicitDuration;
    }

    /**
     * @return true if there is at least one event end condition.
     */
    boolean endHasEventConditions() {
        for (int i = 0; i < endConditions.size(); i++) {
            if (endConditions.elementAt(i) instanceof EventBaseCondition) {
                return true;
            }
        }
        return false;
    }

    /**
     * Computes the time of the last simple duration instance
     * for the given time interval.
     *
     * @param ti the TimeInterval instance for which the time of the last
     *        simple duration instance should be computed.
     *
     * @return the input TimeInterval instance.
     */
    TimeInterval computeLastDur(final TimeInterval ti) {
        Time simpleDur = computeSimpleDuration(ti.end);
        if (simpleDur != null) {
            Time iad = calculateIntermediateActiveDuration(simpleDur);
            if (iad.isResolved()) {
                ti.lastDur = new Time(ti.begin.value + iad.value);
                if (ti.lastDur.greaterThan(ti.end)) {
                    ti.lastDur = ti.end;
                }
            } else {
                ti.lastDur = ti.end;
            }
        } else {
            ti.lastDur = ti.end;
        }

        return ti;
    }

    /**
     * The following computes the element's first interval, as defined
     * in the SMIL specification.
     *
     * @return the first time interval
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-BeginEnd-LC-Start">
     * STARTUP - Getting the first interval</a>
     */
    TimeInterval computeFirstInterval() {
        Time beginAfter = new Time(Long.MIN_VALUE);
        Time parentSimpleEnd = getContainerSimpleDuration();
        
        while (true) { // loop till return
            Time tempBegin = getTimeAfter(beginAfter, beginInstances);
            if (tempBegin == null) {
                return null; // No interval
            }

            if (tempBegin.greaterThan(parentSimpleEnd)) {
                return null; // No interval
            }

            Time tempEnd = null;
            if (endConditions.size() == 0) {
                tempEnd = calculateActiveEnd(tempBegin, null);
            } else {
                tempEnd = getTimeAfter(tempBegin, endInstances);
                if (tempBegin.isSameTime(tempEnd)) {
                    tempEnd = getTimeAfterStrict(tempEnd, endInstances);
                }

                if (tempEnd == null) {
                    if (endHasEventConditions() || endInstances.size() == 0) {
                        tempEnd = Time.UNRESOLVED;
                    } else {
                        return null; // No interval
                    }
                }

                tempEnd = calculateActiveEnd(tempBegin, tempEnd);
            }

            // We have an end - is it after the parent simple begin?
            if (!tempEnd.isResolved() || tempEnd.value > 0) {
                return computeLastDur(new TimeInterval(tempBegin, tempEnd));
            } else {
                beginAfter = tempEnd;
            }
        }
    }

    /**
     * The following computes the element's next interval, as defined
     * in the SMIL specification.
     *
     * @return the next interval given the current begin and end time instances.
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-BeginEnd-LC-End">
     * End of an interval</a>
     */
    TimeInterval computeNextInterval() {
        Time curEnd = previousInterval.end;
        Time beginAfter = curEnd;
        Time parentSimpleEnd = getContainerSimpleDuration();
        Time tempBegin = getTimeAfter(beginAfter, beginInstances);

        if (tempBegin == null) {
            return null;
        }
        
        if (tempBegin.greaterThan(parentSimpleEnd)) {
            return null;
        }

        Time tempEnd = null;
        if (endConditions.size() == 0) {
            tempEnd = calculateActiveEnd(tempBegin, null);
            if (endInstances.size() > 0) {
                // There is no end-attribute specified, but there are time 
                // instances. The SMIL Animation specification says that if
                // there are times instances from DOM Events or DOM 
                // beginElementAt calls, they short cut the active duration. 
                // 
                // See: http://www.w3.org/TR/2001/REC-smil-animation-20010904/
                // section 3.3.4.
                //
                Time tempEnd2 = getTimeAfter(tempBegin, endInstances);
                if (tempEnd2 != null && tempEnd2.isResolved()) {
                    tempEnd = tempEnd2;
                }
            }
        } else {
            // We have a begin value - get an end
            tempEnd = getTimeAfter(tempBegin, endInstances);

            if (curEnd.isSameTime(tempEnd)) {
                tempEnd = getTimeAfterStrict(tempEnd, endInstances);
            }

            if (tempEnd == null) {
                if (endHasEventConditions() || endInstances.size() == 0) {
                    tempEnd = Time.UNRESOLVED;
                } else {
                    return null;
                }
            }

            tempEnd = calculateActiveEnd(tempBegin, tempEnd);
        }

        return computeLastDur(new TimeInterval(tempBegin, tempEnd));
    }

    /**
     * Computes the end time corresponding to the input begin time.
     *
     * @param tempBegin the begin time for which to search an end time.
     * @return the computed end time or null if there is no matching end.
     */
    Time computeEndTime(final Time tempBegin) {
        if (endInstances.size() == 0) {
            return calculateActiveEnd(tempBegin, null);
        } else {
            Time tempEnd = getTimeAfter(tempBegin, endInstances);

            if (tempBegin.isSameTime(tempEnd)) {
                // IMPL NOTE : Do this to get a strictly superior end time
                tempEnd = new Time(tempEnd.value + 1);
                tempEnd = getTimeAfter(tempEnd, endInstances);
            }

            if (tempEnd == null) {
                if (endHasEventConditions() || endInstances.size() == 0) {
                    tempEnd = Time.UNRESOLVED;
                } else {
                    return null;
                }
            }

            return calculateActiveEnd(tempBegin, tempEnd);
        }
    }

    /**
     * Resetting the element clears all of its 'clearOnReset' instances from the
     * begin and end list. This includes event based time instances, time
     * instances resulting from begin() or end() calls and resolved
     * IntervalTimeInstance.
     */
    void reset() {
        currentInterval = null;
        simpleDur = Time.UNRESOLVED;
        previousInterval = null;
        curIter = 0;
        state = STATE_PRE_INIT;
        lastSampleTime = -1;

        int n = beginInstances.size();
        for (int i = n - 1; i >= 0; i--) {
            TimeInstance timeInstance 
                = (TimeInstance) beginInstances.elementAt(i);
            if (timeInstance.clearOnReset) {
                beginInstances.removeElementAt(i);
            } else if (timeInstance instanceof IntervalTimeInstance) {
                ((IntervalTimeInstance) timeInstance).syncTime();
            } 
        }
        n = endInstances.size();
        for (int i = n - 1; i >= 0; i--) {
            TimeInstance timeInstance 
                = (TimeInstance) endInstances.elementAt(i);
            if (timeInstance.clearOnReset) {
                endInstances.removeElementAt(i);
            } else if (timeInstance instanceof IntervalTimeInstance) {
                ((IntervalTimeInstance) timeInstance).syncTime();
            } 
        }
    }

    /**
     * Removes all <code>IntervalTimeInstance</code>s in the begin and end
     * instance list if the syncBase is a descendant of syncTimeContainer
     *
     * @param syncTimeContainer the container under which
     *        <code>IntervalTimeInstance</code> should be removed.
     */
    void removeSyncBaseTimesUnder
        (final TimeContainerSupport syncTimeContainer) {
        int n = beginInstances.size();
        for (int i = n - 1; i >= 0; i--) {
            TimeInstance timeInstance 
                = (TimeInstance) beginInstances.elementAt(i);
            if (timeInstance instanceof IntervalTimeInstance) {
                IntervalTimeInstance iti = (IntervalTimeInstance) timeInstance;
                if (iti.syncBase.isDescendant(syncTimeContainer)) {
                    beginInstances.removeElementAt(i);
                    iti.dispose();
                }
            } 
        }
        n = endInstances.size();
        for (int i = n - 1; i >= 0; i--) {
            TimeInstance timeInstance = 
                    (TimeInstance) endInstances.elementAt(i);
            if (timeInstance instanceof IntervalTimeInstance) {
                IntervalTimeInstance iti = (IntervalTimeInstance) timeInstance;
                if (iti.syncBase.isDescendant(syncTimeContainer)) {
                    endInstances.removeElementAt(i);
                    iti.dispose();
                }
            } 
        }
    }

    /**
     * @param parent the time container which might be on the timed element's
     *        parent hierarchy.
     * @return true if this timed element is equal to 'parent' or is a 
     * descendant of 'parent'
     */
    boolean isDescendant(final TimeContainerSupport parent) {
        if (parent == this) {
            return true;
        } else if (timeContainer != this) {
            return timeContainer.isDescendant(parent);
        } else {
            return false;
        }
    }

    /**
     * Dispatches beginEvent. As per the SMIL 2 specification, this dispatches
     * a beginEvent for the resolved begin time, not the observed begin
     * time. It also dispatches any repeat event that might be needed.
     *
     * @param currentTime the current sampling time.
     *
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-BeginValueSemantics">
     * SMIL2's Begin value semantics</a>
     */
    void dispatchBeginEvent(final Time currentTime) {
        if (animationElement != null) {
            ModelEvent beginEvent = null;
            if (seeking) {
                beginEvent = animationElement.ownerDocument.initEngineEvent
                    (SEEK_BEGIN_EVENT_TYPE, animationElement);
            } else {
                beginEvent = animationElement.ownerDocument.initEngineEvent
                    (BEGIN_EVENT_TYPE, animationElement);
            }

            eventTime.value = currentInterval.begin.value;
            beginEvent.eventTime = toRootContainerSimpleTimeClamp(eventTime);
            animationElement.dispatchEvent(beginEvent);
        }

        dispatchRepeatEvent(currentTime);
    }

    /**
     * Dispatches lastDurEndEvent.
     *
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#smil-timing-Timing-BeginEnd-Restart
     * </a>
     */
    void dispatchLastDurEndEvent() {
        if (animationElement != null) {
            ModelEvent event = animationElement.ownerDocument.initEngineEvent
                (LAST_DUR_END_EVENT_TYPE, animationElement);
            eventTime.value = currentInterval.lastDur.value;
            event.eventTime = toRootContainerSimpleTimeClamp(eventTime);
            animationElement.dispatchEvent(event);
        }
    }

    /**
     * Dispatches seekeEndEvent.
     *
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#smil-timing-Timing-BeginEnd-Restart
     * </a>
     */
    void dispatchSeekEndEvent() {
        if (animationElement != null) {
            ModelEvent event = animationElement.ownerDocument.initEngineEvent
                (SEEK_END_EVENT_TYPE, animationElement);
            animationElement.dispatchEvent(event);
        }
    }

    /**
     * Dispatches endEvent. As per the SMIL 2 specification, this dispatches an
     * endEvent for the resolved end time, not the observed end time.
     *
     * @param currentTime the current sampling time.
     *
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-BeginValueSemantics">
     * SMIL2's Begin value semantics</a>
     */
    void dispatchEndEvent(final Time currentTime) {
        if ((!seeking || state == STATE_PLAYING)
            && animationElement != null) {
            ModelEvent endEvent = animationElement.ownerDocument.initEngineEvent
                (END_EVENT_TYPE, animationElement);
            if (seeking) {
                // We are seeking to a new currentTime and the interval
                // ends during the seek interval and the element was
                // active at the time of seek. The time for the end
                // event should be the time _before_ the seek
                eventTime.value = getRootContainer().lastSampleTime.value;
                endEvent.eventTime = eventTime;
            } else {
                eventTime.value = currentInterval.end.value;
                endEvent.eventTime = toRootContainerSimpleTimeClamp(eventTime);
            }
            
            animationElement.dispatchEvent(endEvent);
        }
        dispatchRepeatEvent(currentTime);
    }

    /**
     * Helper method. 
     *
     * @return this timed element's root container.
     */
    TimeContainerRootSupport getRootContainer() {
        return timeContainer.getRootContainer();
    }

    /**
     * Dispatches repeatEvent. As per the SMIL 2 specification, this dispatches
     * a repeatEvent for the resolved end time, not the observed repeat
     * time(s).
     *
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-BeginValueSemantics">
     * SMIL2's Begin value semantics</a>
     */
    void dispatchRepeatEvent(final Time currentTime) {
        if (!seeking && simpleDur.isResolved() 
            && simpleDur.value > 0) {
            int prevIter = curIter;

            Time maxTime = currentTime;
            if (currentTime.greaterThan(currentInterval.end)) {
                maxTime = currentInterval.end;
            }

            curIter = (int) ((maxTime.value - currentInterval.begin.value) 
                             / 
                             simpleDur.value);

            if (curIter < 0) {
                curIter = 0;
            }

            if (maxTime.isSameTime(currentInterval.end)
                && 
                ((maxTime.value - currentInterval.begin.value) 
                 % 
                 simpleDur.value 
                 == 0)) {
                curIter--;
            }

            if (animationElement != null) {
                for (int i = prevIter; i < curIter; i++) {
                    ModelEvent repeatEvent 
                        = animationElement.ownerDocument.initEngineEvent
                        (REPEAT_EVENT_TYPE, animationElement);
                    
                    // The event time is the time at which the repeat happened,
                    // not the time at which it was detected (that would be
                    // currentTime).  So we have to compute the repeat time here
                    Time repeatTime = eventTime;
                    repeatTime.value = currentInterval.begin.value
                        + (i + 1) * simpleDur.value;
                    repeatTime = toRootContainerSimpleTimeClamp(repeatTime);
                    repeatEvent.eventTime = repeatTime;
                    repeatEvent.repeatCount = i + 1;
                    animationElement.dispatchEvent(repeatEvent);
                }
            }

            if (prevIter != curIter) {
                onStartingRepeat(prevIter, curIter);
            }
        }
    }

    /**
     * To be overridden by extensions (TimeContainerSupport) to do specific
     * processing when a new iteration is started.
     *
     * @param prevIter the last iteration this element was playing.
     * @param curIter the new iteration this element is playing.
     */
    protected void onStartingRepeat(final int prevIter, final int curIter) {
    }

    /**
     * Checks if the input interval is non null. If it is, that interval
     * becomes the new currentInterval and the element sets its state 
     * accordingly.
     *
     * @param interval the interval to check.
     * @return true if the candidate interval has become the current interval.
     */
    boolean checkNewInterval(final TimeInterval interval) {
        if (interval == null) {
            return false;
        } else {
            currentInterval = interval;
            simpleDur = computeSimpleDuration(currentInterval.end);

            // Transition to the current state
            // Note that we move to the 'playing' stage only in the
            // sample method, where event dispatching is done as well.
            if (previousInterval == null) {
                state = STATE_WAITING_INTERVAL_0;
            } else {
                state = STATE_WAITING_INTERVAL_N;
            }

            // Notify dependents of the new interval
            dispatchOnNewInterval();
            return true;
        }
    }

    /**
     * Helper method to find the active interval, or next interval, for
     * the requested time.
     *
     * @param seekToTime the time we are seeking the active or next interval
     *        for.
     * @return the <code>TimeInterval</code> corresponding to
     *         <code>seekToTime</code>
     */
    TimeInterval seekToInterval(final Time seekToTime) {
        previousInterval = null;
        TimeInterval interval = computeFirstInterval();
        if (interval == null) {
            return null;
        }

        while (interval != null && seekToTime.greaterThan(interval.end)) {
            previousInterval = interval;
            interval = computeNextInterval();
        }

        return interval;
    }

    /**
     * Helper method.
     *
     * @return true if the root time container is flagged as seeking back.
     */
    boolean isSeekingBack() {
        return getRootContainer().seekingBack;
    }

    /**
     * This method is typically called by this element's time container
     * when it samples.
     *
     * Note that if this element is not in the waiting or playing
     * state, this does nothing. This method assumes that successive
     * calls are made with increasing time values.
     *
     * @param currentTime the time at which this element should be 
     *        sampled. 
     */
    void sample(final Time currentTime) {
        // Handle state transitions if there is a current interval
        boolean endOfInterval = false;
        boolean seekBack = false;

        switch (state) {
        default:
            return;
        case STATE_FILL:
            if (seeking && isSeekingBack()) {
                seekBack = true;
            }
            break;
        case STATE_WAITING_INTERVAL_0:
            if (currentTime.greaterThan(currentInterval.begin)) {
                // This element has begun between the previous sampling
                // and now. Dispatch the beginEvent.
                dispatchBeginEvent(currentTime);
                
                if (currentTime.greaterThan(currentInterval.end)) {
                    // This element has also ended between the 
                    // previous sampling and now. Dispatch the endEvent
                    dispatchLastDurEndEvent();
                    dispatchEndEvent(currentTime);
                    endOfInterval = true;
                }

                state = STATE_PLAYING;
                playFill = false;
            }
            break;
        case STATE_WAITING_INTERVAL_N:
            if (currentTime.greaterThan(currentInterval.begin)) {
                // This element has begun between the previous sampling
                // and now. Dispatch the beginEvent.
                dispatchBeginEvent(currentTime);
                
                if (currentTime.greaterThan(currentInterval.end)) {
                    // This element has also ended between the 
                    // previous sampling and now. Dispatch the endEvent
                    dispatchLastDurEndEvent();
                    dispatchEndEvent(currentTime);
                    endOfInterval = true;
                }

                state = STATE_PLAYING;
                playFill = false;
            } else {
                if (seeking && isSeekingBack()) {
                    seekBack = true;
                }
            }
            break;
        case STATE_PLAYING:
            if (currentTime.greaterThan(currentInterval.lastDur)) {
                if (!playFill) {
                    dispatchLastDurEndEvent();
                    playFill = true;
                }
            }

            if (currentTime.greaterThan(currentInterval.end)) {
                // This element has also ended between the 
                // previous sampling and now. Dispatch the endEvent
                dispatchEndEvent(currentTime);
                endOfInterval = true;
            } else {
                if (!seeking) {
                    dispatchRepeatEvent(currentTime);
                } else if (currentInterval.begin.greaterThan(currentTime)) {
                    // We are seeking backwards and the current interval
                    // began during the seek interval. We need to 
                    // dispatch and end event at the time before the 
                    // seek.
                    dispatchLastDurEndEvent();
                    dispatchEndEvent(currentTime);
                    endOfInterval = true;
                    seekBack = true;
                }
            }
            break;
        }

        // If we just finished an interval,
        // we need to compute the new interval now
        if (endOfInterval) {
            previousInterval = currentInterval;
            currentInterval = null;

            // If we cannot restart, we just go to the fill state
            if (restart == RESTART_NEVER) {
                state = STATE_FILL;
                playFill = false;
            } else {
                TimeInterval nextInterval = null;
                if (!seekBack) {
                    nextInterval = computeNextInterval();
                } else {
                    nextInterval = seekToInterval(currentTime);
                }
                
                if (!checkNewInterval(nextInterval)) {
                    // There is no next interval, we move to the 
                    // fill state.
                    state = STATE_FILL;
                    playFill = false;
                }
            }
             
            // This recursive call is needed in case we sample so far ahead 
            // that we are skipping several intervals.
            // For example if the intervals are:
            // [0, 1000[
            // [2000, 3000[
            // [5000, 6500[
            // [8000, 1000[
            // and we sample at 0 and then 7000
            sample(currentTime);
            return;
        } else if (seekBack) {
            // If we are in the FILL state and we cannot restart, then we stay
            // in FILL state if seeking to after the previous interval's
            // end. Otherwise, we move to the STATE_NO_INTERVAL state.
            if (state == STATE_FILL && restart == RESTART_NEVER) {
                if (previousInterval.end.greaterThan(currentTime)) {
                    dispatchSeekEndEvent();
                    state = STATE_NO_INTERVAL;
                    playFill = false;
                    return;
                } 
            } else {
                TimeInterval seekInterval = seekToInterval(currentTime);
                
                // If the seek interval is the same as the current interval, we
                // do not need to do further sampling because we were in, or
                // have already moved to, the right interval.
                if (seekInterval == null 
                    || currentInterval == null 
                    || !(seekInterval.begin.isSameTime(currentInterval.begin)
                         &&
                         seekInterval.end.isSameTime(currentInterval.end))) {
                    
                    // We need to check if there is an interval active at the
                    // time we are seeking to.
                    if (!checkNewInterval(seekInterval)) {
                        // There is no currentInterval. 
                        // Switch to the right state 
                        if (previousInterval != null) {
                            state = STATE_FILL;
                            playFill = false;
                        } else {
                            dispatchSeekEndEvent();
			    state = STATE_NO_INTERVAL;
                            playFill = false;
                            return;
                        }
                    } else {
                        // There is an interval at the time we are 
                        // seeking to. End the current interval (which
                        // we know is different because of the previous if
                        // statement) and sample again to have the new current
                        // interval kick-in.
                        dispatchSeekEndEvent();
                        sample(currentTime);
                        return;
                    }
                }
            }
        }
        
        // 
        // Different behaviors depending on the state. At this point, we
        // should only be in one of the following states:
        // - STATE_FILL
        // - STATE_PLAYING
        // - STATE_WAITING_INTERVAL_0
        // - STATE_WAITING_INTERVAL_N
        // 
        long localTime = 0;
        switch (state) {
        default:
            // This should _never_ happen, given this method's code.
            // This is extremely hard to test (would require changing
            // state during the execution of this method) and does 
            // not need to be covered by the test suite. 
            throw new IllegalStateException();
        case STATE_WAITING_INTERVAL_N:
        case STATE_FILL:
            if (fillBehavior == FILL_BEHAVIOR_FREEZE) {
                // If we are in STATE_WAITING_INTERVAL_N or STATE_FILL,
                // it means that we had a previous interval. Therefore,
                // previousInterval is guaranteed to be not null.
                localTime = previousInterval.lastDur.value 
                            - previousInterval.begin.value;
            } else {
                return;
            }
            break;
        case STATE_WAITING_INTERVAL_0:
            return;
        case STATE_PLAYING:
            // If we are still in the PLAYING state but we are past the
            // end of the last simple duration end, make sure we sample at
            // that last simple duration time.
            if (!playFill) {
                localTime = currentTime.value - currentInterval.begin.value;
            } else {
                localTime = currentInterval.lastDur.value 
                    - currentInterval.begin.value;
            }
            break;
        }

        // If we get to this point, it means we were able to
        // compute a localTime we need to sample at. We are
        // either playing the animation or we are in a frozen
        // state. Compute the simple time from the localTime.
        if (simpleDur.isResolved()) {
            localTime = localTime % simpleDur.value;
            if (state != STATE_PLAYING || playFill) {
                // If we are not playing, it means we are frozen
                // (either STATE_WAITING_N or STATE_FILL).
                //
                // Make sure we sample on the end of the simple time 
                // if we froze at the end of the interval.
                // 
                // Note that we do not need to test if the value
                // was 0 before %. Because we are _not_ playing, it
                // means we are _not_ on the first value of the 
                // interval, therefore, we never run into the condition
                // where localTime would be 0 before % and therefore,
                // we do not need to test for that condition
                //
                if (localTime == 0) {
                    // Freeze on the last value in the simple duration
                    // interval only when we freeze.
                    localTime = simpleDur.value;
                }
            }
        }

        // At this point, we have computed our simple time.
        // Ready to connect with the animation engine.
        sampleAt(localTime);

        // Keep the last simpleTime this element was sampled at
        lastSampleTime = localTime;
    }

    /**
     * Samples this element at the given simple time.
     * Should be overridden for specific behaviors.
     *
     * @param simpleTime this timed element simple time.
     */
    void sampleAt(final long simpleTime) {
    }

    /**
     * Called when this element is the target of an hyperlink. The 
     * behavior is that defined in the SMIL 2 specification.
     * 
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-HyperlinksAndTiming">
     * SMIL 2 Specification</a>
     */
    public void activate() {
        // 1. If the target element is active, seek the document time back to
        // the begin time of the current interval for the element.
        if (state == STATE_PLAYING) {
            Time seekToTime = new Time(currentInterval.begin.value);
            seekToTime = toRootContainerSimpleTimeClamp(seekToTime);

            if (seekToTime.isResolved()) {
                seekTo(seekToTime);
                return;
            }
        }

        //
        // 2. Else if the target element begin time is resolved (i.e. there is
        // at least one interval defined for the element), seek the document
        // time (forward or back, as needed) to the begin time of the first
        // interval for the target element. Note that the begin time may be
        // resolved as a result of an earlier hyperlink, DOM or event
        // activation. Once the begin time is resolved (and until the element is
        // reset, e.g. when the parent repeats), hyperlink traversal always
        // seeks. For a discussion of "reset", see Resetting element state. Note
        // also that for an element begin to be resolved, the begin time of all
        // ancestor elements must also be resolved.
        //
        TimeInterval firstInterval = computeFirstInterval();
        if (firstInterval != null) {
            Time seekToTime = new Time(firstInterval.begin.value);

            seekToTime = toRootContainerSimpleTimeClamp(seekToTime);

            if (seekToTime.isResolved()) {
                seekTo(seekToTime);
                return;
            }
        }

        // 3. Else (i.e. there are no defined intervals for the element), the
        // target element begin time must be resolved. This may require seeking
        // and/or resolving ancestor elements as well. This is done by recursing
        // from the target element up to the closest ancestor element that has a
        // resolved begin time (again noting that for an element to have a
        // resolved begin time, all of its ancestors must have resolved begin
        // times). Then, the recursion is "unwound", and for each ancestor in
        // turn (beneath the resolved ancestor) as well as the target element,
        // the following steps are performed:
        //
        //     1. If the element begin time is resolved, seek the document time
        //     (forward or back, as needed) to the begin time of the first
        //     interval for the target element.
        //
        //     2. Else (if the begin time is not resolved), just resolve the
        //     element begin time at the current time on its parent time
        //     container (given the current document position). Disregard the
        //     sync-base or event base of the element, and do not
        //     "back-propagate" any timing logic to resolve the element, but
        //     rather treat it as though it were defined with begin="indefinite"
        //     and just resolve begin time to the current parent time. This
        //     should create an interval and propagate to time dependents.
        //
        seekTo(Time.UNRESOLVED);
    }

    /**
     * Implementation helper method for seekTo behavior.
     *
     * @param seekToTime the time to seek to. 
     */
    void seekTo(final Time seekToTime) {
        if (seekToTime.isResolved()) {
            // Now, move up the container graph.
            timeContainer.seekTo(seekToTime);
        } else {
            // 3. Else (i.e. there are no defined intervals for the element),
            // the target element begin time must be resolved. This may require
            // seeking and/or resolving ancestor elements as well. This is done
            // by recursing from the target element up to the closest ancestor
            // element that has a resolved begin time (again noting that for an
            // element to have a resolved begin time, all of its ancestors must
            // have resolved begin times).
            if (timeContainer.state != STATE_PLAYING) {
                TimeInterval firstContainerInterval 
                    = timeContainer.computeFirstInterval();
                if (firstContainerInterval == null) {
                    timeContainer.seekTo(Time.UNRESOLVED);
                } else {
                    Time parentSeekTo 
                        = new Time(firstContainerInterval.begin.value);
                    parentSeekTo 
                        = timeContainer
                        .toRootContainerSimpleTimeClamp(parentSeekTo);
                    timeContainer.seekTo(parentSeekTo);
                }
            }

            // Then, the recursion is "unwound", and for each ancestor in turn
            // (beneath the resolved ancestor) as well as the target element,
            // the following steps are performed:
            //
            //     1. If the element begin time is resolved, seek the document
            //     time (forward or back, as needed) to the begin time of the
            //     first interval for the target element.
            //
            //     2. Else (if the begin time is not resolved), just resolve the
            //     element begin time at the current time on its parent time
            //     container (given the current document position). Disregard
            //     the sync-base or event base of the element, and do not
            //     "back-propagate" any timing logic to resolve the element, but
            //     rather treat it as though it were defined with
            //     begin="indefinite" and just resolve begin time to the current
            //     parent time. This should create an interval and propagate to
            //     time dependents.
            //
            TimeInterval firstInterval = computeFirstInterval();
            if (firstInterval == null) {
                begin();
                firstInterval = computeFirstInterval();
            }
            

            Time goToTime = new Time(firstInterval.begin.value);
            if (goToTime.value < 0) {
                goToTime.value = 0;
            }

            timeContainer.seekTo(toRootContainerSimpleTimeClamp(goToTime));
        }
    }

    /**
     * Should be called whenever there is a change in the begin time instance 
     * list.
     */
    private void reEvaluateBeginTime() {
        switch (state) {
        case STATE_WAITING_INTERVAL_0:
            {
                TimeInterval curInt = computeFirstInterval();
                if (curInt != null) {
                    currentInterval.setBegin(curInt.begin);
                    currentInterval.setEnd(curInt.end);
                    computeLastDur(currentInterval);
                } else {
                    // There no first interval any more, after
                    // the begin instance has been changed.
                    // We move back to the no interval state
                    pruneCurrentInterval();
                }
            }
            break;
        case STATE_WAITING_INTERVAL_N:
            {
                TimeInterval curInt = computeNextInterval();
                if (curInt != null) {
                    currentInterval.setBegin(curInt.begin);
                    currentInterval.setEnd(curInt.end);
                    computeLastDur(currentInterval);
                } else {
                    pruneCurrentInterval();
                }
            }
            
        default:
            return;
        }
    }
    
    /**
     * Recomputes the end time. This only does something if the 
     * element is playing or waiting to play, because it may update
     * the currentInterval's end time.
     */
    void reEvaluateEndTime() {
        switch (state) {
        case STATE_WAITING_INTERVAL_0:
        case STATE_WAITING_INTERVAL_N:
        case STATE_PLAYING:
            // We are dealing with a new end time and the animation is
            // playing. Re-evaluate the end time and update the 
            // current interval.
            //
            // Note that we cannot simply do a computeNextInterval as this
            // might yield a begin time different from the one on the 
            // playing interval.
            //
            Time newEnd = computeEndTime(currentInterval.begin);

            // It is unclear what should happen here if newEnd is null.
            // This could happen if a list of end times was empty and
            // a SyncBaseCondition, for example, inserts a new end instance
            // that is _before_ the currentInterval's begin time. In that
            // situation, should we just ignore newEnd or should we 
            // prune the currentInterval (if WAITING_0 or WAITING_N) and
            // stop it (if PLAYING)?
            //
            // Pending further investigation of this corner case, we simply
            // ignore a null newEnd
            if (newEnd != null) {
                currentInterval.setEnd(newEnd);
                computeLastDur(currentInterval);
            }
            break;
        default:
            break;
        }
    }

    /**
     * Helper method invoked when the current interval becomes invalid
     * after re-evaluation of the begin and end instances. 
     *
     * @see <a href="file:///D:/work/doc/specs/smil20.html#smil-timing-q86">
     *      SMIL 2: Principles for building and pruning intervals</a>
     */
    private void pruneCurrentInterval() {
        TimeInterval prunedInterval = currentInterval;
        currentInterval = null;
        if (previousInterval != null) {
            state = STATE_FILL;
        } else {
            state = STATE_NO_INTERVAL;
        }

        prunedInterval.prune();
    }

    /**
     * This method inserts the input time instance in the instance list
     *
     * @param newInstance the instance to reposition in its instance list
     * @see #addTimeInstance
     * @see #onTimeInstanceUpdate
     */
    private void insertTimeInstance(final TimeInstance newInstance) {
        Vector instances = beginInstances;
        if (!newInstance.isBegin) {
            instances = endInstances;
        }

        int n = instances.size();
        int i = 0;
        Time newTime = newInstance.time;
        for (i = 0; i < n; i++) {
            TimeInstance timeInstance 
                = (TimeInstance) instances.elementAt(i);
            if (!newTime.greaterThan(timeInstance.time)) {
                instances.insertElementAt(newInstance, i);
                break;
            }
        }

        if (i == n) {
            instances.addElement(newInstance);
        }
    }

    /**
     * Called to add a new <code>TimeInstance</code> to the begin or end
     * instance list (depending on the new instance's <code>isBegin</code>
     * setting).
     *
     * This is also called as a result of invoking beginAt or endAt.
     *
     * @param newInstance the new <code>TimeInstance</code> to add to the 
     *        list. Should not be null.
     * @see #beginAt
     * @see #endAt
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-BeginEnd-Restart">
     * SMIL 2, Interaction with restart semantic</a>
     */
    void addTimeInstance(final TimeInstance newInstance) {
        if (timingUpdate) {
            // We are in a timing dependency cycle. Break now.
            return;
        }
        
        timingUpdate = true;

        try {
            insertTimeInstance(newInstance);
            
            // Do not handle instance time list changes if we are 
            // initializing the time graph.
            if (state == STATE_PRE_INIT) {
                return;
            }
            
            if (!newInstance.isBegin) {
                reEvaluateEndTime();
                return;
            }
            
            // Impact the current interval if needed
            switch (state) {
            case STATE_NO_INTERVAL:
                checkNewInterval(computeFirstInterval());
                break;
            case STATE_FILL:
                if (restart != RESTART_NEVER) {
                    checkNewInterval(computeNextInterval());
                } 
                // Else, don't do anything: this element cannot
                // restart.
                break;
            case STATE_WAITING_INTERVAL_0:
                {
                    TimeInterval curInt = computeFirstInterval();
                    if (curInt != null) {
                        currentInterval.setBegin(curInt.begin);
                        currentInterval.setEnd(curInt.end);
                        computeLastDur(currentInterval);
                    }
                }
                break;
            case STATE_WAITING_INTERVAL_N:
                {
                    TimeInterval curInt = computeNextInterval();
                    // We just added a new time instance. It is not possible
                    // that adding a time instance would make 
                    // computeNextInterval return a null interval here. 
                    // So we do not test for a null condition because it 
                    // can't happen. If it happens,
                    // it means there is a bug in computeNextInterval or that
                    // the beginInstance list was messed with, which should not
                    // happen.
                    currentInterval.setBegin(curInt.begin);
                    currentInterval.setEnd(curInt.end);
                    computeLastDur(currentInterval);
                } 
                break;
            case STATE_PLAYING:
                if (newInstance.isBegin) {
                    if (restart == RESTART_ALWAYS) {
                        if (currentInterval.end
                                .greaterThan(newInstance.time)
                            &&
                            newInstance.time
                                .greaterThan(currentInterval.begin)) {
                            // Update the current interval end time with this
                            // new begin time.
                            currentInterval.setEnd(newInstance.time);
                            computeLastDur(currentInterval);
                            
                            // Next interval will be computed in the next call
                            // to sample();
                        }
                        // Ignore the new instance if it if falls after or
                        // before the current interval
                    }
                    // Ignore the new instance if we cannot restart this
                    // animation.
                } 
                break;
            default:
                throw new IllegalStateException();
            }
        } finally {
            timingUpdate = false;
        }
    }

    /**
     * Called by <code>TimeInstance</code>s when they are updated.
     * In response, the TimedElement may recompute its current
     * interval. 
     *
     * @param timeInstance the <code>TimeInstance</code> that was just 
     *        updated.
     *
     * @throws NullPointerException if timeInstance is null.
     *
     * @see <a
     * href="http://www.w3.org/TR/smil20/smil-timing.html#Timing-SemanticsOfTimingModel">
     * SMIL 2: Building the instance times lists</a>
     */
    void onTimeInstanceUpdate(final TimeInstance timeInstance) {
        if (timingUpdate) {
            // We are in a timing dependency cycle, break now.
            return;
        }

        timingUpdate = true;

        try {
            // First, reposition the instance in the instance list
            if (timeInstance.isBegin) {
                beginInstances.removeElement(timeInstance);
            } else {
                endInstances.removeElement(timeInstance);
            }
            insertTimeInstance(timeInstance);
            
            if (state == STATE_PRE_INIT) {
                return;
            }
            
            if (timeInstance.isBegin) {
                // We are dealing with a begin instance change.
                reEvaluateBeginTime();
            } else {
                // We are dealing with an end time instance.
                reEvaluateEndTime();
            }
        } finally {
            timingUpdate = false;
        }
    }

    /**
     * Called to remove a <code>TimeInstance</code> when a 
     * <code>TimeInterval</code> is pruned. This causes a re-evaluation
     * of the current interval's begin or end time.
     *
     * @param timeInstance the instance to remove from its instance list.
     */
    void removeTimeInstance(final TimeInstance timeInstance) {
        if (timeInstance.isBegin) {
            beginInstances.removeElement(timeInstance);
        } else {
            endInstances.removeElement(timeInstance);
        }
        
        if (timingUpdate) {
            return;
        }
        
        timingUpdate = true;
        try {
            if (state != STATE_PRE_INIT) {
                if (timeInstance.isBegin) {
                    reEvaluateBeginTime();
                } else {
                    reEvaluateEndTime();
                }
            }
        } finally {
            timingUpdate = false;
        }
    }

    /**
     * Called by <code>TimeCondition</code>s when they are constructed.
     *
     * @param newCondition the new <code>TimeCondition</code> to add to the 
     *        list. Should not be null.
     */
    void addTimeCondition(final TimeCondition newCondition) {
        if (newCondition.isBegin) {
            beginConditions.addElement(newCondition);
        } else {
            endConditions.addElement(newCondition);
        }
    }

    /**
     * @param eventCondition the <code>EventBaseCondition</code> to check
     *        against.
     * @return true if this <code>TimedElement</code> has a begin condition
     *         corresponding to the input <code>EventBaseCondition</code>
     */
    boolean hasBeginCondition(final EventBaseCondition eventCondition) {
        int n = beginConditions.size();
        for (int i = 0; i < n; i++) {
            TimeCondition condition = 
                (TimeCondition) beginConditions.elementAt(i);
            if (condition instanceof EventBaseCondition) {
                EventBaseCondition beginCondition = 
                    (EventBaseCondition) condition;
                if (beginCondition.eventType.equals(eventCondition.eventType)
                    &&
                    beginCondition.eventBase == eventCondition.eventBase) {
                    return true;
                }
                                                    
            }
        }

        return false;
    }

    /**
     * @return this <code>TimedElement</code>'s current time.
     */
    Time getCurrentTime() {
        return timeContainer.getSimpleTime();
    }

    /**
     * Converts the input 'local' time to an absolute wallclock time.
     *
     * @param localTime the time to convert to wallclock time
     * @return the time converted to an absolute wallclock time.
     */
    long toWallClockTime(final long localTime) {
        return timeContainer.toWallClockTime(localTime);
    }

    /**
     * Converts the input simple time (i.e., a time in the parent container's
     * simple duration) to a root container simple time (i.e., a time
     * in the root time container's simple time interval). Note that this method
     * mutates the input time value. If the associated time container does not 
     * have a current interval, this method returns Time.UNRESOLVED.
     *
     * @param simpleTime the time in the parent container's simple duration.
     *        Should not be null. If simpleTime is Time.UNRESOLVED, the returned
     *        time is UNRESOLVED. If simpleTime is Time.INDEFINITE, the returned
     *        time is INDEFINITE.
     * @return a time in the root time container's simple duration (i.e., in 
     *         the root container's simple time interval).
     */
    Time toRootContainerSimpleTime(Time simpleTime) {
        if (timeContainer.currentInterval == null) {
            return Time.UNRESOLVED;
        }

        if (!simpleTime.isResolved()) {
            return simpleTime;
        }

        // Convert to the container's container simple time

        // Account for repeat behavior. This yields a time
        // in the [0, ActiveTime[ time system
        if (timeContainer.simpleDur.isResolved()) {
            simpleTime.value += 
                timeContainer.curIter * timeContainer.simpleDur.value;
        }

        // By adding the timeContainer's begin value, we are
        // translating the simpleTime to a time in the container's
        // container simple duration [0, simpleDur[ interval.
        simpleTime.value += timeContainer.currentInterval.begin.value;

        // Now, move the conversion one level up.
        return timeContainer.toRootContainerSimpleTime(simpleTime);
    }

    /**
     * Same definition and behavior as toRootContainerSimpleTime, except that
     * this method clamps values to the container's simple duration. This 
     * means that a value smaller than zero is converted to 0 and a value
     * greater than the simple duration is reduced to the simple duration.
     *
     * @param simpleTime the time in the parent container's simple duration.
     *        Should not be null. If simpleTime is Time.UNRESOLVED, the returned
     *        time is UNRESOLVED. If simpleTime is Time.INDEFINITE, the returned
     *        time is INDEFINITE.
     * @return a time in the root time container's simple duration (i.e., in 
     *         the root container's simple time interval).
     */
    Time toRootContainerSimpleTimeClamp(final Time simpleTime) {
        if (timeContainer.currentInterval == null) {
            return Time.UNRESOLVED;
        }

        if (!simpleTime.isResolved()) {
            return simpleTime;
        }

        // Clamp to zero.
        if (simpleTime.value < 0) {
            simpleTime.value = 0;
        }

        // Convert to the container's _container_ simple time

        // Account for repeat behavior. This yields a time
        // in the [0, ActiveTime[ time system
        if (timeContainer.simpleDur.isResolved()) {
            // Clamp to the simple duration.
            if (simpleTime.value > timeContainer.simpleDur.value) {
                simpleTime.value = timeContainer.simpleDur.value;
            }
            simpleTime.value += 
                timeContainer.curIter * timeContainer.simpleDur.value;
        } else {
            // Still clamp to the container's active duration.
            if (timeContainer.currentInterval.end.isResolved()) {
                long maxDur = timeContainer.currentInterval.end.value 
                    - timeContainer.currentInterval.begin.value;
                if (simpleTime.value > maxDur) {
                    simpleTime.value = maxDur;
                }
            }
        }

        // By adding the timeContainer's begin value, we are
        // translating the simpleTime to a time in the container's
        // container simple duration [0, simpleDur[ interval.
        simpleTime.value += timeContainer.currentInterval.begin.value;

        // Now, move the conversion one level up.
        return timeContainer.toRootContainerSimpleTimeClamp(simpleTime);
    }

    /**
     * Converts the input root container simple time (i.e., a time in the root
     * container's simple time interval) to a time in this element's time
     * container simple duration. Note that this method mutates the input
     * argument. If the associated time container does not 
     * have a current interval, this method returns Time.UNRESOLVED.
     *
     * @param simpleTime the time in the root container's simple duration.  If
     * simpleTime is Time.UNRESOLVED, the returned time is UNRESOLVED. If
     * simpleTime is Time.INDEFINITE, the returned time is INDEFINITE.
     *
     * @return a simple time in the parent container's simple duration The
     * return value is in the [0, container simple duration] interval.
     */
    Time toContainerSimpleTime(Time simpleTime) {
        if (timeContainer.currentInterval == null) {
            return Time.UNRESOLVED;
        }

        if (!simpleTime.isResolved()) {
            return simpleTime;
        }

        // Convertion to the container's container simple time
        simpleTime = timeContainer.toContainerSimpleTime(simpleTime);
        
        // Account for the container's begin value
        simpleTime.value -= timeContainer.currentInterval.begin.value;

        // Account for repeat behavior
        if (timeContainer.simpleDur.isResolved()) {
            simpleTime.value -= 
                timeContainer.curIter * timeContainer.simpleDur.value;
        }

        return simpleTime;
    }

    /**
     * @return the container's simple duration.
     */
    Time getContainerSimpleDuration() {
        return timeContainer.simpleDur;
    }

    /**
     * Debug helper.
     * @return a description of this object.
     */
    public String toString() {
        String str = "[Animation: " + animationElement + "] [" 
                        + getStateString() + "]";
        switch (state) {
        case STATE_WAITING_INTERVAL_N:
        case STATE_WAITING_INTERVAL_0:
        case STATE_PLAYING:
            str += "[" 
                + currentInterval.begin 
                + ", " 
                + currentInterval.end 
                /*
                + ", "
                + currentInterval.lastDur
                */
                + "]";
            break;
        default:
            break;
        }
        return str;
    }

    /**
     * Debug helper. Converts the state to a string.
     *
     * @return a string describing the element's state.
     */
    String getStateString() {
        switch (state) {
        case STATE_PRE_INIT:
            return "PRE_INIT";
        case STATE_WAITING_INTERVAL_0:
            return "WAITING_INTERVAL_0";
        case STATE_WAITING_INTERVAL_N:
            return "WAITING_INTERVAL_N";
        case STATE_NO_INTERVAL:
            return "NO_INTERVAL";
        case STATE_FILL:
            return "FILL";
        case STATE_PLAYING:
            return "PLAYING";
        default:
            return "UNKNOWN";
        }
    }
}