public void Test_S2LatLngRect_GetVertex() { S2LatLngRect r1 = new(new R1Interval(0, S2.M_PI_2), new S1Interval(-Math.PI, 0)); Assert.Equal(r1.Vertex(0), S2LatLng.FromRadians(0, Math.PI)); Assert.Equal(r1.Vertex(1), S2LatLng.FromRadians(0, 0)); Assert.Equal(r1.Vertex(2), S2LatLng.FromRadians(S2.M_PI_2, 0)); Assert.Equal(r1.Vertex(3), S2LatLng.FromRadians(S2.M_PI_2, Math.PI)); // Make sure that GetVertex() returns vertices in CCW order. for (int i = 0; i < 4; ++i) { double lat = S2.M_PI_4 * (i - 2); double lng = S2.M_PI_2 * (i - 2) + 0.2; S2LatLngRect r = new( new R1Interval(lat, lat + S2.M_PI_4), new S1Interval( Math.IEEERemainder(lng, S2.M_2_PI), Math.IEEERemainder(lng + S2.M_PI_2, S2.M_2_PI))); for (int k = 0; k < 4; ++k) { Assert.True(S2Pred.Sign( r.Vertex(k - 1).ToPoint(), r.Vertex(k).ToPoint(), r.Vertex(k + 1).ToPoint()) > 0); } } }
// Return the exterior angle at vertex B in the triangle ABC. The return // value is positive if ABC is counterclockwise and negative otherwise. If // you imagine a constant walking from A to B to C, this is the angle that the // constant turns at vertex B (positive = left = CCW, negative = right = CW). // This quantity is also known as the "geodesic curvature" at B. // // Ensures that TurnAngle(a,b,c) == -TurnAngle(c,b,a) for all distinct // a,b,c. The result is undefined if (a == b || b == c), but is either // -Pi or Pi if (a == c). All points should be normalized. public static double TurnAngle(S2Point a, S2Point b, S2Point c) { // We use RobustCrossProd() to get good accuracy when two points are very // close together, and Sign() to ensure that the sign is correct for // turns that are close to 180 degrees. // // Unfortunately we can't save RobustCrossProd(a, b) and pass it as the // optional 4th argument to Sign(), because Sign() requires a.CrossProd(b) // exactly (the robust version differs in magnitude). double angle = S2.RobustCrossProd(a, b).Angle(S2.RobustCrossProd(b, c)); // Don't return Sign() * angle because it is legal to have (a == c). return((S2Pred.Sign(a, b, c) > 0) ? angle : -angle); }
// A slightly more efficient version of Project() where the cross product of // the two endpoints has been precomputed. The cross product does not need to // be normalized, but should be computed using S2.RobustCrossProd() for the // most accurate results. Requires that x, a, and b have unit length. public static S2Point Project(S2Point x, S2Point a, S2Point b, S2Point a_cross_b) { System.Diagnostics.Debug.Assert(a.IsUnitLength()); System.Diagnostics.Debug.Assert(b.IsUnitLength()); System.Diagnostics.Debug.Assert(x.IsUnitLength()); // TODO(ericv): When X is nearly perpendicular to the plane containing AB, // the result is guaranteed to be close to the edge AB but may be far from // the true projected result. This could be fixed by computing the product // (A x B) x X x (A x B) using methods similar to S2::RobustCrossProd() and // S2::GetIntersection(). However note that the error tolerance would need // to be significantly larger in order for this calculation to succeed in // double precision most of the time. For example to avoid higher precision // when X is within 60 degrees of AB the minimum error would be 18 * DBL_ERR, // and to avoid higher precision when X is within 87 degrees of AB the // minimum error would be 120 * DBL_ERR. // The following is not necessary to meet accuracy guarantees but helps // to avoid unexpected results in unit tests. if (x == a || x == b) { return(x); } // Find the closest point to X along the great circle through AB. Note that // we use "n" rather than a_cross_b in the final cross product in order to // avoid the possibility of underflow. S2Point n = a_cross_b.Normalize(); S2Point p = S2.RobustCrossProd(n, x).CrossProd(n).Normalize(); // If this point is on the edge AB, then it's the closest point. S2Point pn = p.CrossProd(n); if (S2Pred.Sign(p, n, a, pn) > 0 && S2Pred.Sign(p, n, b, pn) < 0) { return(p); } // Otherwise, the closest point is either A or B. return(((x - a).Norm2() <= (x - b).Norm2()) ? a : b); }
// Like Area(), but returns a positive value for counterclockwise triangles // and a negative value otherwise. public static double SignedArea(S2Point a, S2Point b, S2Point c) { return(S2Pred.Sign(a, b, c) * Area(a, b, c)); }
// Compute the convex hull of the input geometry provided. // // If there is no geometry, this method returns an empty loop containing no // points (see S2Loop.IsEmpty). // // If the geometry spans more than half of the sphere, this method returns a // full loop containing the entire sphere (see S2Loop.IsFull). // // If the geometry contains 1 or 2 points, or a single edge, this method // returns a very small loop consisting of three vertices (which are a // superset of the input vertices). // // Note that this method does not clear the geometry; you can continue // adding to it and call this method again if desired. public S2Loop GetConvexHull() { // Test whether the bounding cap is convex. We need this to proceed with // the algorithm below in order to construct a point "origin" that is // definitely outside the convex hull. S2Cap cap = GetCapBound(); if (cap.Height() >= 1 - 10 * S2Pred.DBL_ERR) { return(S2Loop.kFull); } // This code implements Andrew's monotone chain algorithm, which is a simple // variant of the Graham scan. Rather than sorting by x-coordinate, instead // we sort the points in CCW order around an origin O such that all points // are guaranteed to be on one side of some geodesic through O. This // ensures that as we scan through the points, each new point can only // belong at the end of the chain (i.e., the chain is monotone in terms of // the angle around O from the starting point). S2Point origin = cap.Center.Ortho(); points_.Sort(new OrderedCcwAround(origin)); // Remove duplicates. We need to do this before checking whether there are // fewer than 3 points. var tmp = points_.Distinct().ToList(); points_.Clear(); points_.AddRange(tmp); // Special cases for fewer than 3 points. if (points_.Count < 3) { if (!points_.Any()) { return(S2Loop.kEmpty); } else if (points_.Count == 1) { return(GetSinglePointLoop(points_[0])); } else { return(GetSingleEdgeLoop(points_[0], points_[1])); } } // Verify that all points lie within a 180 degree span around the origin. System.Diagnostics.Debug.Assert(S2Pred.Sign(origin, points_.First(), points_.Last()) >= 0); // Generate the lower and upper halves of the convex hull. Each half // consists of the maximal subset of vertices such that the edge chain makes // only left (CCW) turns. var lower = new List <S2Point>(); var upper = new List <S2Point>(); GetMonotoneChain(lower); points_.Reverse(); GetMonotoneChain(upper); // Remove the duplicate vertices and combine the chains. System.Diagnostics.Debug.Assert(lower.First() == upper.Last()); System.Diagnostics.Debug.Assert(lower.Last() == upper.First()); lower.RemoveAt(lower.Count - 1); upper.RemoveAt(lower.Count - 1); lower.AddRange(upper); return(new S2Loop(lower)); }
private static void TestCrossing(S2Point a, S2Point b, S2Point c, S2Point d, int crossing_sign, int signed_crossing_sign) { // For degenerate edges, CrossingSign() is documented to return 0 if two // vertices from different edges are the same and -1 otherwise. The // TestCrossings() function below uses various argument permutations that // can sometimes create this case, so we fix it now if necessary. if (a == c || a == d || b == c || b == d) { crossing_sign = 0; } // As a sanity check, make sure that the expected value of // "signed_crossing_sign" is consistent with its documented properties. if (crossing_sign == 1) { Assert.Equal(signed_crossing_sign, S2Pred.Sign(a, b, c)); } else if (crossing_sign == 0 && signed_crossing_sign != 0) { Assert.Equal(signed_crossing_sign, (a == c || b == d) ? 1 : -1); } Assert.Equal(crossing_sign, S2.CrossingSign(a, b, c, d)); S2EdgeCrosser crosser = new(a, b, c); Assert.Equal(crossing_sign, crosser.CrossingSign(d)); Assert.Equal(crossing_sign, crosser.CrossingSign(c)); Assert.Equal(crossing_sign, crosser.CrossingSign(d, c)); Assert.Equal(crossing_sign, crosser.CrossingSign(c, d)); Assert.Equal(signed_crossing_sign != 0, S2.EdgeOrVertexCrossing(a, b, c, d)); crosser.RestartAt(c); Assert.Equal(signed_crossing_sign != 0, crosser.EdgeOrVertexCrossing(d)); Assert.Equal(signed_crossing_sign != 0, crosser.EdgeOrVertexCrossing(c)); Assert.Equal(signed_crossing_sign != 0, crosser.EdgeOrVertexCrossing(d, c)); Assert.Equal(signed_crossing_sign != 0, crosser.EdgeOrVertexCrossing(c, d)); crosser.RestartAt(c); Assert.Equal(signed_crossing_sign, crosser.SignedEdgeOrVertexCrossing(d)); Assert.Equal(-signed_crossing_sign, crosser.SignedEdgeOrVertexCrossing(c)); Assert.Equal(-signed_crossing_sign, crosser.SignedEdgeOrVertexCrossing(d, c)); Assert.Equal(signed_crossing_sign, crosser.SignedEdgeOrVertexCrossing(c, d)); // Check that the crosser can be re-used. crosser.Init(c, d); crosser.RestartAt(a); Assert.Equal(crossing_sign, crosser.CrossingSign(b)); Assert.Equal(crossing_sign, crosser.CrossingSign(a)); // Now try all the same tests with CopyingEdgeCrosser. S2CopyingEdgeCrosser crosser2 = new(a, b, c); Assert.Equal(crossing_sign, crosser2.CrossingSign(d)); Assert.Equal(crossing_sign, crosser2.CrossingSign(c)); Assert.Equal(crossing_sign, crosser2.CrossingSign(d, c)); Assert.Equal(crossing_sign, crosser2.CrossingSign(c, d)); crosser2.RestartAt(c); Assert.Equal(signed_crossing_sign != 0, crosser2.EdgeOrVertexCrossing(d)); Assert.Equal(signed_crossing_sign != 0, crosser2.EdgeOrVertexCrossing(c)); Assert.Equal(signed_crossing_sign != 0, crosser2.EdgeOrVertexCrossing(d, c)); Assert.Equal(signed_crossing_sign != 0, crosser2.EdgeOrVertexCrossing(c, d)); crosser2.RestartAt(c); Assert.Equal(signed_crossing_sign, crosser2.SignedEdgeOrVertexCrossing(d)); Assert.Equal(-signed_crossing_sign, crosser2.SignedEdgeOrVertexCrossing(c)); Assert.Equal(-signed_crossing_sign, crosser2.SignedEdgeOrVertexCrossing(d, c)); Assert.Equal(signed_crossing_sign, crosser2.SignedEdgeOrVertexCrossing(c, d)); // Check that the crosser can be re-used. crosser2.Init(c, d); crosser2.RestartAt(a); Assert.Equal(crossing_sign, crosser2.CrossingSign(b)); Assert.Equal(crossing_sign, crosser2.CrossingSign(a)); }