/* * $RCSfile: TimedElementSupport.java,v $ * * Copyright 1990-2009 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-active, 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 Begin 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)) { if (restartBegin == Time.INDEFINITE) { ad = Time.INDEFINITE; } else { if (ad.isResolved()) { ad.value = restartBegin.value; } else { ad = new Time(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"; } } }