public void ShouldHaveConsistentDistance(
     [Random(-1000, 1000.0, 5)] double lat,
     [Random(-1000.0, 1000.0, 5)] double lon,
     [Random(-360.0, 360.0, 10)] double bearing,
     [Random(0.0, 1000.0, 10)] double distance)
 {
     var sourceGeo = new Geo(lat, lon);
     var destGeo = sourceGeo.Offset(Geo.KilometersToRadians(distance), Geo.DegreesToRadians(bearing));
     Assert.That(sourceGeo.DistanceKilometers(destGeo), Is.EqualTo(distance).Within(0.0003));
 }
 /// <summary>
 /// Bounce a platform around inside a given perimeter
 /// </summary>
 /// <param name="perimeter">A GeoArray of points in the perimeter, which must satisfy certain conditions (no segments may cross and the polygon 
 /// must be closed)</param>
 /// <param name="startPoint">A Geo containing the starting point.  If this is null, a point inside the perimeter is chosen randomly</param>
 /// <param name="initialCourse">The initial course, in radians from true north.  If startPoint is null, or if initialCourse is NaN, this is 
 /// chosen randomly</param>
 /// <param name="minimumCourseLength">The minimum length of the returned course, in meters</param>
 /// <returns>A GeoArray containing the start location and each point where the course bounces off the perimeter</returns>
 public static GeoArray PerimeterBounce(this GeoArray perimeter, Geo startPoint, double initialCourse, double minimumCourseLength)
 {
     if (!perimeter.IsClosed) throw new InvalidPerimeterException("Perimeter is not closed");
     if (perimeter.HasCrossingSegments) throw new InvalidPerimeterException("Perimeter is not a simple polygon (segments cross each other)");
     var points = new List<Geo>();
     while ((startPoint == null) || (!startPoint.IsInside(perimeter)))
     {
         var distance = Random.NextDouble() * perimeter.BoundingCircle.Radius;
         var azimuth = Random.NextDouble() * MoreMath.TwoPi;
         startPoint = perimeter.Center.Offset(distance, azimuth);
     }
     points.Add(startPoint);
     if (double.IsNaN(initialCourse)) initialCourse = Random.NextDouble()*MoreMath.TwoPi;
     var courseSegment = new GeoSegment(startPoint, startPoint.Offset(Geo.KilometersToRadians(0.001), initialCourse)).Scale(1000 * Geo.RadiansToKilometers(MoreMath.PiOverTwo));
     var bounceCount = 1;
     while (minimumCourseLength > 0)
     {
         var minIntersectionRange = double.MaxValue;
         GeoSegment firstIntersectingSegment = null;
         Geo firstIntersection = null;
         foreach (var segment in perimeter.Segments)
         {
             var intersection = courseSegment.Intersection(segment);
             if (intersection == null) continue;
             var curIntersectionRange = startPoint.DistanceRadians(intersection);
             if (curIntersectionRange < Geo.KilometersToRadians(0.001) || curIntersectionRange >= minIntersectionRange) continue;
             minIntersectionRange = curIntersectionRange;
             firstIntersectingSegment = segment;
             firstIntersection = intersection;
         }
         if (firstIntersection == null) throw new PerimeterBounceException(string.Format("Course segment failed to intersect the perimeter on bounce {0}", bounceCount));
         var actualCourseSegment = new GeoSegment(points.Last(), firstIntersection);
         minimumCourseLength -= Geo.RadiansToKilometers(actualCourseSegment.LengthRadians) * 1000;
         points.Add(firstIntersection);
         var reflectedSegment = courseSegment.Reflect(firstIntersectingSegment);
         var reflectionAzimuth = reflectedSegment[0].Azimuth(reflectedSegment[1]);
         courseSegment = new GeoSegment(reflectedSegment[1], 1000 * Geo.RadiansToKilometers(MoreMath.PiOverTwo), Geo.RadiansToDegrees(reflectionAzimuth));
         bounceCount++;
     }
     return new GeoArray(points);
 }
        public void Inflations()
       {
           var rect = new GeoRect(44, 41, -69, -72);
           var nw = new Geo(rect.North, rect.West,true);
           var se = new Geo(rect.South, rect.East,true);

           var kmToInflate = 10.0f;

           nw = nw.Offset(Geo.KilometersToRadians(Math.Sqrt(2) * kmToInflate), Geo.DegreesToRadians(315));
           se = se.Offset(Geo.KilometersToRadians(Math.Sqrt(2) * kmToInflate), Geo.DegreesToRadians(135));

           var rect1 = new GeoRect(nw.Latitude,se.Latitude,se.Longitude,nw.Longitude);

           var inflatedRect = GeoRect.Inflate(rect, kmToInflate);
           Assert.IsTrue(rect1.Equals(inflatedRect));
       }
        /// <summary>
        /// Takes a start location and a proposed end location, and returns a reflected end location
        /// If the proposed end location is outside of the current figure, the end location is reflected
        /// by the normal to the intersecting segment, and returned to the caller.  If the proposed end
        /// location is inside the current figure, then it is simply returned to the caller.
        /// </summary>
        /// <param name="startLocation">Start location for the current proposed move</param>
        /// <param name="proposedEndLocation">End location for the current proposed move</param>
        /// <param name="proposedCourse"> </param>
        /// <returns>Actual end location that remains inside the figure, which may be reflected from the
        /// proposed end location provided if the proposed end location lies outside the figure</returns>
        public override Geo Reflect(Geo startLocation, Geo proposedEndLocation, out Course proposedCourse)
        {
            //Geo start = new Geo(StartLocation);
            //Geo end = new Geo(ProposedEndLocation);
#if MATLAB_DEBUG_OUTPUT
            MatlabDumpVertices();
#endif
            proposedCourse = new Course(startLocation, proposedEndLocation);
            if (Contains(proposedEndLocation))
                return proposedEndLocation;

            //start.Move(ProposedCourse.ReciprocalDegrees, 1000);
            //end.Move(ProposedCourse.Degrees, 1000);
#if MATLAB_DEBUG_OUTPUT
            Console.WriteLine("Course=zeros(2,2);\nCourse(1,:)=[{0} {1}];\nCourse(2,:)=[{2} {3}];",
                startLocation.Longitude, startLocation.Latitude, 
                proposedEndLocation.Longitude, proposedEndLocation.Latitude);
#endif
            
            var proposedCourseSegment = new OverlayLineSegment(startLocation, proposedEndLocation);
            for (var i = 0; i < Segments.Count(); i++)
            {
                var intersect = proposedCourseSegment.IntersectionPoint(Segments[i]);
#if MATLAB_DEBUG_OUTPUT
                Console.WriteLine("Intersects({0},:)=[{1} {2}];", i + 1, intersect.Longitude, intersect.Latitude);
#endif
                //if (intersect == null) continue;
                if (!proposedCourseSegment.Contains(intersect) || !Segments[i].Contains(intersect)) continue;
                proposedCourse = proposedCourse.Reflect(Normals[i]);
                var distance = startLocation.DistanceKilometers(proposedEndLocation);
                var result = startLocation.Offset(Geo.KilometersToRadians(distance), proposedCourse.Radians);
                return result;
            }
#if MATLAB_DEBUG_OUTPUT
            Console.WriteLine("figure;");
            Console.WriteLine("plot(Vertices(:, 1), Vertices(:, 2), 'g-*');");
            Console.WriteLine("hold on;");
            Console.WriteLine("plot(Course(:, 1), Course(:, 2), 'r-o');");
            Console.WriteLine("plot(Intersects(:, 1), Intersects(:, 2), 'bx');");
            Console.WriteLine("legend('Area Boundary', 'Course Segment under review', 'Calculated intersection points');");
#endif
            throw new GeometricException("The proposed course didn't intersect with any of the edges of the figure");
        }
        void MovementModel()
        {
            var timestepCount = (int) NemoPlatform.NemoScenario.Duration.DivideBy(NemoBase.SimulationStepTime);
            var currentTime = NemoPlatform.NemoScenario.StartTime;
            PlatformStates = new PlatformStates();
            NemoTrackdef curTrackdef = null;
            var curLocation = new Geo();
            Course course = null;
            //double curCourseDegrees = 0;
            double curSpeedMetersPerSecond = 0;
            var overlayPoints = new List<Geo>();
            OverlayShape curTrackBoundingRegion = null;
            
            CourseChangePoints = new List<CourseChangeDatum>();

            overlayPoints.Add(new Geo(NemoPlatform.Trackdefs[0].InitialLocation));
            // Put trackdefs in ascending start-time order, if they weren't already
            NemoPlatform.Trackdefs.Sort();

            for (var i = 0; i < timestepCount; i++, currentTime += NemoBase.SimulationStepTime)
            {
                // if we have a current trackdef we're processing, AND that current trackdef DOES NOT CONTAIN the current time
                // THEN we no longer have a current trackdef
                if ((curTrackdef != null) && (!curTrackdef.Contains(currentTime))) curTrackdef = null;
                // if we don't have a current trackdef
                if (curTrackdef == null)
                {
                    // look through all of our trackdefs
                    foreach (var trackdef in NemoPlatform.Trackdefs) // If we find one that contains the current time
                        if (trackdef.Contains(currentTime))
                        {
                            // make this trackdef the current one
                            curTrackdef = trackdef;
                            curLocation = curTrackdef.InitialLocation;
                            course = new Course(curTrackdef.InitialCourse);
                            curSpeedMetersPerSecond = curTrackdef.InitialSpeed * 0.514444444f;
                            CourseChangePoints.Add(new CourseChangeDatum
                            {
                                IsStart = true,
                                Location = curLocation,
                                NewCourse = course.Degrees,
                            });

                            // Conversion factor for knots to meters per second
                            if (curTrackdef.OverlayFile != null)
                            {
                                if (curTrackdef.OverlayFile.Shapes.Count() != 1) throw new PlatformMovementException(string.Format("Specified overlay file {0} is unsuitable for use as a bounding region.\nReason(s): Overlay file contains multiple shapes, therefore the bounding shape is undefined", curTrackdef.OverlayFile.FileName));
                                curTrackBoundingRegion = curTrackdef.OverlayFile.Shapes[0];
#if true
                                if (!curTrackBoundingRegion.IsUsableAsPerimeter)
                                {
                                    var reasons = new StringBuilder();
                                    if (!curTrackBoundingRegion.IsClosed) reasons.Append("Bounding region is not closed, ");
                                    if (curTrackBoundingRegion.HasCrossingSegments) reasons.Append("Bounding region is not a simple polygon (segments cross each other), ");
                                    if (reasons.Length != 0) reasons.Remove(reasons.Length - 2, 2); // Remove the trailing ", "
                                    throw new PlatformMovementException(string.Format("Specified overlay file {0} is unsuitable for use as a bounding region.\nReason(s): {1}", curTrackdef.OverlayFile.FileName, reasons));
                                }
#endif
                                if (!curTrackBoundingRegion.Contains(curLocation)) throw new PlatformMovementException(string.Format("Specified start location ({0:0.####}, {1:0.####}) is not contained within the trackdef bounding region", curLocation.Latitude, curLocation.Longitude));
                            }
                            else
                            {
                                // Else, the current trackdef's overlay file IS null, and if the type is perimeter_bounce, that's a no-no
                                if (curTrackdef.TrackType.ToLower() == "perimeter_bounce") throw new PlatformMovementException("PERIMETER_BOUNCE trackdefs require a bounding region, none was supplied.");
                            }
                            break;
                        }
                }
                // If we have a current trackdef, use it, otherwise we don't update the location or course
                if (curTrackdef != null)
                {
                    switch (curTrackdef.TrackType.ToLower())
                    {
                        default:
                        case "stationary":
                            curSpeedMetersPerSecond = 0;
                            break;
                        case "straight_line":
                            // straight line navigation code
                            curLocation = curLocation.Offset(Geo.KilometersToRadians((curSpeedMetersPerSecond * NemoBase.SimulationStepTime.TotalSeconds) / 1000), course.Radians);
                            break;
                        case "perimeter_bounce":
                            // perimeter bounce navigation code here
                            var proposedLocation = curLocation.Offset(Geo.KilometersToRadians((curSpeedMetersPerSecond * NemoBase.SimulationStepTime.TotalSeconds) / 1000), course.Radians);
                            //proposedLocation = new EarthCoordinate3D(curLocation);
                            //proposedLocation.Move(curCourseDegrees, curSpeedMetersPerSecond*NemoBase.SimulationStepTime.TotalSeconds);
                            if (curTrackBoundingRegion == null) throw new PlatformBehaviorException("Platform behavior is specified as Perimeter Bounce, but no bounding shape was specified");
                            if (curTrackBoundingRegion.Contains(proposedLocation)) curLocation = proposedLocation;
                            else
                            {
                                //curLocation.Compare(proposedLocation);
                                Course proposedCourse;
                                proposedLocation = new Geo(curTrackBoundingRegion.Reflect(curLocation, proposedLocation, out proposedCourse));
                                if (!curTrackBoundingRegion.Contains(proposedLocation))
                                {
                                    proposedLocation = new Geo(curTrackBoundingRegion.Reflect(curLocation, proposedLocation, out proposedCourse));
                                    if (!curTrackBoundingRegion.Contains(proposedLocation)) throw new PlatformMovementException("Two reflections failed to keep the platform inside the bounding region.  Please check the bounding region closely for small pockets or other irregularities");
                                }

                                var newCourse = new Course(curLocation, proposedLocation);
                                CourseChangePoints.Add(new CourseChangeDatum
                                {
                                    Location = curLocation,
                                    OldCourse = course.Degrees,
                                    NewCourse = newCourse.Degrees,
                                });

                                //curLocation.Compare(proposedLocation);
                                course = newCourse;
                                curLocation = new Geo(proposedLocation);
                                if (!curTrackBoundingRegion.Contains(curLocation)) throw new PlatformMovementException("Reflected position is outside the bounding region");
                                overlayPoints.Add(curLocation);
                            }
                            break;
                    }
                }
                else
                    // If we don't have a current trackdef, that means our speed is zero
                    curSpeedMetersPerSecond = 0;

                // Put the current location, course, speed and time into the PlatformStates list
                PlatformStates.Add(new PlatformLocation
                                   {
                                       Location = curLocation,
                                       Course = (float) course.Degrees,
                                       Speed = (float) curSpeedMetersPerSecond,
                                       //SimulationTime = currentTime
                                   });
            }
            overlayPoints.Add(new Geo(curLocation));
            CourseOverlay = new OverlayLineSegments(overlayPoints.ToArray(), Colors.Orange, 1, LineStyle.Dot);
            CourseEnd = new OverlayPoint(curLocation, Colors.Red, 2);
            CourseChangePoints.Add(new CourseChangeDatum
            {
                IsEnd = true,
                Location = curLocation,
                OldCourse = course.Degrees,
            });

            CourseStart = new OverlayPoint(NemoPlatform.Trackdefs[0].InitialLocation, Colors.Green, 2);
        }