public SingleEyeGazeData SelectSingleEye(GazeData gaze)
        {
            var average = (gaze.LeftEye.Validity == EyeValidity.Valid && gaze.RightEye.Validity == EyeValidity.Valid)
                          ? new EyeData(EyeValidity.Valid, EyeSampleUtils.Average(gaze.LeftEye, gaze.RightEye))
                          : EyeData.Default;

            return(new SingleEyeGazeData(average, gaze.Timestamp));
        }
        // .-|.-|.-|
        public static IObservable <EyeMovement> MergeAdjacentFixations(this IObservable <EyeMovement> movements, TimeSpan maxTimeBetweenFixations, double maxAngleBetweenFixations)
        {
            return(movements.Buffer(m => m.MovementType == EyeMovementType.Fixation)
                   .Scan((aggregate, buffer) =>
            {
                if (aggregate.Any())
                {
                    var lastFixation = aggregate.FirstOrDefault(m => m.MovementType == EyeMovementType.Fixation);
                    var nextFixation = buffer.FirstOrDefault(m => m.MovementType == EyeMovementType.Fixation);
                    var nextMovements = buffer.Except(new[] { nextFixation }).ToArray();

                    if (lastFixation != null && nextFixation != null)
                    {
                        var timeBetweenFixations = (nextFixation.Timestamp - lastFixation.EndTimestamp);
                        if (timeBetweenFixations <= maxTimeBetweenFixations)
                        {
                            var lastSample = lastFixation.Samples.Last().Eye;
                            var nextSample = nextFixation.Samples.First().Eye;

                            var averageSample = EyeSampleUtils.Average(lastSample, nextSample);

                            double angle = averageSample.GetVisualAngle(lastFixation.AverageSample, nextFixation.AverageSample);

                            if (angle <= maxAngleBetweenFixations)
                            {
                                var mergedSamples = aggregate.SelectMany(m => m.Samples).Concat(nextFixation.Samples);
                                var newAverageSample = EyeSampleUtils.Average(lastFixation.Samples.Concat(nextFixation.Samples).Select(s => s.Eye));

                                // merge
                                var mergedFixation = new List <EyeMovement>()
                                {
                                    new EyeMovement
                                    (
                                        EyeMovementType.Fixation,
                                        mergedSamples,
                                        newAverageSample,
                                        lastFixation.Timestamp,
                                        nextFixation.EndTimestamp
                                    )
                                };

                                if (nextMovements != null && nextMovements.Any())
                                {
                                    mergedFixation.AddRange(nextMovements);
                                }
                                return mergedFixation;
                            }
                        }
                    }
                }
                return buffer;
            })
                   .Where(b => b.Any())
                   .Buffer((first, current) => first.First().Timestamp != current.First().Timestamp)
                   .Where(b => b.Any())
                   .Select(b => b.Last())
                   .SelectMany(b => b));
        }
        public static IObservable <EyeMovement> ClassifyMovements(this IObservable <EyeVelocity> velocities, double velocityThreshold)
        {
            return(velocities.Buffer((first, current) => ClassifyMovement(first, velocityThreshold) != ClassifyMovement(current, velocityThreshold))
                   .Where(b => b.Any())
                   .Buffer(2, 1)
                   .Scan <IList <IList <EyeVelocity> >, EyeMovement>(null, (movement, buffers) =>
            {
                var current = buffers.First();
                var last = buffers.LastOrDefault();

                var currentFirstSample = current.First();
                DateTimeOffset startTimestamp = currentFirstSample.Eye.Timestamp;

                if (movement != null)
                {
                    var startTicksDiff = movement.EndTimestamp - movement.Samples.Last().Eye.Timestamp;

                    startTimestamp = startTimestamp.Subtract(startTicksDiff.Duration());
                }

                var currentLastSample = current.Last();
                DateTimeOffset endTimestamp = currentLastSample.Eye.Timestamp;

                if (last != null && last != current)
                {
                    var endTicksDiff = (last.First().Eye.Timestamp - endTimestamp).Duration().Ticks / 2;

                    endTimestamp = endTimestamp.Add(TimeSpan.FromTicks(endTicksDiff));
                }

                EyeMovementType movementType = ClassifyMovement(current.First(), velocityThreshold);

                EyeSample averageSample = null;
                if (movementType == EyeMovementType.Fixation)
                {
                    averageSample = EyeSampleUtils.Average(current.Select(s => s.Eye));
                }

                return new EyeMovement(movementType, current, averageSample, startTimestamp, endTimestamp);
            }));
        }