/// <returns>
        /// Returns whether this collection has a non-empty intersection with the inverse of the other collection
        /// </returns>
        internal bool IntersectsInverseOf(TimeIntervalCollection other)
        {
            Debug.Assert(!_invertCollection);  // Make sure we never leave inverted mode enabled

            if (this.ContainsNullPoint && !other.ContainsNullPoint)  // Intersection at null points
            {
                return true;
            }
            if (this.IsEmptyOfRealPoints)  // We are empty, and have no null point; we have nothing to intersect
            {
                return false;
            }
            else if (other.IsEmptyOfRealPoints ||  // We are non-empty, and other is the inverse of empty (e.g. covers all real numbers, so we must intersect), OR...
                     this._nodeTime[0] < other._nodeTime[0])  // Neither TIC is empty, and we start first; this means the inverted "other" by necessity
                                                              // overlaps our first node, so it must intersect either our node or subsequent interval.
            {
                return true;
            }
            else  // Neither TIC is empty, and other starts no later than we do; then use regular intersection logic with inverted boolean flags
            {
                other.SetInvertedMode(true);

                bool returnValue = IntersectsHelper(other);

                other.SetInvertedMode(false);  // Make sure we don't leave other TIC in an inverted state!

                return returnValue;
            }
        }
        // Returns true if we know at this point whether an intersection is possible between tic1 and tic2
        // The fact of whether an intersection was found is stored in the ref parameter intersectionFound
        static private bool IntersectsHelperUnequalCase(ref TimeIntervalCollection tic1, ref TimeIntervalCollection tic2, ref bool intersectionFound)
        {
            Debug.Assert(!intersectionFound);  // If an intersection was already found, we should not reach this far

            if (tic1.CurrentNodeIsInterval)  // If we are within an interval in tic1, we immediately have an intersection
            {
                // If we have gotten into this method, tic1._current comes earlier than does tic2._current;
                // Suppose the following assert is false; then by Rule #2A, tic2's previous interval must be included;
                // If this was the case, then tic2's previous interval overlapped tic1's current interval.  Since it's
                // included, we would have encountered an intersection before even reaching this method!  Then you
                // should not even be here now.  Else suppose we are at tic2's first node, then the below Assert
                // follows directly from Rule #3.
                Debug.Assert(tic2.CurrentNodeIsPoint || tic2.CurrentNodeIsInterval);

                intersectionFound = true;
                return true;
            }
            else if (tic1.CurrentIsAtLastNode)  // // If we are already at the end of tic1, we ran out of nodes that may have an intersection
            {
                intersectionFound = false;
                return true;
            }
            else  // Else we are inside a non-included interval in tic1, no intersection is possible, but keep advancing tic2._current
            {
                while (!tic2.CurrentIsAtLastNode && (tic2.NextNodeTime <= tic1.NextNodeTime))
                {
                    tic2.MoveNext();
                }

                // If nextNodeTime1 is null, we should never get here because the IF statement would have caught it and quit
                Debug.Assert(!tic1.CurrentIsAtLastNode);  // Thus tic1._current can be safely advanced now

                // Now tic1._current can be safely advanced forward
                tic1.MoveNext();

                // If we broke out of Case I, its conditional should no longer hold true:
                Debug.Assert(tic1.CurrentNodeTime >= tic2.CurrentNodeTime);

                // Enforce our invariant: neither index gets too far ahead of the other.
                Debug.Assert(tic2.CurrentIsAtLastNode || (tic1.CurrentNodeTime < tic2.NextNodeTime));
                Debug.Assert(tic1.CurrentIsAtLastNode || (tic2.CurrentNodeTime < tic1.NextNodeTime));

                // Tell the main algorithm to continue working
                return false;
            }
        }
        // Returns true if we know at this point whether an intersection is possible between tic1 and tic2
        // The fact of whether an intersection was found is stored in the ref parameter intersectionFound
        static private bool IntersectsHelperEqualCase(ref TimeIntervalCollection tic1, ref TimeIntervalCollection tic2, ref bool intersectionFound)
        {
            // If the nodes match exactly, check if the points are both included, or if the intervals are both included
            if ((tic1.CurrentNodeIsPoint && tic2.CurrentNodeIsPoint) ||
                (tic1.CurrentNodeIsInterval && tic2.CurrentNodeIsInterval))
            {
                intersectionFound = true;
                return true;
            }
            // We did not find an intersection, but advance whichever index has a closer next node
            else if (!tic1.CurrentIsAtLastNode && (
                tic2.CurrentIsAtLastNode || (tic1.NextNodeTime < tic2.NextNodeTime)))
            {
                tic1.MoveNext();
            }
            else if (!tic2.CurrentIsAtLastNode && (
                     tic1.CurrentIsAtLastNode || (tic2.NextNodeTime < tic1.NextNodeTime)))
            {
                tic2.MoveNext();
            }
            else if (!tic1.CurrentIsAtLastNode && !tic2.CurrentIsAtLastNode)
            {
                // If both indices have room to advance, and we haven't yet advanced either one, it must be the next nodes are also exactly equal
                Debug.Assert(tic1.NextNodeTime == tic2.NextNodeTime);

                // It is necessary to advance both indices simultaneously, otherwise we break our invariant - one will be too far ahead
                tic1.MoveNext();
                tic2.MoveNext();
            }
            else  // The only way we could get here is if both indices are pointing to the last nodes
            {
                Debug.Assert(tic1.CurrentIsAtLastNode && tic2.CurrentIsAtLastNode);

                // We have exhausted all the nodes and not found an intersection; bail
                intersectionFound = false;
                return true;
            }

            // Enforce our invariant: neither index gets too far ahead of the other.
            Debug.Assert(tic2.CurrentIsAtLastNode || (tic1.CurrentNodeTime < tic2.NextNodeTime));
            Debug.Assert(tic1.CurrentIsAtLastNode || (tic2.CurrentNodeTime < tic1.NextNodeTime));

            // Tell the main algorithm to continue working
            return false;
        }
        // This method was made separate to detect intersections with inverses when needed
        private bool IntersectsHelper(TimeIntervalCollection other)
        {
            // Make sure the indexers are starting next to each other
            IntersectsHelperPrepareIndexers(ref this, ref other);

            // The outer loop does not bail, rather we return directly from inside the loop
            bool intersectionFound = false;
            while (true)
            {
                // The inner loops iterate through the subset of a TIC

                // CASE I.
                // In this case, index1 is the dominant indexer: index2 is on its turf and we keep advancing index2 and checking for intesections
                // After this helper, index2 will no longer be ahead of index1
                if ((this.CurrentNodeTime < other.CurrentNodeTime) &&
                    IntersectsHelperUnequalCase(ref this, ref other, ref intersectionFound))
                {
                    return intersectionFound;
                }


                // CASE II.
                // In this case, index2 is the dominant indexer: index1 is on its turf and we keep advancing index1 and checking for intesections
                // After this helper, index1 will no longer be ahead of index2
                if ((this.CurrentNodeTime > other.CurrentNodeTime) &&
                    IntersectsHelperUnequalCase(ref other, ref this, ref intersectionFound))
                {
                    return intersectionFound;
                }


                // CASE III.
                // In this case, neither indexer is dominant: they are pointing to the same point in time
                // We keep doing this until the indices are no longer equal
                while (this.CurrentNodeTime == other.CurrentNodeTime)
                {
                    if (IntersectsHelperEqualCase(ref this, ref other, ref intersectionFound))
                    {
                        return intersectionFound;
                    }
                }
            }
        }
        // Make sure the indexers are starting next to each other
        static private void IntersectsHelperPrepareIndexers(ref TimeIntervalCollection tic1, ref TimeIntervalCollection tic2)
        {
            Debug.Assert(!tic1.IsEmptyOfRealPoints);  // We shouldn't reach here if either TIC is empty
            Debug.Assert(!tic2.IsEmptyOfRealPoints);

            tic1.MoveFirst();  // Point _current to the first node in both TICs
            tic2.MoveFirst();

            // First bring tic1._current and tic2._current within an interval of each other
            if (tic1.CurrentNodeTime < tic2.CurrentNodeTime)
            {
                // Keep advancing tic1._current as far as possible while keeping _nodeTime[tic1._current] < _nodeTime[tic2._current]
                while (!tic1.CurrentIsAtLastNode && (tic1.NextNodeTime <= tic2.CurrentNodeTime))
                {
                    tic1.MoveNext();
                }
            }
            else if (tic2.CurrentNodeTime < tic1.CurrentNodeTime)
            {
                // Keep advancing tic2._current as far as possible while keeping _nodeTime[tic1._current] > _nodeTime[tic2._current]
                while (!tic2.CurrentIsAtLastNode && (tic2.NextNodeTime <= tic1.CurrentNodeTime))
                {
                    tic2.MoveNext();
                }
            }
        }
        // Used for optimizing slip computation in Clock
        // This method will discard nodes beyond the first two nodes.
        // The only scenario where this method is called on a larger-than-size-2 TIC is
        // when the parent of a Media wraps around in a Repeat.  Then we only enter
        // the Media's active period on the wraparound part of the TIC, so it is the only important
        // part to leave.
        // Example: the parent has Duration=10 and RepeatBehavior=Forever.  It went from 9ms to 2ms (wraparound).
        // Our default TIC is {[0, 2], (9, 10)}.  Slipping this by 1 will change it to {[1, 2]}.  It is apparent
        // that this is the only part of the parent that actually overlaps our active zone.
        internal TimeIntervalCollection SlipBeginningOfConnectedInterval(TimeSpan slipTime)
        {
            if (slipTime == TimeSpan.Zero)  // The no-op case
            {
                return this;
            }            

            TimeIntervalCollection slippedCollection;
            if (_count < 2 || slipTime > _nodeTime[1] - _nodeTime[0])
            {
                // slipTime > the connected duration, which basically eliminates the parent TIC interval for us;
                // This would only happen when media "outruns" the parent container, producing negative slip.
                slippedCollection = TimeIntervalCollection.Empty;
            }
            else
            {
                // Just shift the first node by slipAmount; the constructor handles the a==b case.
                slippedCollection = new TimeIntervalCollection(_nodeTime[0] + slipTime, _nodeIsPoint[0],
                                                               _nodeTime[1]           , _nodeIsPoint[1]);
            }

            if (this.ContainsNullPoint)
            {
                slippedCollection.AddNullPoint();
            }
            return slippedCollection;
        }
        /// <returns>
        /// Returns whether this collection has a non-empty intersection with the other collection
        /// </returns>
        // RUNNING TIME: O(_count) worst-case
        // IMPLEMENTATION FOR INTERSECTS(OTHER) OPERATION:
        //
        //  We implement intersection by "stacking" the two TICs atop each other and seeing if
        //  there is any point or interval common to both.  We do this by having two indexers,
        //  index1 and index2, traverse the lengths of both TICs simultaneously.  We maintain
        //  the following invariant: each indexer, when "projected" onto the other TIC than the one
        //  it actually indexes into, falls less than a node ahead of the other indexer.
        //  To rephrase intuitively, the indexers never fall out of step by having one get
        //  too far ahead of the other.
        //
        //  Example:
        //
        //  this    ----[0]----[1]--------------------[2]----[3]-----------[4]---------[5]------...
        //  other   --------------------[0]----[1]------------------[2]----------------[3]------...
        //                      ^index1
        //                               ^index2
        //
        //  Our invariant means that one of the indexed nodes either coincides exactly with
        //  the other, as is the case for nodes this[4] and other[2] in the above example,
        //  or "projects" into the other node's subsequent interval; in the above example,
        //  other[index2] projects onto the interval of this[index1].
        //
        //  At each iteration, we check for an intersection at:
        //    A) the latter of the indexed nodes, and
        //    B) the interval right after the latter indexed node
        //
        //  3 possible scenarios:
        //  CASE I.   index1 < index2  intersects if _nodeIsInterval[index1] && (_nodeIsPoint[index2] || _nodeIsInterval[index2])
        //  CASE II.  index1 > index2  intersects if _nodeIsInterval[index2] && (_nodeIsPoint[index1] || _nodeIsInterval[index1])
        //  CASE III. index1 = index2  intersects if (_nodeIsPoint[index1] && _nodeIsPoint[index2]) || (_nodeIsInterval[index1] && _nodeIsInterval[index2])
        //
        //  We say that in Case I, index1 is dominant in the sense that index2 points to a node on index1's "turf";
        //  We move index2 through index1's entire interval to check for intersections against it.  Once index2 passes
        //  index1's interval, we advance index1 as well.  Then we again check which scenario we end up in.
        //
        //  Case II is treated anti-symmetrically to Case I.
        //
        //  Case III is special, because we cannot treat it the same as Case I or II.  This is becasue we have to check
        //  for a point-point intersection, and check which indexer should be advanced next.  It is possible that both
        //  indexers need to be advanced if the next 2 nodes are also equal.
        //
        //  We continue advancing the pointers until we find an intersection or run out of nodes on either of the TICs.
        //
        internal bool Intersects(TimeIntervalCollection other)
        {
            Debug.Assert(!_invertCollection);  // Make sure we never leave inverted mode enabled

            if (this.ContainsNullPoint && other.ContainsNullPoint)  // Short-circuit null point intersections
            {
                return true;
            }
            else if (this.IsEmptyOfRealPoints || other.IsEmptyOfRealPoints)  // Only intersection with an empty TIC is at null points, which case is already handled
            {
                return false;
            }
            else  // Both TICs are non-empty and don't intersect at the null point
            {
                return IntersectsHelper(other);
            }
        }
        /// <summary>
        /// Take a single projection point and insert into the output collection.
        /// NOTE: projection should have allocated arrays.
        /// </summary>
        /// <param name="projection">The output collection.</param>
        /// <param name="activeDuration">The duration of the active period.</param>
        /// <param name="periodInTicks">The length of a simple duration in ticks.</param>
        /// <param name="isAutoReversed">Whether autoreversing is enabled</param>
        /// <param name="includeMaxPoint">Whether the fill zone forces the max point to be included.</param>
        private void ProjectionFoldPoint(ref TimeIntervalCollection projection, Nullable<TimeSpan> activeDuration,
                                         long periodInTicks, bool isAutoReversed, bool includeMaxPoint)
        {
            Debug.Assert(CurrentNodeIsPoint);  // We should only call this method when we project a legitimate point
            Debug.Assert(!CurrentNodeIsInterval);

            long currentProjection;
            if (isAutoReversed)  // Take autoreversing into account
            {
                long doublePeriod = periodInTicks << 1;
                currentProjection = CurrentNodeTime.Ticks % doublePeriod;

                if (currentProjection > periodInTicks)
                {
                    currentProjection = doublePeriod - currentProjection;
                }
            }
            else  // No autoReversing
            {
                if (includeMaxPoint && activeDuration.HasValue && CurrentNodeTime == activeDuration)
                {
                    currentProjection = periodInTicks;  // Exceptional end case: we are exactly at the last point
                }
                else
                {
                    currentProjection = CurrentNodeTime.Ticks % periodInTicks;
                }
            }

            projection.MergePoint(TimeSpan.FromTicks(currentProjection));
        }
        /// <summary>
        /// Take a single projection segment [CurrentNodeTime, NextNodeTime], break it into parts and merge the
        /// folded parts into this collection.
        /// NOTE: the TIC is normalized so beginTime = TimeSpan.Zero and we are already clipped.
        /// NOTE: projection should have allocated arrays.
        /// </summary>
        /// <param name="projection">The output projection.</param>
        /// <param name="activeDuration">The duration of the active period.</param>
        /// <param name="periodInTicks">The length of a simple duration in ticks.</param>
        /// <param name="isAutoReversed">Whether autoreversing is enabled</param>
        /// <param name="includeMaxPoint">Whether the fill zone forces the max point to be included.</param>
        private bool ProjectionFoldInterval(ref TimeIntervalCollection projection, Nullable<TimeSpan> activeDuration,
                                            long periodInTicks, bool isAutoReversed, bool includeMaxPoint)
        {
            // Project the begin point for the segment, then look if we are autoreversing or not.
            long intervalLength = (NextNodeTime - CurrentNodeTime).Ticks;
            long timeBeforeNextPeriod, currentProjection;

            // Now see how the segment falls across periodic boundaries:
            // Case 1: segment stretches across a full period (we can exit early, since we cover the entire range of values)
            // Case 2: NON-AUTEREVERSED: segment stretches across two partial periods (we need to split into two segments and insert them into the projection)
            // Case 2: AUTOREVERSED: we need to pick the larger half of the partial period and project only that half, since it fully overlaps the other.
            // Case 3: segment is fully contained within a single period (just add the segment into the projection)
            // These cases are handled very differently for AutoReversing and non-AutoReversing timelines.

            if (isAutoReversed)  // In the autoreversed case, we "fold" the segment onto itself and eliminate the redundant parts
            {
                bool beginOnReversingArc;
                long doublePeriod = periodInTicks << 1;
                currentProjection = CurrentNodeTime.Ticks % doublePeriod;

                if (currentProjection < periodInTicks)  // We are on a forward-moving segment
                {
                    beginOnReversingArc = false;
                    timeBeforeNextPeriod = periodInTicks - currentProjection;
                }
                else  // We are on a reversing segment, adjust the values accordingly
                {
                    beginOnReversingArc = true;
                    currentProjection = doublePeriod - currentProjection;
                    timeBeforeNextPeriod = currentProjection;
                }

                Debug.Assert(timeBeforeNextPeriod > 0);

                long timeAfterNextPeriod = intervalLength - timeBeforeNextPeriod;  // How much of our interval protrudes into the next period(s); this may be negative if we don't reach it.
                // See which part of the segment -- before or after part -- "dominates" when we fold them unto each other.
                if (timeAfterNextPeriod > 0)  // Case 1 or 2: we reach into the next period but don't know if we completely cover it
                {
                    bool collectionIsSaturated;
                        
                    if (timeBeforeNextPeriod >= timeAfterNextPeriod)  // Before "dominates"
                    {
                        bool includeTime = CurrentNodeIsPoint;

                        if (timeBeforeNextPeriod == timeAfterNextPeriod)  // Corner case where before and after overlap exactly, find the IsPoint union
                        {
                            includeTime = includeTime || NextNodeIsPoint;
                        }

                        if (beginOnReversingArc)
                        {
                            projection.MergeInterval(TimeSpan.Zero,                         true,
                                                     TimeSpan.FromTicks(currentProjection), includeTime);
                            collectionIsSaturated = includeTime && (currentProjection == periodInTicks);
                        }
                        else
                        {
                            projection.MergeInterval(TimeSpan.FromTicks(currentProjection), includeTime,
                                                     TimeSpan.FromTicks(periodInTicks),     true);
                            collectionIsSaturated = includeTime && (currentProjection == 0);
                        }
                    }
                    else  // After "dominates"
                    {
                        if (beginOnReversingArc)
                        {
                            long clippedTime = timeAfterNextPeriod < periodInTicks ? timeAfterNextPeriod : periodInTicks;
                            
                            projection.MergeInterval(TimeSpan.Zero,                   true,
                                                     TimeSpan.FromTicks(clippedTime), NextNodeIsPoint);
                            collectionIsSaturated = NextNodeIsPoint && (clippedTime == periodInTicks);
                        }
                        else
                        {
                            long clippedTime = timeAfterNextPeriod < periodInTicks ? periodInTicks - timeAfterNextPeriod : 0;

                            projection.MergeInterval(TimeSpan.FromTicks(clippedTime),   NextNodeIsPoint,
                                                     TimeSpan.FromTicks(periodInTicks), true);
                            collectionIsSaturated = NextNodeIsPoint && (clippedTime == 0);
                        }
                    }
                    return collectionIsSaturated;  // See if we just saturated the collection
                }
                else  // Case 3: timeAfterNextPeriod < 0, we are fully contained in the current period
                {
                    // No need to split anything, insert the interval directly
                    if (beginOnReversingArc)  // Here the nodes are reversed
                    {
                        projection.MergeInterval(TimeSpan.FromTicks(currentProjection - intervalLength), NextNodeIsPoint,
                                                 TimeSpan.FromTicks(currentProjection),                  CurrentNodeIsPoint);
                    }
                    else
                    {
                        projection.MergeInterval(TimeSpan.FromTicks(currentProjection),                  CurrentNodeIsPoint,
                                                 TimeSpan.FromTicks(currentProjection + intervalLength), NextNodeIsPoint);
                    }
                    return false;  // Keep computing the projection
                }
            }
            else  // No AutoReversing
            {                
                currentProjection = CurrentNodeTime.Ticks % periodInTicks;
                timeBeforeNextPeriod = periodInTicks - currentProjection;

                // The only way to get 0 is if we clipped by endTime which equals CurrentNodeTime, which should not have been allowed
                Debug.Assert(intervalLength > 0);

                if (intervalLength > periodInTicks)  // Case 1. We may stretch across a whole arc, even if we start from the end and wrap back around
                {
                    // Quickly transform the collection into a saturated collection
                    projection._nodeTime[0] = TimeSpan.Zero;
                    projection._nodeIsPoint[0] = true;
                    projection._nodeIsInterval[0] = true;

                    projection._nodeTime[1] = TimeSpan.FromTicks(periodInTicks);
                    projection._nodeIsPoint[1] = includeMaxPoint;
                    projection._nodeIsInterval[1] = false;

                    _count = 2;
                    return true;  // Bail early, we have the result ready
                }
                else if (intervalLength >= timeBeforeNextPeriod)  // Case 2. We stretch until the next period begins (but not long enough to cover the length of a full period)
                {
                    // Split the segment into two projected segments by wrapping around the period boundary
                    projection.MergeInterval(TimeSpan.FromTicks(currentProjection),                     CurrentNodeIsPoint,
                                             TimeSpan.FromTicks(periodInTicks),                         false);
                    if (intervalLength > timeBeforeNextPeriod)  // See if we have a legitimate interval in the second clipped part
                    {
                        projection.MergeInterval(TimeSpan.Zero,                                             true,
                                                 TimeSpan.FromTicks(intervalLength - timeBeforeNextPeriod), NextNodeIsPoint);
                    }
                    else if (NextNodeIsPoint)  // We only seem to have a point, wrapped around at zero (or in the exceptional case, at the max)
                    {
                        if (includeMaxPoint && activeDuration.HasValue && NextNodeTime == activeDuration)  // Exceptional end case: we are exactly at the last point
                        {
                            projection.MergePoint(TimeSpan.FromTicks(periodInTicks));
                        }
                        else
                        {
                            projection.MergePoint(TimeSpan.Zero);
                        }
                    }
                    return false;  // Keep computing the projection
                }
                else  // Case 3: We fall within a single period
                {
                    // No need to split anything, insert the interval directly
                    projection.MergeInterval(TimeSpan.FromTicks(currentProjection),                    CurrentNodeIsPoint,
                                             TimeSpan.FromTicks(currentProjection + intervalLength),   NextNodeIsPoint);
                    return false;  // Keep computing the projection
                }
            }
        }
        /// <summary>
        /// Performs the NORMALIZE operation, as described in the comments to the general projection function.
        /// Clip begin and end times, normalize by beginTime, scale by speedRatio.
        /// </summary>
        /// <param name="projection">The normalized collection to create.</param>
        /// <param name="beginTime">Begin time of the active period for clipping.</param>
        /// <param name="endTime">End time of the active period for clipping.</param>
        /// <param name="speedRatio">The ratio by which to scale begin and end time.</param>
        /// <param name="includeFillPeriod">Whether a non-zero fill period exists.</param>
        private void ProjectionNormalize(ref TimeIntervalCollection projection,
                                         TimeSpan beginTime, Nullable<TimeSpan> endTime, bool includeFillPeriod, double speedRatio)
        {
            Debug.Assert(!IsEmptyOfRealPoints);
            Debug.Assert(projection.IsEmpty);
            
            projection.EnsureAllocatedCapacity(this._nodeTime.Length);

            this.MoveFirst();
            projection.MoveFirst();

            // Get to the non-clipped zone; we must overlap the active zone, so we should terminate at some point.
            while (!CurrentIsAtLastNode && NextNodeTime <= beginTime)
            {
                MoveNext();
            }

            if (CurrentNodeTime < beginTime)  // This means we have an interval clipped by beginTime
            {
                if (CurrentNodeIsInterval)
                {
                    projection._count++;
                    projection.CurrentNodeTime = TimeSpan.Zero;
                    projection.CurrentNodeIsPoint = true;
                    projection.CurrentNodeIsInterval = true;
                    projection.MoveNext();
                }
                this.MoveNext();
            }

            while(_current < _count && (!endTime.HasValue || CurrentNodeTime < endTime))  // Copy the main set of segments, transforming them
            {
                double timeOffset = (double)((this.CurrentNodeTime - beginTime).Ticks);
                
                projection._count++;
                projection.CurrentNodeTime = TimeSpan.FromTicks((long)(speedRatio * timeOffset));
                projection.CurrentNodeIsPoint = this.CurrentNodeIsPoint;
                projection.CurrentNodeIsInterval = this.CurrentNodeIsInterval;

                projection.MoveNext();
                this.MoveNext();
            }

            Debug.Assert(_current > 0);  // The only way _current could stay at zero is if the collection begins at (or past) the end of active period
            if (_current < _count  // We have an interval reaching beyond the active zone, clip that interval
             && (_nodeIsInterval[_current - 1]
              || (CurrentNodeTime == endTime.Value && CurrentNodeIsPoint && includeFillPeriod)))
            {
                Debug.Assert(endTime.HasValue && CurrentNodeTime >= endTime.Value);

                double timeOffset = (double)((endTime.Value - beginTime).Ticks);

                projection._count++;
                projection.CurrentNodeTime = TimeSpan.FromTicks((long)(speedRatio * timeOffset));
                projection.CurrentNodeIsPoint = includeFillPeriod && (CurrentNodeTime > endTime.Value || CurrentNodeIsPoint);
                projection.CurrentNodeIsInterval = false;
            }
        }
        /// <summary>
        /// Performs the FOLD operation, as described in the comments to the general projection function.
        /// We assume this method is only called with a finite, non-zero period length.
        /// The TIC is normalized so beginTime = 0.
        /// NOTE: projection should have allocated arrays.
        /// </summary>
        /// <param name="projection">The output projection.</param>
        /// <param name="activeDuration">The duration of the active period.</param>
        /// <param name="periodInTicks">The length of a simple duration in ticks.</param>
        /// <param name="isAutoReversed">Whether we have auto-reversing.</param>
        /// <param name="includeMaxPoint">Whether the fill zone forces the max point to be included.</param>
        private void ProjectionFold(ref TimeIntervalCollection projection, Nullable<TimeSpan> activeDuration,
                                    long periodInTicks, bool isAutoReversed, bool includeMaxPoint)
        {
            Debug.Assert(!IsEmptyOfRealPoints);  // The entire projection process assumes we are not empty (have an intersection with the active zone).
            Debug.Assert(periodInTicks > 0);  // We do not handle the degenerate case here.

            // Find the smallest n such that _nodeTime[n+1] > beginTime; if n is the last index, then consider _nodeTime[n+1] to be infinity
            MoveFirst();
            Debug.Assert(CurrentNodeTime >= TimeSpan.Zero);  // Verify that we are already clipped

            bool quitFlag = false;

            // As we walk, we maintain the invarant that the interval BEFORE _current is not included.
            // Otherwise we handle the interval and skip the interval's last node.

            // Process the remaining points and segments
            do
            {
                if (CurrentNodeIsInterval)  // Project the interval starting here
                {
                    quitFlag = ProjectionFoldInterval(ref projection, activeDuration, periodInTicks, isAutoReversed, includeMaxPoint);  // Project and break up the clipped segment
                    _current += NextNodeIsInterval ? 1 : 2;  // Step over the next node if it's merely the end of this interval
                }
                else  // This must be a lone point; the previous interval is no included by our invariant
                {
                    Debug.Assert(CurrentNodeIsPoint);
                    ProjectionFoldPoint(ref projection, activeDuration, periodInTicks, isAutoReversed, includeMaxPoint);
                    _current++;
                }

            } while (!quitFlag && (_current < _count));
            // While we haven't run out of indices, and haven't moved past endTime
        }
        /// <returns>
        /// Returns a collection which is the projection of this collection onto the defined periodic function.
        /// </returns>
        /// <remarks>
        /// The object on which this method is called is a timeline's parent's collection of intervals.
        /// The periodic collection passed via parameters describes the active/fill periods of the timeline.
        /// The output is the projection of (this) object using the parameter function of the timeline.
        /// 
        /// We assume this function is ONLY called when this collection overlaps the active zone.
        /// 
        /// The periodic function maps values from domain to range within its activation period of [beginTime, endTime);
        /// in the fill period [endTime, endTime+fillDuration) everything maps to a constant post-fill value, and outside of
        /// those periods every value maps to null.
        /// 
        /// The projection process can be described as three major steps:
        /// 
        /// (1) NORMALIZE this collection: offset the TIC's coordinates by BeginTime and scale by SpeedRatio.
        /// 
        /// (2) FOLD this collection.  This means we convert from parent-time coordinate space into the space of
        ///     a single simpleDuration for the child.  This is equivalent to "cutting up" the parent TIC into
        ///     equal-length segments (of length Period) and overlapping them -- taking their union.  This lets us
        ///     know exactly which values inside the simpleDuration we have reached on the child.  In the case of
        ///     autoreversed timelines, we do the folding similiar to folding a strip of paper -- alternating direction.
        /// 
        /// (3) WARP the resulting collection.  We now convert from simpleDuration domain coordinates into
        ///     coordinates in the range of the timeline function.  We do this by applying the "warping" effects of
        ///     acceleration, and deceleration.
        /// 
        /// In the special case of infinite simple duration, we essentially are done after performing NORMALIZE,
        /// because no periodicity or acceleration is present.
        /// 
        /// In the ultimate degenerate case of zero duration, we terminate early and project the zero point.
        /// 
        /// </remarks>
        /// <param name="projection">An empty output projection, passed by reference to allow TIC reuse.</param>
        /// <param name="beginTime">Begin time of the periodic function.</param>
        /// <param name="endTime">The end (expiration) time of the periodic function.  Null indicates positive infinity.</param>
        /// <param name="fillDuration">The fill time appended at the end of the periodic function.  Zero indicates no fill period.  Forever indicates infinite fill period.</param>
        /// <param name="period">Length of a single iteration in the periodic collection.</param>
        /// <param name="appliedSpeedRatio">Ratio by which to scale down the periodic collection.</param>
        /// <param name="accelRatio">Ratio of the length of the accelerating portion of the iteration.</param>
        /// <param name="decelRatio">Ratio of the length of the decelerating portion of the iteration.</param>
        /// <param name="isAutoReversed">Indicates whether reversed arcs should follow after forward arcs.</param>
        internal void ProjectOntoPeriodicFunction(ref TimeIntervalCollection projection,
                                                  TimeSpan beginTime, Nullable<TimeSpan> endTime,
                                                  Duration fillDuration, Duration period,
                                                  double appliedSpeedRatio, double accelRatio, double decelRatio,
                                                  bool isAutoReversed)
        {
            Debug.Assert(projection.IsEmpty);
            Debug.Assert(!_invertCollection);  // Make sure we never leave inverted mode enabled
            Debug.Assert(!endTime.HasValue || beginTime <= endTime);     // Ensure legitimate begin/end clipping parameters

            Debug.Assert(!IsEmptyOfRealPoints);  // We assume this function is ONLY called when this collection overlaps the active zone.  So we cannot be empty.
            Debug.Assert(!endTime.HasValue || endTime >= _nodeTime[0]);  // EndTime must come at or after our first node (it can be infinite)
            Debug.Assert(_nodeTime[_count - 1] >= beginTime);  // Our last node must come at least at begin time (since we must intersect the active period)
            Debug.Assert(endTime.HasValue || fillDuration == TimeSpan.Zero);   // Either endTime is finite, or it's infinite hence we cannot have any fill zone
            Debug.Assert(!period.HasTimeSpan || period.TimeSpan > TimeSpan.Zero || (endTime.HasValue && beginTime == endTime));  // Check the consistency of degenerate case where simple duration is zero; expiration time should equal beginTime
            Debug.Assert(!_nodeIsInterval[_count - 1]);  // We should not have an infinite domain set

            // We initially project all intervals into a single period of the timeline, creating a union of the projected segments.
            // Then we warp the time coordinates of the resulting TIC from domain to range, applying the effects of speed/accel/decel

            bool nullPoint = _containsNullPoint  // Start by projecting the null point directly, then check whether we fall anywhere outside of the active and fill period

             || _nodeTime[0] < beginTime  // If we intersect space before beginTime, or...

              || (endTime.HasValue && fillDuration.HasTimeSpan  // ...the active and fill periods don't stretch forever, and...
               && (_nodeTime[_count - 1] > endTime.Value + fillDuration.TimeSpan  // ...we intersect space after endTime+fill, or...
                || (_nodeTime[_count - 1] == endTime.Value + fillDuration.TimeSpan  // ...as we fall right onto the end of fill zone...
                 && _nodeIsPoint[_count - 1] && (endTime > beginTime || fillDuration.TimeSpan > TimeSpan.Zero))));  // ...we may have a point intersection with the stopped zone

            // Now consider the main scenarios:

            if (endTime.HasValue && beginTime == endTime)  // Degenerate case when our active period is a single point; project only the point
            {
                projection.InitializePoint(TimeSpan.Zero);
            }
            else  // The case of non-zero active duration
            {
                bool includeFillPeriod = !fillDuration.HasTimeSpan || fillDuration.TimeSpan > TimeSpan.Zero;  // This variable represents whether we have a non-zero fill zone

                if (period.HasTimeSpan)  // We have a finite TimeSpan period and non-zero activation duration
                {
                    TimeIntervalCollection tempCollection = new TimeIntervalCollection();

                    ProjectionNormalize(ref tempCollection, beginTime, endTime, includeFillPeriod, appliedSpeedRatio);

                    long periodInTicks = period.TimeSpan.Ticks;
                    Nullable<TimeSpan> activeDuration;
                    bool includeMaxPoint;

                    if (endTime.HasValue)
                    {
                        activeDuration = endTime.Value - beginTime;
                        includeMaxPoint = includeFillPeriod && (activeDuration.Value.Ticks % periodInTicks == 0);  // Fill starts at a boundary
                    }
                    else
                    {
                        activeDuration = null;
                        includeMaxPoint = false;
                    }

                    projection.EnsureAllocatedCapacity(_minimumCapacity);
                    tempCollection.ProjectionFold(ref projection, activeDuration, periodInTicks, isAutoReversed, includeMaxPoint);

                    if (accelRatio + decelRatio > 0)
                    {
                        projection.ProjectionWarp(periodInTicks, accelRatio, decelRatio);
                    }
                }
                else  // Infinite period degenerate case; we perform straight 1-1 linear mapping, offset by begin time and clipped
                {
                    ProjectionNormalize(ref projection, beginTime, endTime, includeFillPeriod, appliedSpeedRatio);
                }
            }

            projection._containsNullPoint = nullPoint;  // Ensure we have the null point properly set
        }
        /// <summary>
        /// Used for projecting the end of a fill period.  When calling, we already know that we intersect the fill period
        /// but not the active period.
        /// </summary>
        /// <returns>
        /// Returns a collection which is the projection of the argument point onto the defined periodic function.
        /// </returns>
        /// <param name="projection">An empty output projection, passed by reference to allow TIC reuse.</param>
        /// <param name="beginTime">Begin time of the periodic function.</param>
        /// <param name="endTime">The end (expiration) time of the periodic function.</param>
        /// <param name="period">Length of a single iteration in the periodic collection.</param>
        /// <param name="appliedSpeedRatio">Ratio by which to scale down the periodic collection.</param>
        /// <param name="accelRatio">Ratio of the length of the accelerating portion of the iteration.</param>
        /// <param name="decelRatio">Ratio of the length of the decelerating portion of the iteration.</param>
        /// <param name="isAutoReversed">Indicates whether reversed arcs should follow after forward arcs.</param>
        internal void ProjectPostFillZone(ref TimeIntervalCollection projection,
                                          TimeSpan beginTime, TimeSpan endTime, Duration period,
                                          double appliedSpeedRatio, double accelRatio, double decelRatio,
                                          bool isAutoReversed)
        {
            Debug.Assert(projection.IsEmpty);  // Make sure the projection was properly cleared first
            Debug.Assert(!_invertCollection);  // Make sure we never leave inverted mode enabled
            Debug.Assert(beginTime <= endTime);     // Ensure legitimate begin/end clipping parameters

            Debug.Assert(!IsEmptyOfRealPoints);  // We assume this function is ONLY called when this collection overlaps the postfill zone.  So we cannot be empty.
            Debug.Assert(!period.HasTimeSpan || period.TimeSpan > TimeSpan.Zero || beginTime == endTime);  // Check the consistency of degenerate case where simple duration is zero; expiration time should equal beginTime
            Debug.Assert(!_nodeIsInterval[_count - 1]);  // We should not have an infinite domain set

            long outputInTicks;

            if (beginTime == endTime)  // Degenerate case when our active period is a single point; project only that point
            {
                outputInTicks = 0;
            }
            else  // The case of non-zero active duration
            {
                outputInTicks = (long)(appliedSpeedRatio * (double)(endTime - beginTime).Ticks);

                if (period.HasTimeSpan)  // Case of finite simple duration; in the infinite case we are already done
                {
                    long periodInTicks = period.TimeSpan.Ticks;  // Start by folding the point into its place inside a simple duration

                    if (isAutoReversed)
                    {
                        long doublePeriod = periodInTicks << 1;  // Fast multiply by 2
                        outputInTicks = outputInTicks % doublePeriod;

                        if (outputInTicks > periodInTicks)
                        {
                            outputInTicks = doublePeriod - outputInTicks;
                        }
                    }
                    else
                    {
                        outputInTicks = outputInTicks % periodInTicks;
                        if (outputInTicks == 0)
                        {
                            outputInTicks = periodInTicks;  // If we are at the end, stick to the max value
                        }
                    }

                    if (accelRatio + decelRatio > 0)  // Now if we have acceleration, warp the point by the correct amount
                    {
                        double dpPeriod = (double)periodInTicks;
                        double inversePeriod = 1 / dpPeriod;
                        double halfMaxRate = 1 / (2 - accelRatio - decelRatio);  // Constants to simplify 
                        double t;

                        long accelEnd = (long)(dpPeriod * accelRatio);
                        long decelStart = periodInTicks - (long)(dpPeriod * decelRatio);

                        if (outputInTicks < accelEnd)  // We are in accel zone
                        {
                            t = (double)outputInTicks;
                            outputInTicks = (long)(halfMaxRate * inversePeriod * t * t / accelRatio);
                        }
                        else if (outputInTicks <= decelStart)  // We are in the linear zone
                        {
                            t = (double)outputInTicks;
                            outputInTicks = (long)(halfMaxRate * (2 * t - accelRatio));
                        }
                        else  // We are in decel zone
                        {
                            t = (double)(periodInTicks - outputInTicks);
                            outputInTicks = periodInTicks - (long)(halfMaxRate * inversePeriod * t * t / decelRatio);
                        }
                    }
                }
            }

            projection.InitializePoint(TimeSpan.FromTicks(outputInTicks));
        }
Exemplo n.º 14
0
 internal virtual void ComputeCurrentFillInterval(TimeIntervalCollection parentIntervalCollection,
                                                  TimeSpan beginTime, TimeSpan endTime, Duration period,
                                                  double appliedSpeedRatio, double accelRatio, double decelRatio,
                                                  bool isAutoReversed)
 {
 }
Exemplo n.º 15
0
 internal override void ComputeCurrentFillInterval(TimeIntervalCollection parentIntervalCollection,
                                                   TimeSpan beginTime, TimeSpan endTime, Duration period,
                                                   double appliedSpeedRatio, double accelRatio, double decelRatio,
                                                   bool isAutoReversed)
 {
     _currentIntervals.Clear();
     parentIntervalCollection.ProjectPostFillZone(ref _currentIntervals,
                                                  beginTime, endTime,
                                                  period, appliedSpeedRatio,
                                                  accelRatio, decelRatio, isAutoReversed);
 }
Exemplo n.º 16
0
 internal override void ComputeCurrentIntervals(TimeIntervalCollection parentIntervalCollection,
                                                TimeSpan beginTime, TimeSpan? endTime,
                                                Duration fillDuration, Duration period,
                                                double appliedSpeedRatio, double accelRatio, double decelRatio,
                                                bool isAutoReversed)
 {
     _currentIntervals.Clear();
     parentIntervalCollection.ProjectOntoPeriodicFunction(ref _currentIntervals,
                                                          beginTime, endTime,
                                                          fillDuration, period, appliedSpeedRatio,
                                                          accelRatio, decelRatio, isAutoReversed);
 }