1/*
2 * Copyright (C) 2008 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "SMILTimeContainer.h"
28
29#if ENABLE(SVG)
30#include "Document.h"
31#include "NodeTraversal.h"
32#include "SVGNames.h"
33#include "SVGSMILElement.h"
34#include "SVGSVGElement.h"
35#include <wtf/CurrentTime.h>
36
37using namespace std;
38
39namespace WebCore {
40
41static const double animationFrameDelay = 0.025;
42
43SMILTimeContainer::SMILTimeContainer(SVGSVGElement* owner)
44    : m_beginTime(0)
45    , m_pauseTime(0)
46    , m_accumulatedPauseTime(0)
47    , m_presetStartTime(0)
48    , m_documentOrderIndexesDirty(false)
49    , m_timer(this, &SMILTimeContainer::timerFired)
50    , m_ownerSVGElement(owner)
51#ifndef NDEBUG
52    , m_preventScheduledAnimationsChanges(false)
53#endif
54{
55}
56
57SMILTimeContainer::~SMILTimeContainer()
58{
59#ifndef NDEBUG
60    ASSERT(!m_preventScheduledAnimationsChanges);
61#endif
62}
63
64void SMILTimeContainer::schedule(SVGSMILElement* animation, SVGElement* target, const QualifiedName& attributeName)
65{
66    ASSERT(animation->timeContainer() == this);
67    ASSERT(target);
68    ASSERT(animation->hasValidAttributeName());
69
70#ifndef NDEBUG
71    ASSERT(!m_preventScheduledAnimationsChanges);
72#endif
73
74    ElementAttributePair key(target, attributeName);
75    OwnPtr<AnimationsVector>& scheduled = m_scheduledAnimations.add(key, nullptr).iterator->value;
76    if (!scheduled)
77        scheduled = adoptPtr(new AnimationsVector);
78    ASSERT(!scheduled->contains(animation));
79    scheduled->append(animation);
80
81    SMILTime nextFireTime = animation->nextProgressTime();
82    if (nextFireTime.isFinite())
83        notifyIntervalsChanged();
84}
85
86void SMILTimeContainer::unschedule(SVGSMILElement* animation, SVGElement* target, const QualifiedName& attributeName)
87{
88    ASSERT(animation->timeContainer() == this);
89
90#ifndef NDEBUG
91    ASSERT(!m_preventScheduledAnimationsChanges);
92#endif
93
94    ElementAttributePair key(target, attributeName);
95    AnimationsVector* scheduled = m_scheduledAnimations.get(key);
96    ASSERT(scheduled);
97    size_t idx = scheduled->find(animation);
98    ASSERT(idx != notFound);
99    scheduled->remove(idx);
100}
101
102void SMILTimeContainer::notifyIntervalsChanged()
103{
104    // Schedule updateAnimations() to be called asynchronously so multiple intervals
105    // can change with updateAnimations() only called once at the end.
106    startTimer(0);
107}
108
109SMILTime SMILTimeContainer::elapsed() const
110{
111    if (!m_beginTime)
112        return 0;
113    return currentTime() - m_beginTime - m_accumulatedPauseTime;
114}
115
116bool SMILTimeContainer::isActive() const
117{
118    return m_beginTime && !isPaused();
119}
120
121bool SMILTimeContainer::isPaused() const
122{
123    return m_pauseTime;
124}
125
126bool SMILTimeContainer::isStarted() const
127{
128    return m_beginTime;
129}
130
131void SMILTimeContainer::begin()
132{
133    ASSERT(!m_beginTime);
134    double now = currentTime();
135
136    // If 'm_presetStartTime' is set, the timeline was modified via setElapsed() before the document began.
137    // In this case pass on 'seekToTime=true' to updateAnimations().
138    m_beginTime = now - m_presetStartTime;
139    updateAnimations(SMILTime(m_presetStartTime), m_presetStartTime ? true : false);
140    m_presetStartTime = 0;
141
142    if (m_pauseTime) {
143        m_pauseTime = now;
144        m_timer.stop();
145    }
146}
147
148void SMILTimeContainer::pause()
149{
150    ASSERT(!isPaused());
151    m_pauseTime = currentTime();
152
153    if (m_beginTime)
154        m_timer.stop();
155}
156
157void SMILTimeContainer::resume()
158{
159    ASSERT(isPaused());
160
161    if (m_beginTime)
162        m_accumulatedPauseTime += currentTime() - m_pauseTime;
163
164    m_pauseTime = 0;
165    startTimer(0);
166}
167
168void SMILTimeContainer::setElapsed(SMILTime time)
169{
170    // If the documment didn't begin yet, record a new start time, we'll seek to once its possible.
171    if (!m_beginTime) {
172        m_presetStartTime = time.value();
173        return;
174    }
175
176    if (m_beginTime)
177        m_timer.stop();
178
179    double now = currentTime();
180    m_beginTime = now - time.value();
181
182    m_accumulatedPauseTime = 0;
183    if (m_pauseTime)
184        m_pauseTime = now;
185
186#ifndef NDEBUG
187    m_preventScheduledAnimationsChanges = true;
188#endif
189    GroupedAnimationsMap::iterator end = m_scheduledAnimations.end();
190    for (GroupedAnimationsMap::iterator it = m_scheduledAnimations.begin(); it != end; ++it) {
191        AnimationsVector* scheduled = it->value.get();
192        unsigned size = scheduled->size();
193        for (unsigned n = 0; n < size; n++)
194            scheduled->at(n)->reset();
195    }
196#ifndef NDEBUG
197    m_preventScheduledAnimationsChanges = false;
198#endif
199
200    updateAnimations(time, true);
201}
202
203void SMILTimeContainer::startTimer(SMILTime fireTime, SMILTime minimumDelay)
204{
205    if (!m_beginTime || isPaused())
206        return;
207
208    if (!fireTime.isFinite())
209        return;
210
211    SMILTime delay = max(fireTime - elapsed(), minimumDelay);
212    m_timer.startOneShot(delay.value());
213}
214
215void SMILTimeContainer::timerFired(Timer<SMILTimeContainer>*)
216{
217    ASSERT(m_beginTime);
218    ASSERT(!m_pauseTime);
219    updateAnimations(elapsed());
220}
221
222void SMILTimeContainer::updateDocumentOrderIndexes()
223{
224    unsigned timingElementCount = 0;
225    for (Element* element = m_ownerSVGElement; element; element = ElementTraversal::next(element, m_ownerSVGElement)) {
226        if (SVGSMILElement::isSMILElement(element))
227            static_cast<SVGSMILElement*>(element)->setDocumentOrderIndex(timingElementCount++);
228    }
229    m_documentOrderIndexesDirty = false;
230}
231
232struct PriorityCompare {
233    PriorityCompare(SMILTime elapsed) : m_elapsed(elapsed) {}
234    bool operator()(SVGSMILElement* a, SVGSMILElement* b)
235    {
236        // FIXME: This should also consider possible timing relations between the elements.
237        SMILTime aBegin = a->intervalBegin();
238        SMILTime bBegin = b->intervalBegin();
239        // Frozen elements need to be prioritized based on their previous interval.
240        aBegin = a->isFrozen() && m_elapsed < aBegin ? a->previousIntervalBegin() : aBegin;
241        bBegin = b->isFrozen() && m_elapsed < bBegin ? b->previousIntervalBegin() : bBegin;
242        if (aBegin == bBegin)
243            return a->documentOrderIndex() < b->documentOrderIndex();
244        return aBegin < bBegin;
245    }
246    SMILTime m_elapsed;
247};
248
249void SMILTimeContainer::sortByPriority(Vector<SVGSMILElement*>& smilElements, SMILTime elapsed)
250{
251    if (m_documentOrderIndexesDirty)
252        updateDocumentOrderIndexes();
253    std::sort(smilElements.begin(), smilElements.end(), PriorityCompare(elapsed));
254}
255
256void SMILTimeContainer::updateAnimations(SMILTime elapsed, bool seekToTime)
257{
258    SMILTime earliestFireTime = SMILTime::unresolved();
259
260#ifndef NDEBUG
261    // This boolean will catch any attempts to schedule/unschedule scheduledAnimations during this critical section.
262    // Similarly, any elements removed will unschedule themselves, so this will catch modification of animationsToApply.
263    m_preventScheduledAnimationsChanges = true;
264#endif
265
266    AnimationsVector animationsToApply;
267    GroupedAnimationsMap::iterator end = m_scheduledAnimations.end();
268    for (GroupedAnimationsMap::iterator it = m_scheduledAnimations.begin(); it != end; ++it) {
269        AnimationsVector* scheduled = it->value.get();
270
271        // Sort according to priority. Elements with later begin time have higher priority.
272        // In case of a tie, document order decides.
273        // FIXME: This should also consider timing relationships between the elements. Dependents
274        // have higher priority.
275        sortByPriority(*scheduled, elapsed);
276
277        SVGSMILElement* resultElement = 0;
278        unsigned size = scheduled->size();
279        for (unsigned n = 0; n < size; n++) {
280            SVGSMILElement* animation = scheduled->at(n);
281            ASSERT(animation->timeContainer() == this);
282            ASSERT(animation->targetElement());
283            ASSERT(animation->hasValidAttributeName());
284
285            // Results are accumulated to the first animation that animates and contributes to a particular element/attribute pair.
286            if (!resultElement) {
287                if (!animation->hasValidAttributeType())
288                    continue;
289                resultElement = animation;
290            }
291
292            // This will calculate the contribution from the animation and add it to the resultsElement.
293            if (!animation->progress(elapsed, resultElement, seekToTime) && resultElement == animation)
294                resultElement = 0;
295
296            SMILTime nextFireTime = animation->nextProgressTime();
297            if (nextFireTime.isFinite())
298                earliestFireTime = min(nextFireTime, earliestFireTime);
299        }
300
301        if (resultElement)
302            animationsToApply.append(resultElement);
303    }
304
305    unsigned animationsToApplySize = animationsToApply.size();
306    if (!animationsToApplySize) {
307#ifndef NDEBUG
308        m_preventScheduledAnimationsChanges = false;
309#endif
310        startTimer(earliestFireTime, animationFrameDelay);
311        return;
312    }
313
314    // Apply results to target elements.
315    for (unsigned i = 0; i < animationsToApplySize; ++i)
316        animationsToApply[i]->applyResultsToTarget();
317
318#ifndef NDEBUG
319    m_preventScheduledAnimationsChanges = false;
320#endif
321
322    startTimer(earliestFireTime, animationFrameDelay);
323}
324
325}
326
327#endif // ENABLE(SVG)
328