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); } } }
public void Test_S1ChordAngle_GetS2PointConstructorMaxError() { // Check that the error bound returned by GetS2PointConstructorMaxError() is // large enough. for (var iter = 0; iter < 100000; ++iter) { S2Testing.Random.Reset(iter); // Easier to reproduce a specific case. var x = S2Testing.RandomPoint(); var y = S2Testing.RandomPoint(); if (S2Testing.Random.OneIn(10)) { // Occasionally test a point pair that is nearly identical or antipodal. var r = S1Angle.FromRadians(S2.DoubleError * S2Testing.Random.RandDouble()); y = S2.GetPointOnLine(x, y, r); if (S2Testing.Random.OneIn(2)) { y = -y; } } S1ChordAngle dist = new(x, y); var error = dist.GetS2PointConstructorMaxError(); var er1 = S2Pred.CompareDistance(x, y, dist.PlusError(error)); if (er1 > 0) { } Assert.True(er1 <= 0); var er2 = S2Pred.CompareDistance(x, y, dist.PlusError(-error)); if (er2 < 0) { } Assert.True(er2 >= 0); } }
// Given a point X and an edge AB, check that the distance from X to AB is // "distance_radians" and the closest point on AB is "expected_closest". private static void CheckDistance(S2Point x, S2Point a, S2Point b, double distance_radians, S2Point expected_closest) { x = x.Normalize(); a = a.Normalize(); b = b.Normalize(); expected_closest = expected_closest.Normalize(); Assert2.Near(distance_radians, S2.GetDistance(x, a, b).Radians, S2.DoubleError); S2Point closest = S2.Project(x, a, b); Assert.True(S2Pred.CompareEdgeDistance( closest, a, b, new S1ChordAngle(S2.kProjectPerpendicularErrorS1Angle)) < 0); // If X is perpendicular to AB then there is nothing further we can expect. if (distance_radians != S2.M_PI_2) { if (expected_closest == new S2Point()) { // This special value says that the result should be A or B. Assert.True(closest == a || closest == b); } else { Assert.True(S2.ApproxEquals(closest, expected_closest)); } } S1ChordAngle min_distance = S1ChordAngle.Zero; Assert.False(S2.UpdateMinDistance(x, a, b, ref min_distance)); min_distance = S1ChordAngle.Infinity; Assert.True(S2.UpdateMinDistance(x, a, b, ref min_distance)); Assert2.Near(distance_radians, min_distance.ToAngle().Radians, S2.DoubleError); }
public void Test_S2ClosestEdgeQuery_TrueDistanceLessThanS1ChordAngleDistance() { // Tests that IsConservativeDistanceLessOrEqual returns points where the // true distance is slightly less than the one computed by S1ChordAngle. // // The points below had the worst error from among 100,000 random pairs. S2Point p0 = new(0.78516762584829192, -0.50200400690845970, -0.36263449417782678); S2Point p1 = new(0.78563011732429433, -0.50187655940493503, -0.36180828883938054); // The S1ChordAngle distance is ~4 ulps greater than the true distance. Distance dist1 = new(p0, p1); var limit = dist1.Predecessor().Predecessor().Predecessor().Predecessor(); Assert.True(S2Pred.CompareDistance(p0, p1, limit) < 0); // Verify that IsConservativeDistanceLessOrEqual() still returns "p1". S2Point[] index_points = new[] { p0 }; MutableS2ShapeIndex index = new(); index.Add(new S2PointVectorShape(index_points)); S2ClosestEdgeQuery query = new(index); S2ClosestEdgeQuery.PointTarget target1 = new(p1); Assert.False(query.IsDistanceLess(target1, limit)); Assert.False(query.IsDistanceLessOrEqual(target1, limit)); Assert.True(query.IsConservativeDistanceLessOrEqual(target1, limit)); }
public void Test_S2ClosestEdgeQuery_IsConservativeDistanceLessOrEqual() { // Test int num_tested = 0; int num_conservative_needed = 0; for (int iter = 0; iter < 1000; ++iter) { S2Testing.Random.Reset(iter + 1); // Easier to reproduce a specific case. S2Point x = S2Testing.RandomPoint(); S2Point dir = S2Testing.RandomPoint(); S1Angle r = S1Angle.FromRadians(Math.PI * Math.Pow(1e-30, S2Testing.Random.RandDouble())); S2Point y = S2.InterpolateAtDistance(r, x, dir); Distance limit = new(r); if (S2Pred.CompareDistance(x, y, limit) <= 0) { MutableS2ShapeIndex index = new(); index.Add(new S2PointVectorShape(new S2Point[] { x })); S2ClosestEdgeQuery query = new(index); S2ClosestEdgeQuery.PointTarget target = new(y); Assert.True(query.IsConservativeDistanceLessOrEqual(target, limit)); ++num_tested; if (!query.IsDistanceLess(target, limit)) { ++num_conservative_needed; } } } // Verify that in most test cases, the distance between the target points // was close to the desired value. Also verify that at least in some test // cases, the conservative distance test was actually necessary. Assert.True(num_tested >= 300); Assert.True(num_tested <= 700); Assert.True(num_conservative_needed >= 25); }
/// <summary> /// Returns true if wedge A contains wedge B. Equivalent to but faster than /// GetWedgeRelation() == WEDGE_PROPERLY_CONTAINS || WEDGE_EQUALS. /// REQUIRES: A and B are non-empty. /// </summary> public static bool WedgeContains(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { // For A to contain B (where each loop interior is defined to be its left // side), the CCW edge order around ab1 must be a2 b2 b0 a0. We split // this test into two parts that test three vertices each. return( S2Pred.OrderedCCW(a2, b2, b0, ab1) && S2Pred.OrderedCCW(b0, a0, a2, ab1)); }
/// <summary> /// Returns true if wedge A intersects wedge B. Equivalent to but faster /// than GetWedgeRelation() != WEDGE_IS_DISJOINT. /// REQUIRES: A and B are non-empty. /// </summary> /// <returns></returns> public static bool WedgeIntersects(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { // For A not to intersect B (where each loop interior is defined to be // its left side), the CCW edge order around ab1 must be a0 b2 b0 a2. // Note that it's important to write these conditions as negatives // (!OrderedCCW(a,b,c,o) rather than Ordered(c,b,a,o)) to get correct // results when two vertices are the same. return(!( S2Pred.OrderedCCW(a0, b2, b0, ab1) && S2Pred.OrderedCCW(b0, a2, a0, ab1))); }
// 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); }
public void Test_S2_ProjectError() { for (int iter = 0; iter < 1000; ++iter) { S2Testing.Random.Reset(iter + 1); // Easier to reproduce a specific case. S2Point a = ChoosePoint(); S2Point b = ChoosePoint(); S2Point n = S2.RobustCrossProd(a, b).Normalize(); S2Point x = S2Testing.SamplePoint(new S2Cap(n, S1Angle.FromRadians(1e-15))); S2Point p = S2.Project(x, a, b); Assert.True(S2Pred.CompareEdgeDistance( p, a, b, new S1ChordAngle(S2.kProjectPerpendicularErrorS1Angle)) < 0); } }
/// <summary> /// Returns the relation from wedge A to B. /// REQUIRES: A and B are non-empty. /// </summary> public static WedgeRelation GetWedgeRelation(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { // There are 6 possible edge orderings at a shared vertex (all // of these orderings are circular, i.e. abcd == bcda): // // (1) a2 b2 b0 a0: A contains B // (2) a2 a0 b0 b2: B contains A // (3) a2 a0 b2 b0: A and B are disjoint // (4) a2 b0 a0 b2: A and B intersect in one wedge // (5) a2 b2 a0 b0: A and B intersect in one wedge // (6) a2 b0 b2 a0: A and B intersect in two wedges // // We do not distinguish between 4, 5, and 6. // We pay extra attention when some of the edges overlap. When edges // overlap, several of these orderings can be satisfied, and we take // the most specific. if (a0 == b0 && a2 == b2) { return(WedgeRelation.WEDGE_EQUALS); } if (S2Pred.OrderedCCW(a0, a2, b2, ab1)) { // The cases with this vertex ordering are 1, 5, and 6, // although case 2 is also possible if a2 == b2. if (S2Pred.OrderedCCW(b2, b0, a0, ab1)) { return(WedgeRelation.WEDGE_PROPERLY_CONTAINS); } // We are in case 5 or 6, or case 2 if a2 == b2. return((a2 == b2) ? WedgeRelation.WEDGE_IS_PROPERLY_CONTAINED : WedgeRelation.WEDGE_PROPERLY_OVERLAPS); } // We are in case 2, 3, or 4. if (S2Pred.OrderedCCW(a0, b0, b2, ab1)) { return(WedgeRelation.WEDGE_IS_PROPERLY_CONTAINED); } return(S2Pred.OrderedCCW(a0, b0, a2, ab1) ? WedgeRelation.WEDGE_IS_DISJOINT : WedgeRelation.WEDGE_PROPERLY_OVERLAPS); }
// Given a point P, return the minimum level at which an edge of some S2Cell // parent of P is nearly collinear with S2.Origin. This is the minimum // level for which Sign() may need to resort to expensive calculations in // order to determine which side of an edge the origin lies on. private static int GetMinExpensiveLevel(S2Point p) { S2CellId id = new(p); for (int level = 0; level <= S2.kMaxCellLevel; ++level) { S2Cell cell = new(id.Parent(level)); for (int k = 0; k < 4; ++k) { S2Point a = cell.Vertex(k); S2Point b = cell.Vertex(k + 1); if (S2Pred.TriageSign(a, b, S2.Origin, a.CrossProd(b)) == 0) { return(level); } } } return(S2.kMaxCellLevel + 1); }
// 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); }
public void Test_S2_GetUpdateMinInteriorDistanceMaxError() { // Check that the error bound returned by // GetUpdateMinInteriorDistanceMaxError() is large enough. for (int iter = 0; iter < 10000; ++iter) { S2Point a0 = S2Testing.RandomPoint(); var lenRadians = Math.PI * Math.Pow(1e-20, S2Testing.Random.RandDouble()); S1Angle len = S1Angle.FromRadians(lenRadians); if (S2Testing.Random.OneIn(4)) { len = S1Angle.FromRadians(S2.M_PI) - len; } S2Point a1 = S2.GetPointOnLine(a0, S2Testing.RandomPoint(), len); // TODO(ericv): The error bound holds for antipodal points, but the S2 // predicates used to test the error do not support antipodal points yet. if (a1 == -a0) { continue; } S2Point n = S2.RobustCrossProd(a0, a1).Normalize(); double f = Math.Pow(1e-20, S2Testing.Random.RandDouble()); S2Point a = ((1 - f) * a0 + f * a1).Normalize(); var rRadians = S2.M_PI_2 * Math.Pow(1e-20, S2Testing.Random.RandDouble()); S1Angle r = S1Angle.FromRadians(rRadians); if (S2Testing.Random.OneIn(2)) { r = S1Angle.FromRadians(S2.M_PI_2) - r; } S2Point x = S2.GetPointOnLine(a, n, r); S1ChordAngle min_dist = S1ChordAngle.Infinity; if (!S2.UpdateMinInteriorDistance(x, a0, a1, ref min_dist)) { --iter; continue; } double error = S2.GetUpdateMinDistanceMaxError(min_dist); Assert.True(S2Pred.CompareEdgeDistance(x, a0, a1, min_dist.PlusError(error)) <= 0); Assert.True(S2Pred.CompareEdgeDistance(x, a0, a1, min_dist.PlusError(-error)) >= 0); } }
// 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)); }