public void testFaceUVtoXYZ() { // Check that each face appears exactly once. var sum = new S2Point(); for (var face = 0; face < 6; ++face) { var center = S2Projections.FaceUvToXyz(face, 0, 0); assertEquals(S2Projections.GetNorm(face), center); assertEquals(Math.Abs(center[center.LargestAbsComponent]), 1.0); sum = sum + S2Point.Fabs(center); } assertEquals(sum, new S2Point(2, 2, 2)); // Check that each face has a right-handed coordinate system. for (var face = 0; face < 6; ++face) { assertEquals( S2Point.CrossProd(S2Projections.GetUAxis(face), S2Projections.GetVAxis(face)).DotProd( S2Projections.FaceUvToXyz(face, 0, 0)), 1.0); } // Check that the Hilbert curves on each face combine to form a // continuous curve over the entire cube. for (var face = 0; face < 6; ++face) { // The Hilbert curve on each face starts at (-1,-1) and terminates // at either (1,-1) (if axes not swapped) or (-1,1) (if swapped). var sign = ((face & S2.SwapMask) != 0) ? -1 : 1; assertEquals(S2Projections.FaceUvToXyz(face, sign, -sign), S2Projections.FaceUvToXyz((face + 1) % 6, -1, -1)); } }
private static void TestRotate(S2Point p, S2Point axis, S1Angle angle) { S2Point result = S2.Rotate(p, axis, angle); // "result" should be unit length. Assert.True(result.IsUnitLength()); // "result" and "p" should be the same distance from "axis". const double kMaxPositionError = S2.DoubleError; Assert.True((new S1Angle(result, axis) - new S1Angle(p, axis)).Abs() <= kMaxPositionError); // Check that the rotation angle is correct. We allow a fixed error in the // *position* of the result, so we need to convert this into a rotation // angle. The allowable error can be very large as "p" approaches "axis". double axis_distance = p.CrossProd(axis).Norm(); double max_rotation_error; if (axis_distance < kMaxPositionError) { max_rotation_error = S2.M_2_PI; } else { max_rotation_error = Math.Asin(kMaxPositionError / axis_distance); } double actual_rotation = S2.TurnAngle(p, axis, result) + Math.PI; double rotation_error = Math.IEEERemainder(angle.Radians - actual_rotation, S2.M_2_PI); Assert.True(rotation_error <= max_rotation_error); }
public void testGetLengthCentroid() { // Construct random great circles and divide them randomly into segments. // Then make sure that the length and centroid are correct. Note that // because of the way the centroid is computed, it does not matter how // we split the great circle into segments. for (var i = 0; i < 100; ++i) { // Choose a coordinate frame for the great circle. var x = randomPoint(); var y = S2Point.Normalize(S2Point.CrossProd(x, randomPoint())); var z = S2Point.Normalize(S2Point.CrossProd(x, y)); var vertices = new List <S2Point>(); for (double theta = 0; theta < 2 * S2.Pi; theta += Math.Pow(rand.NextDouble(), 10)) { var p = (x * Math.Cos(theta)) + (y * Math.Sin(theta)); if (vertices.Count == 0 || !p.Equals(vertices[vertices.Count - 1])) { vertices.Add(p); } } // Close the circle. vertices.Add(vertices[0]); var line = new S2Polyline(vertices); var length = line.ArcLengthAngle; assertTrue(Math.Abs(length.Radians - 2 * S2.Pi) < 2e-14); } }
/** * Return a right-handed coordinate frame (three orthonormal vectors). Returns * an array of three points: x,y,z */ public IReadOnlyList <S2Point> getRandomFrame() { var p0 = randomPoint(); var p1 = S2Point.Normalize(S2Point.CrossProd(p0, randomPoint())); var p2 = S2Point.Normalize(S2Point.CrossProd(p0, p1)); return(new List <S2Point>(new[] { p0, p1, p2 })); }
public void testFaces() { IDictionary <S2Point, int> edgeCounts = new Dictionary <S2Point, int>(); IDictionary <S2Point, int> vertexCounts = new Dictionary <S2Point, int>(); for (var face = 0; face < 6; ++face) { var id = S2CellId.FromFacePosLevel(face, 0, 0); var cell = new S2Cell(id); JavaAssert.Equal(cell.Id, id); JavaAssert.Equal(cell.Face, face); JavaAssert.Equal(cell.Level, 0); // Top-level faces have alternating orientations to get RHS coordinates. JavaAssert.Equal(cell.Orientation, face & S2.SwapMask); Assert.True(!cell.IsLeaf); for (var k = 0; k < 4; ++k) { if (edgeCounts.ContainsKey(cell.GetEdgeRaw(k))) { edgeCounts[cell.GetEdgeRaw(k)] = edgeCounts[cell .GetEdgeRaw(k)] + 1; } else { edgeCounts[cell.GetEdgeRaw(k)] = 1; } if (vertexCounts.ContainsKey(cell.GetVertexRaw(k))) { vertexCounts[cell.GetVertexRaw(k)] = vertexCounts[cell .GetVertexRaw(k)] + 1; } else { vertexCounts[cell.GetVertexRaw(k)] = 1; } assertDoubleNear(cell.GetVertexRaw(k).DotProd(cell.GetEdgeRaw(k)), 0); assertDoubleNear(cell.GetVertexRaw((k + 1) & 3).DotProd( cell.GetEdgeRaw(k)), 0); assertDoubleNear(S2Point.Normalize( S2Point.CrossProd(cell.GetVertexRaw(k), cell .GetVertexRaw((k + 1) & 3))).DotProd(cell.GetEdge(k)), 1.0); } } // Check that edges have multiplicity 2 and vertices have multiplicity 3. foreach (var i in edgeCounts.Values) { JavaAssert.Equal(i, 2); } foreach (var i in vertexCounts.Values) { JavaAssert.Equal(i, 3); } }
// 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 testUVNorms() { // Check that GetUNorm and GetVNorm compute right-handed normals for // an edge in the increasing U or V direction. for (var face = 0; face < 6; ++face) { for (double x = -1; x <= 1; x += 1 / 1024.0) { assertDoubleNear( S2Point.CrossProd( S2Projections.FaceUvToXyz(face, x, -1), S2Projections.FaceUvToXyz(face, x, 1)) .Angle(S2Projections.GetUNorm(face, x)), 0); assertDoubleNear( S2Point.CrossProd( S2Projections.FaceUvToXyz(face, -1, x), S2Projections.FaceUvToXyz(face, 1, x)) .Angle(S2Projections.GetVNorm(face, x)), 0); } } }
// 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); }
protected S2Point samplePoint(S2Cap cap) { // We consider the cap axis to be the "z" axis. We choose two other axes to // complete the coordinate frame. var z = cap.Axis; var x = z.Ortho; var y = S2Point.CrossProd(z, x); // The surface area of a spherical cap is directly proportional to its // height. First we choose a random height, and then we choose a random // point along the circle at that height. var h = rand.NextDouble() * cap.Height; var theta = 2 * S2.Pi * rand.NextDouble(); var r = Math.Sqrt(h * (2 - h)); // Radius of circle. // (cos(theta)*r*x + sin(theta)*r*y + (1-h)*z).Normalize() return(S2Point.Normalize(((x * Math.Cos(theta) * r) + (y * Math.Sin(theta) * r)) + (z * (1 - h)))); }
// 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 S2CapBasicTest() { // Test basic properties of empty and full caps. var empty = S2Cap.Empty; var full = S2Cap.Full; Assert.True(empty.IsValid); Assert.True(empty.IsEmpty); Assert.True(empty.Complement.IsFull); Assert.True(full.IsValid); Assert.True(full.IsFull); Assert.True(full.Complement.IsEmpty); JavaAssert.Equal(full.Height, 2.0); assertDoubleNear(full.Angle.Degrees, 180); // Containment and intersection of empty and full caps. Assert.True(empty.Contains(empty)); Assert.True(full.Contains(empty)); Assert.True(full.Contains(full)); Assert.True(!empty.InteriorIntersects(empty)); Assert.True(full.InteriorIntersects(full)); Assert.True(!full.InteriorIntersects(empty)); // Singleton cap containing the x-axis. var xaxis = S2Cap.FromAxisHeight(new S2Point(1, 0, 0), 0); Assert.True(xaxis.Contains(new S2Point(1, 0, 0))); Assert.True(!xaxis.Contains(new S2Point(1, 1e-20, 0))); JavaAssert.Equal(xaxis.Angle.Radians, 0.0); // Singleton cap containing the y-axis. var yaxis = S2Cap.FromAxisAngle(new S2Point(0, 1, 0), S1Angle.FromRadians(0)); Assert.True(!yaxis.Contains(xaxis.Axis)); JavaAssert.Equal(xaxis.Height, 0.0); // Check that the complement of a singleton cap is the full cap. var xcomp = xaxis.Complement; Assert.True(xcomp.IsValid); Assert.True(xcomp.IsFull); Assert.True(xcomp.Contains(xaxis.Axis)); // Check that the complement of the complement is *not* the original. Assert.True(xcomp.Complement.IsValid); Assert.True(xcomp.Complement.IsEmpty); Assert.True(!xcomp.Complement.Contains(xaxis.Axis)); // Check that very small caps can be represented accurately. // Here "kTinyRad" is small enough that unit vectors perturbed by this // amount along a tangent do not need to be renormalized. var kTinyRad = 1e-10; var tiny = S2Cap.FromAxisAngle(S2Point.Normalize(new S2Point(1, 2, 3)), S1Angle.FromRadians(kTinyRad)); var tangent = S2Point.Normalize(S2Point.CrossProd(tiny.Axis, new S2Point(3, 2, 1))); Assert.True(tiny.Contains(tiny.Axis + (tangent * 0.99 * kTinyRad))); Assert.True(!tiny.Contains(tiny.Axis + (tangent * 1.01 * kTinyRad))); // Basic tests on a hemispherical cap. var hemi = S2Cap.FromAxisHeight(S2Point.Normalize(new S2Point(1, 0, 1)), 1); JavaAssert.Equal(hemi.Complement.Axis, -hemi.Axis); JavaAssert.Equal(hemi.Complement.Height, 1.0); Assert.True(hemi.Contains(new S2Point(1, 0, 0))); Assert.True(!hemi.Complement.Contains(new S2Point(1, 0, 0))); Assert.True(hemi.Contains(S2Point.Normalize(new S2Point(1, 0, -(1 - EPS))))); Assert.True(!hemi.InteriorContains(S2Point.Normalize(new S2Point(1, 0, -(1 + EPS))))); // A concave cap. var concave = S2Cap.FromAxisAngle(getLatLngPoint(80, 10), S1Angle.FromDegrees(150)); Assert.True(concave.Contains(getLatLngPoint(-70 * (1 - EPS), 10))); Assert.True(!concave.Contains(getLatLngPoint(-70 * (1 + EPS), 10))); Assert.True(concave.Contains(getLatLngPoint(-50 * (1 - EPS), -170))); Assert.True(!concave.Contains(getLatLngPoint(-50 * (1 + EPS), -170))); // Cap containment tests. Assert.True(!empty.Contains(xaxis)); Assert.True(!empty.InteriorIntersects(xaxis)); Assert.True(full.Contains(xaxis)); Assert.True(full.InteriorIntersects(xaxis)); Assert.True(!xaxis.Contains(full)); Assert.True(!xaxis.InteriorIntersects(full)); Assert.True(xaxis.Contains(xaxis)); Assert.True(!xaxis.InteriorIntersects(xaxis)); Assert.True(xaxis.Contains(empty)); Assert.True(!xaxis.InteriorIntersects(empty)); Assert.True(hemi.Contains(tiny)); Assert.True(hemi.Contains( S2Cap.FromAxisAngle(new S2Point(1, 0, 0), S1Angle.FromRadians(S2.PiOver4 - EPS)))); Assert.True(!hemi.Contains( S2Cap.FromAxisAngle(new S2Point(1, 0, 0), S1Angle.FromRadians(S2.PiOver4 + EPS)))); Assert.True(concave.Contains(hemi)); Assert.True(concave.InteriorIntersects(hemi.Complement)); Assert.True(!concave.Contains(S2Cap.FromAxisHeight(-concave.Axis, 0.1))); }
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 bool testBuilder(TestCase test) { for (var iter = 0; iter < 200; ++iter) { // Initialize to the default options, which are changed below var options = S2PolygonBuilderOptions.DirectedXor; options.UndirectedEdges = evalTristate(test.undirectedEdges); options.XorEdges = evalTristate(test.xorEdges); // Each test has a minimum and a maximum merge distance. The merge // distance must be at least the given minimum to ensure that all expected // merging will take place, and it must be at most the given maximum to // ensure that no unexpected merging takes place. // // If the minimum and maximum values are different, we have some latitude // to perturb the vertices as long as the merge distance is adjusted // appropriately. If "p" is the maximum perturbation distance, "min" and // "max" are the min/max merge distances, and "m" is the actual merge // distance for this test, we require that // // x >= min + 2*p and x <= max - 2*p . // // This implies that p <= 0.25 * (max - min). We choose "p" so that it is // zero half of the time, and otherwise chosen randomly up to this limit. var minMerge = S1Angle.FromDegrees(test.minMerge).Radians; var maxMerge = S1Angle.FromDegrees(test.maxMerge).Radians; var r = Math.Max(0.0, 2 * rand.NextDouble() - 1); var maxPerturbation = r * 0.25 * (maxMerge - minMerge); // Now we set the merge distance chosen randomly within the limits above // (min + 2*p and max - 2*p). Half of the time we set the merge distance // to the minimum value. r = Math.Max(0.0, 2 * rand.NextDouble() - 1); options.MergeDistance = S1Angle.FromRadians( minMerge + 2 * maxPerturbation + r * (maxMerge - minMerge - 4 * maxPerturbation)); options.Validate = true; var builder = new S2PolygonBuilder(options); // On each iteration we randomly rotate the test case around the sphere. // This causes the S2PolygonBuilder to choose different first edges when // trying to build loops. var x = randomPoint(); var y = S2Point.Normalize(S2Point.CrossProd(x, randomPoint())); var z = S2Point.Normalize(S2Point.CrossProd(x, y)); foreach (var chain in test.chainsIn) { addChain(chain, x, y, z, maxPerturbation, builder); } var loops = new List <S2Loop>(); var unusedEdges = new List <S2Edge>(); if (test.xorEdges < 0) { builder.AssembleLoops(loops, unusedEdges); } else { var polygon = new S2Polygon(); builder.AssemblePolygon(polygon, unusedEdges); polygon.Release(loops); } var expected = new List <S2Loop>(); foreach (var loop in test.loopsOut) { var vertices = new List <S2Point>(); getVertices(loop, x, y, z, 0, vertices); expected.Add(new S2Loop(vertices)); } // We assume that the vertex locations in the expected output polygon // are separated from the corresponding vertex locations in the input // edges by at most half of the minimum merge distance. Essentially // this means that the expected output vertices should be near the // centroid of the various input vertices. var maxError = 0.5 * minMerge + maxPerturbation; // Note single "|" below so that we print both sets of loops. if (findMissingLoops(loops, expected, maxError, "Actual") | findMissingLoops(expected, loops, maxError, "Expected")) { Console.Error.WriteLine( "During iteration " + iter + ", undirected: " + options.UndirectedEdges + ", xor: " + options.XorEdges + "\n\n"); return(false); } if (unusedEdges.Count != test.numUnusedEdges) { Console.Error.WriteLine("Wrong number of unused edges: " + unusedEdges.Count + " (should be " + test.numUnusedEdges + ")\n"); return(false); } } return(true); }
public void testAreaCentroid() { assertDoubleNear(northHemi.Area, 2 * S2.Pi); assertDoubleNear(eastHemi.Area, 2 * S2.Pi); // Construct spherical caps of random height, and approximate their boundary // with closely spaces vertices. Then check that the area and centroid are // correct. for (var i = 0; i < 100; ++i) { // Choose a coordinate frame for the spherical cap. var x = randomPoint(); var y = S2Point.Normalize(S2Point.CrossProd(x, randomPoint())); var z = S2Point.Normalize(S2Point.CrossProd(x, y)); // Given two points at latitude phi and whose longitudes differ by dtheta, // the geodesic between the two points has a maximum latitude of // atan(Tan(phi) / Cos(dtheta/2)). This can be derived by positioning // the two points at (-dtheta/2, phi) and (dtheta/2, phi). // // We want to position the vertices close enough together so that their // maximum distance from the boundary of the spherical cap is kMaxDist. // Thus we want fabs(atan(Tan(phi) / Cos(dtheta/2)) - phi) <= kMaxDist. var kMaxDist = 1e-6; var height = 2 * rand.NextDouble(); var phi = Math.Asin(1 - height); var maxDtheta = 2 * Math.Acos(Math.Tan(Math.Abs(phi)) / Math.Tan(Math.Abs(phi) + kMaxDist)); maxDtheta = Math.Min(S2.Pi, maxDtheta); // At least 3 vertices. var vertices = new List <S2Point>(); for (double theta = 0; theta < 2 * S2.Pi; theta += rand.NextDouble() * maxDtheta) { var xCosThetaCosPhi = x * (Math.Cos(theta) * Math.Cos(phi)); var ySinThetaCosPhi = y * (Math.Sin(theta) * Math.Cos(phi)); var zSinPhi = z * Math.Sin(phi); var sum = xCosThetaCosPhi + ySinThetaCosPhi + zSinPhi; vertices.Add(sum); } var loop = new S2Loop(vertices); var areaCentroid = loop.AreaAndCentroid; var area = loop.Area; var centroid = loop.Centroid; var expectedArea = 2 * S2.Pi * height; assertTrue(areaCentroid.Area == area); assertTrue(centroid.Equals(areaCentroid.Centroid)); assertTrue(Math.Abs(area - expectedArea) <= 2 * S2.Pi * kMaxDist); // high probability assertTrue(Math.Abs(area - expectedArea) >= 0.01 * kMaxDist); var expectedCentroid = z * expectedArea * (1 - 0.5 * height); assertTrue((centroid.Value - expectedCentroid).Norm <= 2 * kMaxDist); } }