// Returns the true centroid of the loop multiplied by the area of the loop // (see s2centroids.h for details on centroids). The result is not unit // length, so you may want to normalize it. Also note that in general, the // centroid may not be contained by the loop. // // The result is scaled by the loop area for two reasons: (1) it is cheaper to // compute this way, and (2) it makes it easier to compute the centroid of // more complicated shapes (by splitting them into disjoint regions and adding // their centroids). public static S2Point GetCentroid(S2PointLoopSpan loop) { // GetSurfaceIntegral() returns either the integral of position over loop // interior, or the negative of the integral of position over the loop // exterior. But these two values are the same (!), because the integral of // position over the entire sphere is (0, 0, 0). return(GetSurfaceIntegral(loop, S2Centroid.TrueCentroid)); }
// Returns true if the loop area is at most 2*Pi. (A small amount of error is // allowed in order to ensure that loops representing an entire hemisphere are // always considered normalized.) // // Degenerate loops are handled consistently with S2Pred.Sign(), i.e., if a // loop can be expressed as the union of degenerate or nearly-degenerate // counter-clockwise triangles then this method will return true. public static bool IsNormalized(S2PointLoopSpan loop) { // We allow some error so that hemispheres are always considered normalized. // // TODO(ericv): This is no longer required by the S2Polygon implementation, // so alternatively we could create the invariant that a loop is normalized // if and only if its complement is not normalized. return(GetCurvature(loop) >= -GetCurvatureMaxError(loop)); }
// Returns an index "first" and a direction "dir" such that the vertex // sequence (first, first + dir, ..., first + (n - 1) * dir) does not change // when the loop vertex order is rotated or reversed. This allows the loop // vertices to be traversed in a canonical order. public static LoopOrder GetCanonicalLoopOrder(S2PointLoopSpan loop) { // In order to handle loops with duplicate vertices and/or degeneracies, we // return the LoopOrder that minimizes the entire corresponding vertex // *sequence*. For example, suppose that vertices are sorted // alphabetically, and consider the loop CADBAB. The canonical loop order // would be (4, 1), corresponding to the vertex sequence ABCADB. (For // comparison, loop order (4, -1) yields the sequence ABDACB.) // // If two or more loop orders yield identical minimal vertex sequences, then // it doesn't matter which one we return (since they yield the same result). // For efficiency, we divide the process into two steps. First we find the // smallest vertex, and the set of vertex indices where that vertex occurs // (noting that the loop may contain duplicate vertices). Then we consider // both possible directions starting from each such vertex index, and return // the LoopOrder corresponding to the smallest vertex sequence. int n = loop.Count; if (n == 0) { return(new LoopOrder(0, 1)); } var min_indices = new List <int> { 0 }; for (int i = 1; i < n; ++i) { if (loop[i] <= loop[min_indices[0]]) { if (loop[i] < loop[min_indices[0]]) { min_indices.Clear(); } min_indices.Add(i); } } var min_order = new LoopOrder(min_indices[0], 1); foreach (int min_index in min_indices) { var order1 = new LoopOrder(min_index, 1); var order2 = new LoopOrder(min_index + n, -1); if (IsOrderLess(order1, min_order, loop)) { min_order = order1; } if (IsOrderLess(order2, min_order, loop)) { min_order = order2; } } return(min_order); }
// Returns the area of the loop interior, i.e. the region on the left side of // the loop. The result is between 0 and 4*Pi steradians. The implementation // ensures that nearly-degenerate clockwise loops have areas close to zero, // while nearly-degenerate counter-clockwise loops have areas close to 4*Pi. public static double GetArea(S2PointLoopSpan loop) { var area = GetSignedArea(loop); System.Diagnostics.Debug.Assert(Math.Abs(area) <= S2.M_2_PI); if (area < 0.0) { area += S2.M_4_PI; } return(area); }
// Returns the geodesic curvature of the loop, defined as the sum of the turn // angles at each vertex (see S2.TurnAngle). The result is positive if the // loop is counter-clockwise, negative if the loop is clockwise, and zero if // the loop is a great circle. The geodesic curvature is equal to 2*Pi minus // the area of the loop. // // The following cases are handled specially: // // - Degenerate loops (consisting of an isolated vertex or composed entirely // of sibling edge pairs) have a curvature of 2*Pi exactly. // // - The full loop (containing all points, and represented as a loop with no // vertices) has a curvature of -2*Pi exactly. // // - All other loops have a non-zero curvature in the range (-2*Pi, 2*Pi). // For any such loop, reversing the order of the vertices is guaranteed to // negate the curvature. This property can be used to define a unique // normalized orientation for every loop. public static double GetCurvature(S2PointLoopSpan loop) { // By convention, a loop with no vertices contains all points on the sphere. if (!loop.Any()) { return(-S2.M_2_PI); } // Remove any degeneracies from the loop. loop = PruneDegeneracies(loop); // If the entire loop was degenerate, it's turning angle is defined as 2*Pi. if (!loop.Any()) { return(S2.M_2_PI); } // To ensure that we get the same result when the vertex order is rotated, // and that the result is negated when the vertex order is reversed, we need // to add up the individual turn angles in a consistent order. (In general, // adding up a set of numbers in a different order can change the sum due to // rounding errors.) // // Furthermore, if we just accumulate an ordinary sum then the worst-case // error is quadratic in the number of vertices. (This can happen with // spiral shapes, where the partial sum of the turning angles can be linear // in the number of vertices.) To avoid this we use the Kahan summation // algorithm (http://en.wikipedia.org/wiki/Kahan_summation_algorithm). LoopOrder order = GetCanonicalLoopOrder(loop); int i = order.first, dir = order.dir, n = loop.Count; var sum = S2.TurnAngle( loop.GetRemIndex(i + n - dir), loop.GetRemIndex(i), loop.GetRemIndex(i + dir)); double compensation = 0; // Kahan summation algorithm while (--n > 0) { i += dir; var angle = S2.TurnAngle( loop.GetRemIndex(i - dir), loop.GetRemIndex(i), loop.GetRemIndex(i + dir)); double old_sum = sum; angle += compensation; sum += angle; compensation = (old_sum - sum) + angle; } const double kMaxCurvature = S2.M_2_PI - 4 * S2.DoubleEpsilon; sum += compensation; return(Math.Max(-kMaxCurvature, Math.Min(kMaxCurvature, dir * sum))); }
private static void TestAreaConsistentWithCurvature(S2PointLoopSpan loop) { // Check that the area computed using GetArea() is consistent with the loop // curvature. According to the Gauss-Bonnet theorem, the area of the loop // equals 2*Pi minus its curvature. double area = S2.GetArea(loop); double gauss_area = S2.M_2_PI - S2.GetCurvature(loop); // The error bound below is sufficient for current tests but not guaranteed. _ = loop.ToDebugString(); Assert.True(Math.Abs(area - gauss_area) <= 1e-14); }
private static void ExpectSameOrder(S2PointLoopSpan loop1, S2.LoopOrder order1, S2PointLoopSpan loop2, S2.LoopOrder order2) { Assert.Equal(loop1.Count, loop2.Count); int i1 = order1.first, i2 = order2.first; int dir1 = order1.dir, dir2 = order2.dir; for (int n = loop1.Count; --n >= 0;) { Assert.Equal(loop1[i1], loop2[i2]); i1 += dir1; i2 += dir2; } }
// Check that the curvature is *identical* when the vertex order is // rotated, and that the sign is inverted when the vertices are reversed. private static void CheckCurvatureInvariants(S2PointLoopSpan loop_in) { S2.LoopOrder order_in = S2.GetCanonicalLoopOrder(loop_in); var loop = loop_in.ToList(); double expected = S2.GetCurvature(loop); for (int i = 0; i < loop.Count; ++i) { loop.Reverse(); Assert.Equal((expected == S2.M_2_PI) ? expected : -expected, S2.GetCurvature(loop)); ExpectSameOrder(loop_in, order_in, loop, S2.GetCanonicalLoopOrder(loop)); loop.Reverse(); loop.RotateInPlace(1); Assert.Equal(expected, S2.GetCurvature(loop)); ExpectSameOrder(loop_in, order_in, loop, S2.GetCanonicalLoopOrder(loop)); } }
public S2LoopMeasuresTests() { // The full loop is represented as a loop with no vertices. full_ = new(); // A degenerate loop in the shape of a "V". v_loop_ = ParsePointsOrDie("5:1, 0:2, 5:3, 0:2"); // The northern hemisphere, defined using two pairs of antipodal points. north_hemi_ = ParsePointsOrDie("0:-180, 0:-90, 0:0, 0:90"); // The northern hemisphere, defined using three points 120 degrees apart. north_hemi3_ = ParsePointsOrDie("0:-180, 0:-60, 0:60"); // The western hemisphere, defined using two pairs of antipodal points. west_hemi_ = ParsePointsOrDie("0:-180, -90:0, 0:0, 90:0"); // The eastern hemisphere, defined using two pairs of antipodal points. east_hemi_ = ParsePointsOrDie("90:0, 0:0, -90:0, 0:-180"); // A spiral stripe that slightly over-wraps the equator. candy_cane_ = ParsePointsOrDie( "-20:150, -20:-70, 0:70, 10:-150, 10:70, -10:-70"); // A completely degenerate triangle along the equator that Sign() // considers to be CCW. line_triangle_ = ParsePointsOrDie("0:1, 0:2, 0:3"); // A nearly-degenerate CCW chevron near the equator with very long sides // (about 80 degrees). Its area is less than 1e-640, which is too small // to represent in double precision. skinny_chevron_ = ParsePointsOrDie("0:0, -1e-320:80, 0:1e-320, 1e-320:80"); // A loop where the same vertex appears three times. three_leaf_clover_ = ParsePointsOrDie( "0:0, -3:3, 3:3, 0:0, 3:0, 3:-3, 0:0, -3:-3, -3:0"); // A loop with groups of 3 or more vertices in a straight line. tessellated_loop_ = ParsePointsOrDie( "10:34, 5:34, 0:34, -10:34, -10:36, -5:36, 0:36, 10:36"); }
// Returns the maximum error in GetCurvature() for the given loop. This value // is also an upper bound on the error in GetArea(), GetSignedArea(), and // GetApproxArea(). public static double GetCurvatureMaxError(S2PointLoopSpan loop) { // The maximum error can be bounded as follows: // 3.00 * S2Constants.DoubleEpsilon for RobustCrossProd(b, a) // 3.00 * S2Constants.DoubleEpsilon for RobustCrossProd(c, b) // 3.25 * S2Constants.DoubleEpsilon for Angle() // 2.00 * S2Constants.DoubleEpsilon for each addition in the Kahan summation // ------------------- // 11.25 * S2Constants.DoubleEpsilon // // TODO(b/203697029): This error estimate is approximate. There are two // issues: (1) SignedArea needs some improvements to ensure that its error is // actually never higher than GirardArea, and (2) although the number of // triangles in the sum is typically N-2, in theory it could be as high as // 2*N for pathological inputs. But in other respects this error bound is // very conservative since it assumes that the maximum error is achieved on // every triangle. const double kMaxErrorPerVertex = 11.25 * S2.DoubleEpsilon; return(kMaxErrorPerVertex * loop.Count); }
public static string ToDebugString(this S2PointLoopSpan points) => AppendVertices(points.ToArray(), points.Count);
// Returns either the positive area of the region on the left side of the // loop, or the negative area of the region on the right side of the loop, // whichever is smaller in magnitude. The result is between -2*Pi and 2*Pi // steradians. This method is used to accurately compute the area of polygons // consisting of multiple loops. // // The following cases are handled specially: // // - Counter-clockwise loops are guaranteed to have positive area, and // clockwise loops are guaranteed to have negative area. // // - Degenerate loops (consisting of an isolated vertex or composed entirely // of sibling edge pairs) have an area of exactly zero. // // - The full loop (containing all points, and represented as a loop with no // vertices) has a negative area with the minimum possible magnitude. // (This is the "signed equivalent" of having an area of 4*Pi.) public static double GetSignedArea(S2PointLoopSpan loop) { // It is surprisingly difficult to compute the area of a loop robustly. The // main issues are (1) whether degenerate loops are considered to be CCW or // not (i.e., whether their area is close to 0 or 4*Pi), and (2) computing // the areas of small loops with good relative accuracy. // // With respect to degeneracies, we would like GetArea() to be consistent // with S2Loop.Contains(S2Point) in that loops that contain many points // should have large areas, and loops that contain few points should have // small areas. For example, if a degenerate triangle is considered CCW // according to S2Pred.Sign(), then it will contain very few points and // its area should be approximately zero. On the other hand if it is // considered clockwise, then it will contain virtually all points and so // its area should be approximately 4*Pi. // // More precisely, let U be the set of S2Points for which S2.IsUnitLength() // is true, let P(U) be the projection of those points onto the mathematical // unit sphere, and let V(P(U)) be the Voronoi diagram of the projected // points. Then for every loop x, we would like GetArea() to approximately // equal the sum of the areas of the Voronoi regions of the points p for // which x.Contains(p) is true. // // The second issue is that we want to compute the area of small loops // accurately. This requires having good relative precision rather than // good absolute precision. For example, if the area of a loop is 1e-12 and // the error is 1e-15, then the area only has 3 digits of accuracy. (For // reference, 1e-12 is about 40 square meters on the surface of the earth.) // We would like to have good relative accuracy even for small loops. // // To achieve these goals, we combine two different methods of computing the // area. This first method is based on the Gauss-Bonnet theorem, which says // that the area enclosed by the loop equals 2*Pi minus the total geodesic // curvature of the loop (i.e., the sum of the "turning angles" at all the // loop vertices). The big advantage of this method is that as long as we // use S2Pred.Sign() to compute the turning angle at each vertex, then // degeneracies are always handled correctly. In other words, if a // degenerate loop is CCW according to the symbolic perturbations used by // S2Pred.Sign(), then its turning angle will be approximately 2*Pi. // // The disadvantage of the Gauss-Bonnet method is that its absolute error is // about 2e-15 times the number of vertices (see GetCurvatureMaxError). // So, it cannot compute the area of small loops accurately. // // The second method is based on splitting the loop into triangles and // summing the area of each triangle. To avoid the difficulty and expense // of decomposing the loop into a union of non-overlapping triangles, // instead we compute a signed sum over triangles that may overlap (see the // comments for S2Loop.GetSurfaceIntegral). The advantage of this method // is that the area of each triangle can be computed with much better // relative accuracy (using l'Huilier's theorem). The disadvantage is that // the result is a signed area: CCW loops may yield a small positive value, // while CW loops may yield a small negative value (which is converted to a // positive area by adding 4*Pi). This means that small errors in computing // the signed area may translate into a very large error in the result (if // the sign of the sum is incorrect). // // So, our strategy is to combine these two methods as follows. First we // compute the area using the "signed sum over triangles" approach (since it // is generally more accurate). We also estimate the maximum error in this // result. If the signed area is too close to zero (i.e., zero is within // the error bounds), then we double-check the sign of the result using the // Gauss-Bonnet method. If the two methods disagree, we return the smallest // possible positive or negative area based on the result of GetCurvature(). // Otherwise we return the area that we computed originally. // The signed area should be between approximately -4*Pi and 4*Pi. // Normalize it to be in the range [-2*Pi, 2*Pi]. var area = GetSurfaceIntegral(loop, S2.SignedArea); double max_error = GetCurvatureMaxError(loop); // Normalize the area to be in the range (-2*Pi, 2*Pi]. Effectively this // means that hemispheres are always interpreted as having positive area. area = Math.IEEERemainder(area, S2.M_4_PI); if (area == S2.M_N2_PI) { area = S2.M_2_PI; } // If the area is a small negative or positive number, verify that the sign // of the result is consistent with the loop orientation. if (Math.Abs(area) <= max_error) { double curvature = GetCurvature(loop); // Zero-area loops should have a curvature of approximately +/- 2*Pi. System.Diagnostics.Debug.Assert(!(area == 0 && curvature == 0)); if (curvature == S2.M_2_PI) { return(0.0); // Degenerate } if (area <= 0 && curvature > 0) { return(S2.DoubleMinNorm); } // Full loops are handled by the case below. if (area >= 0 && curvature < 0) { return(-S2.DoubleMinNorm); } } return(area); }
// Like GetArea(), except that this method is faster and has more error. The // result is between 0 and 4*Pi steradians. The maximum error is 2.22e-15 // steradians per loop vertex, which works out to about 0.09 square meters per // vertex on the Earth's surface. For example, a loop with 100 vertices has a // maximum error of about 9 square meters. (The actual error is typically // much smaller than this.) The error bound can be computed using // GetCurvatureMaxError(), which returns the maximum error in steradians. public static double GetApproxArea(S2PointLoopSpan loop) { return(S2.M_2_PI - GetCurvature(loop)); }
private static IEnumerable <T> GetSurfaceIntegral <T>(S2PointLoopSpan loop, Func <S2Point, S2Point, S2Point, T> f_tri) { // We sum "f_tri" over a collection T of oriented triangles, possibly // overlapping. Let the sign of a triangle be +1 if it is CCW and -1 // otherwise, and let the sign of a point "x" be the sum of the signs of the // triangles containing "x". Then the collection of triangles T is chosen // such that every point in the loop interior has the same sign x, and every // point in the loop exterior has the same sign (x - 1). Furthermore almost // always it is true that x == 0 or x == 1, meaning that either // // (1) Each point in the loop interior has sign +1, and sign 0 otherwise; or // (2) Each point in the loop exterior has sign -1, and sign 0 otherwise. // // The triangles basically consist of a "fan" from vertex 0 to every loop // edge that does not include vertex 0. However, what makes this a bit // tricky is that spherical edges become numerically unstable as their // length approaches 180 degrees. Of course there is not much we can do if // the loop itself contains such edges, but we would like to make sure that // all the triangle edges under our control (i.e., the non-loop edges) are // stable. For example, consider a loop around the equator consisting of // four equally spaced points. This is a well-defined loop, but we cannot // just split it into two triangles by connecting vertex 0 to vertex 2. // // We handle this type of situation by moving the origin of the triangle fan // whenever we are about to create an unstable edge. We choose a new // location for the origin such that all relevant edges are stable. We also // create extra triangles with the appropriate orientation so that the sum // of the triangle signs is still correct at every point. // The maximum length of an edge for it to be considered numerically stable. // The exact value is fairly arbitrary since it depends on the stability of // the "f_tri" function. The value below is quite conservative but could be // reduced further if desired. const double kMaxLength = Math.PI - 1e-5; if (loop.Count < 3) { yield break; } S2Point origin = loop[0]; for (int i = 1; i + 1 < loop.Count; ++i) { // Let V_i be loop[i], let O be the current origin, and let length(A, B) // be the length of edge (A, B). At the start of each loop iteration, the // "leading edge" of the triangle fan is (O, V_i), and we want to extend // the triangle fan so that the leading edge is (O, V_i+1). // // Invariants: // 1. length(O, V_i) < kMaxLength for all (i > 1). // 2. Either O == V_0, or O is approximately perpendicular to V_0. // 3. "sum" is the oriented integral of f over the area defined by // (O, V_0, V_1, ..., V_i). System.Diagnostics.Debug.Assert(i == 1 || origin.Angle(loop[i]) < kMaxLength); System.Diagnostics.Debug.Assert(origin == loop[0] || Math.Abs(origin.DotProd(loop[0])) < S2.DoubleError); if (loop[i + 1].Angle(origin) > kMaxLength) { // We are about to create an unstable edge, so choose a new origin O' // for the triangle fan. S2Point old_origin = origin; if (origin == loop[0]) { // The following point O' is well-separated from V_i and V_0 (and // therefore V_i+1 as well). Moving the origin transforms the leading // edge of the triangle fan into a two-edge chain (V_0, O', V_i). origin = S2.RobustCrossProd(loop[0], loop[i]).Normalize(); } else if (loop[i].Angle(loop[0]) < kMaxLength) { // All edges of the triangle (O, V_0, V_i) are stable, so we can // revert to using V_0 as the origin. This changes the leading edge // chain (V_0, O, V_i) back into a single edge (V_0, V_i). origin = loop[0]; } else { // (O, V_i+1) and (V_0, V_i) are antipodal pairs, and O and V_0 are // perpendicular. Therefore V_0.CrossProd(O) is approximately // perpendicular to all of {O, V_0, V_i, V_i+1}, and we can choose // this point O' as the new origin. // // NOTE(ericv): The following line is the reason why in rare cases the // triangle sum can have a sign other than -1, 0, or 1. To fix this // we would need to choose either "-origin" or "origin" below // depending on whether the signed area of the triangles chosen so far // is positive or negative respectively. This is easy in the case of // GetSignedArea() but would be extra work for GetCentroid(). In any // case this does not cause any problems in practice. origin = loop[0].CrossProd(old_origin); // The following two triangles transform the leading edge chain from // (V_0, O, V_i) to (V_0, O', V_i+1). // // First we advance the edge (V_0, O) to (V_0, O'). yield return(f_tri(loop[0], old_origin, origin)); } // Advance the edge (O, V_i) to (O', V_i). yield return(f_tri(old_origin, loop[i], origin)); } // Advance the edge (O, V_i) to (O, V_i+1). yield return(f_tri(origin, loop[i], loop[i + 1])); } // If the origin is not V_0, we need to sum one more triangle. if (origin != loop[0]) { // Advance the edge (O, V_n-1) to (O, V_0). yield return(f_tri(origin, loop[^ 1], loop[0]));
public static double GetSurfaceIntegral(S2PointLoopSpan loop, Func <S2Point, S2Point, S2Point, double> f_tri) { return(GetSurfaceIntegral <double>(loop, f_tri).Aggregate(0.0, (a, b) => a + b)); }
// Returns the oriented surface integral of some quantity f(x) over the loop // interior, given a function f_tri(A,B,C) that returns the corresponding // integral over the spherical triangle ABC. Here "oriented surface integral" // means: // // (1) f_tri(A,B,C) should return the integral of f if ABC is counterclockwise // and the integral of -f if ABC is clockwise. // // (2) The result is the integral of f over the loop interior plus or minus // some multiple of the integral of f over the entire sphere. // // Note that there are at least two common situations where property (2) above // is not a limitation: // // - When the integral of f over the entire sphere is zero. For example this // is true when computing centroids. // // - When f is non-negative and the integral over the entire sphere is a // constant known in advance. In this case the correct result can be // obtained by using std::remainder appropriately. // // REQUIRES: The default constructor for T must initialize the value to zero. // (This is true for built-in types such as "double".) public static S2Point GetSurfaceIntegral(S2PointLoopSpan loop, Func <S2Point, S2Point, S2Point, S2Point> f_tri) { return(GetSurfaceIntegral <S2Point>(loop, f_tri).Aggregate(S2Point.Empty, (a, b) => a + b)); }