/// <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); }
static bool IsPointInPolygon(Geo x, Geo center, IList<Geo> poly) { // do this only once and pass it in for the math. // Geo c = Limits.center(poly); // bail out if the point is more than 90 degrees off the // centroid var d = x.DistanceRadians(center); if (d >= (Math.PI / 2)) { return false; } // ray is normal to the great circle from c to x. reusing c to hold ray // info var ray = center.CrossNormalize(x); /* * side is a point on the great circle between c and x. It is used to * choose a direction. */ var side = x.CrossNormalize(ray); var isIn = false; // Why do we need to allocate new Geos? // Geo p1 = new Geo(poly[0]); // Geo p2 = new Geo(poly[0]); var p1 = new Geo(poly[0]); Geo p2; var polySize = poly.Count; for (var i = 1; i < polySize; i++) { p2 = new Geo(poly[i]); /* * p1 and p2 are on different sides of the ray, and the great acircle * between p1 and p2 is on the side that counts; */ if ((p1.Dot(ray) < 0.0) != (p2.Dot(ray) < 0.0) && new GeoSegment(p1, p2).GreatCircleIntersection(ray).Dot(side) > 0.0) { isIn = !isIn; } p1 = new Geo(p2); } // Check for unclosed polygons, if the polygon isn't closed, // do the calculation for the last point to the starting // point. if (!poly[0].Equals(p1)) { p2 = new Geo(poly[0]); if ((p1.Dot(ray) < 0.0) != (p2.Dot(ray) < 0.0) && new GeoSegment(p1, p2).GreatCircleIntersection(ray).Dot(side) > 0.0) { isIn = !isIn; } } return isIn; }