/// <summary>
        /// Enter the calibration validation mode and starts subscribing to gaze data from the eye tracker.
        /// </summary>
        public void EnterValidationMode()
        {
            if (State != ValidationState.NotInValidationMode)
            {
                throw new InvalidOperationException("Validation mode already entered");
            }

            _dataMap = new List <KeyValuePair <NormalizedPoint2D, Queue <GazeDataEventArgs> > >();
            _result.UpdateResult(new List <CalibrationValidationPoint>(), float.NaN, float.NaN, float.NaN, float.NaN, float.NaN, float.NaN);
            State = ValidationState.NotCollectingData;
            _eyeTracker.GazeDataReceived += OnGazeDataReceived;
        }
        public CalibrationValidationResult Compute()
        {
            if (IsCollectingData)
            {
                throw new InvalidOperationException("Compute called while collecting data");
            }

            var points = new List <CalibrationValidationPoint>();

            foreach (var kv in _dataMap)
            {
                var targetPoint2D = kv.Key;
                var samples       = kv.Value;

                var targetPoint3D = targetPoint2D.NormalizedPoint2DToPoint3D(_eyeTracker.GetDisplayArea());

                if (samples.Count < _sampleCount)
                {
                    // We timed out before collecting enough valid samples.
                    // Set the timeout flag and continue.
                    points.Add(new CalibrationValidationPoint(targetPoint2D, -1, -1, -1, -1, -1, -1, true, samples.ToArray()));
                    continue;
                }

                var gazePointAverageLeft   = samples.Average(s => s.LeftEye.GazePoint.PositionInUserCoordinates);
                var gazePointAverageRight  = samples.Average(s => s.RightEye.GazePoint.PositionInUserCoordinates);
                var gazeOriginAverageLeft  = samples.Average(s => s.LeftEye.GazeOrigin.PositionInUserCoordinates);
                var gazeOriginAverageRight = samples.Average(s => s.RightEye.GazeOrigin.PositionInUserCoordinates);

                var directionGazePointLeft = gazeOriginAverageLeft.NormalizedDirection(gazePointAverageLeft);
                var directionTargetLeft    = gazeOriginAverageLeft.NormalizedDirection(targetPoint3D);
                var accuracyLeftEye        = directionTargetLeft.Angle(directionGazePointLeft);

                var directionGazePointRight = gazeOriginAverageRight.NormalizedDirection(gazePointAverageRight);
                var directionTargetRight    = gazeOriginAverageRight.NormalizedDirection(targetPoint3D);
                var accuracyRightEye        = directionTargetRight.Angle(directionGazePointRight);

                var varianceLeft = samples.Select(s => Math.Pow(s
                                                                .LeftEye.GazeOrigin.PositionInUserCoordinates.NormalizedDirection(s.LeftEye.GazePoint.PositionInUserCoordinates)
                                                                .Angle(s.LeftEye.GazeOrigin.PositionInUserCoordinates.NormalizedDirection(gazePointAverageLeft)), 2)).Average();

                var varianceRight = samples.Select(s => Math.Pow(s
                                                                 .RightEye.GazeOrigin.PositionInUserCoordinates.NormalizedDirection(s.RightEye.GazePoint.PositionInUserCoordinates)
                                                                 .Angle(s.RightEye.GazeOrigin.PositionInUserCoordinates.NormalizedDirection(gazePointAverageRight)), 2)).Average();

                var precisionLeftEye     = varianceLeft > 0 ? Math.Sqrt(varianceLeft) : 0;
                var precisionRightEye    = varianceRight > 0 ? Math.Sqrt(varianceRight) : 0;
                var precisionLeftEyeRMS  = samples.RootMeanSquare(s => s.LeftEye);
                var precisionRightEyeRMS = samples.RootMeanSquare(s => s.RightEye);

                points.Add(new CalibrationValidationPoint(
                               targetPoint2D,
                               (float)accuracyLeftEye,
                               (float)precisionLeftEye,
                               (float)accuracyRightEye,
                               (float)precisionRightEye,
                               (float)precisionLeftEyeRMS,
                               (float)precisionRightEyeRMS,
                               false,
                               samples.ToArray()));
            }

            if (points.Count == 0)
            {
                _latestResult.UpdateResult(points, 0, 0, 0);
            }
            else
            {
                var validPoints = points.Where(p => !p.TimedOut);

                if (validPoints.Count() == 0)
                {
                    _latestResult.UpdateResult(points, 0, 0, 0);
                }
                else
                {
                    var avaragePrecisionLeftEye     = validPoints.Select(p => p.PrecisionLeftEye).Average();
                    var avaragePrecisionRightEye    = validPoints.Select(p => p.PrecisionRightEye).Average();
                    var avarageAccuracyLeftEye      = validPoints.Select(p => p.AccuracyLeftEye).Average();
                    var avarageAccuracyRightEye     = validPoints.Select(p => p.AccuracyRightEye).Average();
                    var averagePrecisionLeftEyeRMS  = validPoints.Select(p => p.PrecisionLeftEyeRMS).Average();
                    var averagePrecisionRightEyeRMS = validPoints.Select(p => p.PrecisionRightEyeRMS).Average();

                    _latestResult.UpdateResult(
                        points,
                        (avarageAccuracyLeftEye + avarageAccuracyRightEye) / 2.0f,
                        (avaragePrecisionLeftEye + avaragePrecisionRightEye) / 2.0f,
                        (averagePrecisionLeftEyeRMS + averagePrecisionRightEyeRMS) / 2.0f);
                }
            }

            return(_latestResult);
        }