///////////////////////////////////////////////////////////////////////////// /////////////// (point along edge) functions /////////////// // Given a point X and an edge AB, returns the distance ratio AX / (AX + BX). // If X happens to be on the line segment AB, this is the fraction "t" such // that X == Interpolate(A, B, t). Requires that A and B are distinct. public static double GetDistanceFraction(S2Point x, S2Point a, S2Point b) { System.Diagnostics.Debug.Assert(a != b); var da = x.Angle(a); var db = x.Angle(b); return(da / (da + db)); }
private static void GatherStats(S2Cell cell) { var s = level_stats[cell.Level]; double exact_area = cell.ExactArea(); double approx_area = cell.ApproxArea(); double min_edge = 100, max_edge = 0, avg_edge = 0; double min_diag = 100, max_diag = 0; double min_width = 100, max_width = 0; double min_angle_span = 100, max_angle_span = 0; for (int i = 0; i < 4; ++i) { double edge = cell.VertexRaw(i).Angle(cell.VertexRaw(i + 1)); min_edge = Math.Min(edge, min_edge); max_edge = Math.Max(edge, max_edge); avg_edge += 0.25 * edge; S2Point mid = cell.VertexRaw(i) + cell.VertexRaw(i + 1); double width = S2.M_PI_2 - mid.Angle(cell.EdgeRaw(i + 2)); min_width = Math.Min(width, min_width); max_width = Math.Max(width, max_width); if (i < 2) { double diag = cell.VertexRaw(i).Angle(cell.VertexRaw(i + 2)); min_diag = Math.Min(diag, min_diag); max_diag = Math.Max(diag, max_diag); double angle_span = cell.EdgeRaw(i).Angle(-cell.EdgeRaw(i + 2)); min_angle_span = Math.Min(angle_span, min_angle_span); max_angle_span = Math.Max(angle_span, max_angle_span); } } s.Count += 1; s.Min_area = Math.Min(exact_area, s.Min_area); s.Max_area = Math.Max(exact_area, s.Max_area); s.Avg_area += exact_area; s.Min_width = Math.Min(min_width, s.Min_width); s.Max_width = Math.Max(max_width, s.Max_width); s.Avg_width += 0.5 * (min_width + max_width); s.Min_edge = Math.Min(min_edge, s.Min_edge); s.Max_edge = Math.Max(max_edge, s.Max_edge); s.Avg_edge += avg_edge; s.Max_edge_aspect = Math.Max(max_edge / min_edge, s.Max_edge_aspect); s.Min_diag = Math.Min(min_diag, s.Min_diag); s.Max_diag = Math.Max(max_diag, s.Max_diag); s.Avg_diag += 0.5 * (min_diag + max_diag); s.Max_diag_aspect = Math.Max(max_diag / min_diag, s.Max_diag_aspect); s.Min_angle_span = Math.Min(min_angle_span, s.Min_angle_span); s.Max_angle_span = Math.Max(max_angle_span, s.Max_angle_span); s.Avg_angle_span += 0.5 * (min_angle_span + max_angle_span); double approx_ratio = approx_area / exact_area; s.Min_approx_ratio = Math.Min(approx_ratio, s.Min_approx_ratio); s.Max_approx_ratio = Math.Max(approx_ratio, s.Max_approx_ratio); }
public void Test_S2CellId_Coverage() { // Make sure that random points on the sphere can be represented to the // expected level of accuracy, which in the worst case is Math.Sqrt(2/3) times // the maximum arc length between the points on the sphere associated with // adjacent values of "i" or "j". (It is Math.Sqrt(2/3) rather than 1/2 because // the cells at the corners of each face are stretched -- they have 60 and // 120 degree angles.) double max_dist = 0.5 * S2.kMaxDiag.GetValue(S2.kMaxCellLevel); for (int i = 0; i < 1000000; ++i) { S2Point p = S2Testing.RandomPoint(); S2Point q = new S2CellId(p).ToPointRaw(); Assert.True(p.Angle(q) <= max_dist); } }
// Returns the true centroid of the spherical triangle ABC multiplied by the // signed area of spherical triangle ABC. The reasons for multiplying by the // signed area are (1) this is the quantity that needs to be summed to compute // the centroid of a union or difference of triangles, and (2) it's actually // easier to calculate this way. All points must have unit length. // // Note that the result of this function is defined to be S2Point.Empty if // the triangle is degenerate (and that this is intended behavior). public static S2Point TrueCentroid(S2Point a, S2Point b, S2Point c) { System.Diagnostics.Debug.Assert(a.IsUnitLength()); System.Diagnostics.Debug.Assert(b.IsUnitLength()); System.Diagnostics.Debug.Assert(c.IsUnitLength()); // I couldn't find any references for computing the true centroid of a // spherical triangle... I have a truly marvellous demonstration of this // formula which this margin is too narrow to contain :) // Use Angle() in order to get accurate results for small triangles. var angle_a = b.Angle(c); var angle_b = c.Angle(a); var angle_c = a.Angle(b); var ra = (angle_a == 0) ? 1 : (angle_a / Math.Sin(angle_a)); var rb = (angle_b == 0) ? 1 : (angle_b / Math.Sin(angle_b)); var rc = (angle_c == 0) ? 1 : (angle_c / Math.Sin(angle_c)); // Now compute a point M such that: // // [Ax Ay Az] [Mx] [ra] // [Bx By Bz] [My] = 0.5 * det(A,B,C) * [rb] // [Cx Cy Cz] [Mz] [rc] // // To improve the numerical stability we subtract the first row (A) from the // other two rows; this reduces the cancellation error when A, B, and C are // very close together. Then we solve it using Cramer's rule. // // The result is the true centroid of the triangle multiplied by the // triangle's area. // // TODO(b/205027737): This code still isn't as numerically stable as it could // be. The biggest potential improvement is to compute B-A and C-A more // accurately so that (B-A)x(C-A) is always inside triangle ABC. var x = new S2Point(a.X, b.X - a.X, c.X - a.X); var y = new S2Point(a.Y, b.Y - a.Y, c.Y - a.Y); var z = new S2Point(a.Z, b.Z - a.Z, c.Z - a.Z); var r = new S2Point(ra, rb - ra, rc - ra); return(0.5 * new S2Point( y.CrossProd(z).DotProd(r), z.CrossProd(x).DotProd(r), x.CrossProd(y).DotProd(r))); }
public void Test_TriangleTrueCentroid_SmallTriangles() { // Test TrueCentroid() with very small triangles. This test assumes that // the triangle is small enough so that it is nearly planar. for (int iter = 0; iter < 100; ++iter) { S2Testing.GetRandomFrame(out var p, out var x, out var y); double d = 1e-4 * Math.Pow(1e-4, S2Testing.Random.RandDouble()); S2Point p0 = (p - d * x).Normalize(); S2Point p1 = (p + d * x).Normalize(); S2Point p2 = (p + 3 * d * y).Normalize(); S2Point centroid = S2Centroid.TrueCentroid(p0, p1, p2).Normalize(); // The centroid of a planar triangle is at the intersection of its // medians, which is two-thirds of the way along each median. S2Point expected_centroid = (p + d * y).Normalize(); Assert.True(centroid.Angle(expected_centroid) <= 2e-8); } }
public void testCoverage() { Trace.WriteLine("TestCoverage"); // Make sure that random points on the sphere can be represented to the // expected level of accuracy, which in the worst case is sqrt(2/3) times // the maximum arc length between the points on the sphere associated with // adjacent values of "i" or "j". (It is sqrt(2/3) rather than 1/2 because // the cells at the corners of each face are stretched -- they have 60 and // 120 degree angles.) var maxDist = 0.5 * S2Projections.MaxDiag.GetValue(S2CellId.MaxLevel); for (var i = 0; i < 1000000; ++i) { // randomPoint(); var p = new S2Point(0.37861576725894824, 0.2772406863275093, 0.8830558887338725); var q = S2CellId.FromPoint(p).ToPointRaw(); Assert.True(p.Angle(q) <= maxDist); } }
// Return the area of triangle ABC. This method combines two different // algorithms to get accurate results for both large and small triangles. // The maximum error is about 5e-15 (about 0.25 square meters on the Earth's // surface), the same as GirardArea() below, but unlike that method it is // also accurate for small triangles. Example: when the true area is 100 // square meters, Area() yields an error about 1 trillion times smaller than // GirardArea(). // // All points should be unit length, and no two points should be antipodal. // The area is always positive. public static double Area(S2Point a, S2Point b, S2Point c) { System.Diagnostics.Debug.Assert(a.IsUnitLength()); System.Diagnostics.Debug.Assert(b.IsUnitLength()); System.Diagnostics.Debug.Assert(c.IsUnitLength()); // This method is based on l'Huilier's theorem, // // tan(E/4) = Math.Sqrt(tan(s/2) tan((s-a)/2) tan((s-b)/2) tan((s-c)/2)) // // where E is the spherical excess of the triangle (i.e. its area), // a, b, c, are the side lengths, and // s is the semiperimeter (a + b + c) / 2 . // // The only significant source of error using l'Huilier's method is the // cancellation error of the terms (s-a), (s-b), (s-c). This leads to a // *relative* error of about 1e-16 * s / Math.Min(s-a, s-b, s-c). This compares // to a relative error of about 1e-15 / E using Girard's formula, where E is // the true area of the triangle. Girard's formula can be even worse than // this for very small triangles, e.g. a triangle with a true area of 1e-30 // might evaluate to 1e-5. // // So, we prefer l'Huilier's formula unless dmin < s * (0.1 * E), where // dmin = Math.Min(s-a, s-b, s-c). This basically includes all triangles // except for extremely long and skinny ones. // // Since we don't know E, we would like a conservative upper bound on // the triangle area in terms of s and dmin. It's possible to show that // E <= k1 * s * Math.Sqrt(s * dmin), where k1 = 2*Math.Sqrt(3)/Pi (about 1). // Using this, it's easy to show that we should always use l'Huilier's // method if dmin >= k2 * s^5, where k2 is about 1e-2. Furthermore, // if dmin < k2 * s^5, the triangle area is at most k3 * s^4, where // k3 is about 0.1. Since the best case error using Girard's formula // is about 1e-15, this means that we shouldn't even consider it unless // s >= 3e-4 or so. // // TODO(ericv): Implement rigorous error bounds (analysis already done). double sa = b.Angle(c); double sb = c.Angle(a); double sc = a.Angle(b); double s = 0.5 * (sa + sb + sc); if (s >= 3e-4) { // Consider whether Girard's formula might be more accurate. double s2 = s * s; double dmin = s - Math.Max(sa, Math.Max(sb, sc)); if (dmin < 1e-2 * s * s2 * s2) { // This triangle is skinny enough to consider using Girard's formula. // We increase the area by the approximate maximum error in the Girard // calculation in order to ensure that this test is conservative. double area = GirardArea(a, b, c); if (dmin < s * (0.1 * (area + 5e-15))) { return(area); } } } // Use l'Huilier's formula. return(4 * Math.Atan(Math.Sqrt(Math.Max(0.0, Math.Tan(0.5 * s) * Math.Tan(0.5 * (s - sa)) * Math.Tan(0.5 * (s - sb)) * Math.Tan(0.5 * (s - sc)))))); }
/** * Return true if two points are within the given distance of each other * (mainly useful for testing). */ public static bool ApproxEquals(S2Point a, S2Point b, double maxError) { return a.Angle(b) <= maxError; }
/** * Return the area of triangle ABC. The method used is about twice as * expensive as Girard's formula, but it is numerically stable for both large * and very small triangles. The points do not need to be normalized. The area * is always positive. * * The triangle area is undefined if it contains two antipodal points, and * becomes numerically unstable as the length of any edge approaches 180 * degrees. */ internal static double Area(S2Point a, S2Point b, S2Point c) { // This method is based on l'Huilier's theorem, // // tan(E/4) = sqrt(tan(s/2) tan((s-a)/2) tan((s-b)/2) tan((s-c)/2)) // // where E is the spherical excess of the triangle (i.e. its area), // a, b, c, are the side lengths, and // s is the semiperimeter (a + b + c) / 2 . // // The only significant source of error using l'Huilier's method is the // cancellation error of the terms (s-a), (s-b), (s-c). This leads to a // *relative* error of about 1e-16 * s / Min(s-a, s-b, s-c). This compares // to a relative error of about 1e-15 / E using Girard's formula, where E is // the true area of the triangle. Girard's formula can be even worse than // this for very small triangles, e.g. a triangle with a true area of 1e-30 // might evaluate to 1e-5. // // So, we prefer l'Huilier's formula unless dmin < s * (0.1 * E), where // dmin = Min(s-a, s-b, s-c). This basically includes all triangles // except for extremely long and skinny ones. // // Since we don't know E, we would like a conservative upper bound on // the triangle area in terms of s and dmin. It's possible to show that // E <= k1 * s * sqrt(s * dmin), where k1 = 2*sqrt(3)/Pi (about 1). // Using this, it's easy to show that we should always use l'Huilier's // method if dmin >= k2 * s^5, where k2 is about 1e-2. Furthermore, // if dmin < k2 * s^5, the triangle area is at most k3 * s^4, where // k3 is about 0.1. Since the best case error using Girard's formula // is about 1e-15, this means that we shouldn't even consider it unless // s >= 3e-4 or so. // We use volatile doubles to force the compiler to truncate all of these // quantities to 64 bits. Otherwise it may compute a value of dmin > 0 // simply because it chose to spill one of the intermediate values to // memory but not one of the others. var sa = b.Angle(c); var sb = c.Angle(a); var sc = a.Angle(b); var s = 0.5*(sa + sb + sc); if (s >= 3e-4) { // Consider whether Girard's formula might be more accurate. var s2 = s*s; var dmin = s - Math.Max(sa, Math.Max(sb, sc)); if (dmin < 1e-2*s*s2*s2) { // This triangle is skinny enough to consider Girard's formula. var area = GirardArea(a, b, c); if (dmin < s*(0.1*area)) { return area; } } } // Use l'Huilier's formula. return 4 *Math.Atan( Math.Sqrt( Math.Max(0.0, Math.Tan(0.5*s)*Math.Tan(0.5*(s - sa))*Math.Tan(0.5*(s - sb)) *Math.Tan(0.5*(s - sc))))); }
private void TestFaceClipping(S2Point a_raw, S2Point b_raw) { S2Point a = a_raw.Normalize(); S2Point b = b_raw.Normalize(); // First we test GetFaceSegments. FaceSegmentVector segments = new(); GetFaceSegments(a, b, segments); int n = segments.Count; Assert.True(n >= 1); var msg = new StringBuilder($"\nA={a_raw}\nB={b_raw}\nN={S2.RobustCrossProd(a, b)}\nSegments:\n"); int i1 = 0; foreach (var s in segments) { msg.AppendLine($"{i1++}: face={s.face}, a={s.a}, b={s.b}"); } _logger.WriteLine(msg.ToString()); R2Rect biunit = new(new R1Interval(-1, 1), new R1Interval(-1, 1)); var kErrorRadians = kFaceClipErrorRadians; // The first and last vertices should approximately equal A and B. Assert.True(a.Angle(S2.FaceUVtoXYZ(segments[0].face, segments[0].a)) <= kErrorRadians); Assert.True(b.Angle(S2.FaceUVtoXYZ(segments[n - 1].face, segments[n - 1].b)) <= kErrorRadians); S2Point norm = S2.RobustCrossProd(a, b).Normalize(); S2Point a_tangent = norm.CrossProd(a); S2Point b_tangent = b.CrossProd(norm); for (int i = 0; i < n; ++i) { // Vertices may not protrude outside the biunit square. Assert.True(biunit.Contains(segments[i].a)); Assert.True(biunit.Contains(segments[i].b)); if (i == 0) { continue; } // The two representations of each interior vertex (on adjacent faces) // must correspond to exactly the same S2Point. Assert.NotEqual(segments[i - 1].face, segments[i].face); Assert.Equal(S2.FaceUVtoXYZ(segments[i - 1].face, segments[i - 1].b), S2.FaceUVtoXYZ(segments[i].face, segments[i].a)); // Interior vertices should be in the plane containing A and B, and should // be contained in the wedge of angles between A and B (i.e., the dot // products with a_tangent and b_tangent should be non-negative). S2Point p = S2.FaceUVtoXYZ(segments[i].face, segments[i].a).Normalize(); Assert.True(Math.Abs(p.DotProd(norm)) <= kErrorRadians); Assert.True(p.DotProd(a_tangent) >= -kErrorRadians); Assert.True(p.DotProd(b_tangent) >= -kErrorRadians); } // Now we test ClipToPaddedFace (sometimes with a padding of zero). We do // this by defining an (x,y) coordinate system for the plane containing AB, // and converting points along the great circle AB to angles in the range // [-Pi, Pi]. We then accumulate the angle intervals spanned by each // clipped edge; the union over all 6 faces should approximately equal the // interval covered by the original edge. double padding = S2Testing.Random.OneIn(10) ? 0.0 : 1e-10 * Math.Pow(1e-5, S2Testing.Random.RandDouble()); S2Point x_axis = a, y_axis = a_tangent; S1Interval expected_angles = new(0, a.Angle(b)); S1Interval max_angles = expected_angles.Expanded(kErrorRadians); S1Interval actual_angles = new(); for (int face = 0; face < 6; ++face) { if (ClipToPaddedFace(a, b, face, padding, out var a_uv, out var b_uv)) { S2Point a_clip = S2.FaceUVtoXYZ(face, a_uv).Normalize(); S2Point b_clip = S2.FaceUVtoXYZ(face, b_uv).Normalize(); Assert.True(Math.Abs(a_clip.DotProd(norm)) <= kErrorRadians); Assert.True(Math.Abs(b_clip.DotProd(norm)) <= kErrorRadians); if (a_clip.Angle(a) > kErrorRadians) { Assert2.DoubleEqual(1 + padding, Math.Max(Math.Abs(a_uv[0]), Math.Abs(a_uv[1]))); } if (b_clip.Angle(b) > kErrorRadians) { Assert2.DoubleEqual(1 + padding, Math.Max(Math.Abs(b_uv[0]), Math.Abs(b_uv[1]))); } double a_angle = Math.Atan2(a_clip.DotProd(y_axis), a_clip.DotProd(x_axis)); double b_angle = Math.Atan2(b_clip.DotProd(y_axis), b_clip.DotProd(x_axis)); // Rounding errors may cause b_angle to be slightly less than a_angle. // We handle this by constructing the interval with FromPointPair(), // which is okay since the interval length is much less than Math.PI. S1Interval face_angles = S1Interval.FromPointPair(a_angle, b_angle); Assert.True(max_angles.Contains(face_angles)); actual_angles = actual_angles.Union(face_angles); } } Assert.True(actual_angles.Expanded(kErrorRadians).Contains(expected_angles)); }
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 void testCoverage() { Trace.WriteLine("TestCoverage"); // Make sure that random points on the sphere can be represented to the // expected level of accuracy, which in the worst case is sqrt(2/3) times // the maximum arc length between the points on the sphere associated with // adjacent values of "i" or "j". (It is sqrt(2/3) rather than 1/2 because // the cells at the corners of each face are stretched -- they have 60 and // 120 degree angles.) var maxDist = 0.5*S2Projections.MaxDiag.GetValue(S2CellId.MaxLevel); for (var i = 0; i < 1000000; ++i) { // randomPoint(); var p = new S2Point(0.37861576725894824, 0.2772406863275093, 0.8830558887338725); var q = S2CellId.FromPoint(p).ToPointRaw(); Assert.True(p.Angle(q) <= maxDist); } }
/** * Given a point X and an edge AB, return the distance ratio AX / (AX + BX). * If X happens to be on the line segment AB, this is the fraction "t" such * that X == Interpolate(A, B, t). Requires that A and B are distinct. */ public static double GetDistanceFraction(S2Point x, S2Point a0, S2Point a1) { Preconditions.CheckArgument(!a0.Equals(a1)); var d0 = x.Angle(a0); var d1 = x.Angle(a1); return d0/(d0 + d1); }