Exemple #1
0
        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));
            }
        }
Exemple #2
0
    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);
            }
        }
Exemple #6
0
    // 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)));
    }
Exemple #7
0
 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);
         }
     }
 }
Exemple #8
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))));
        }
Exemple #10
0
    // 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);
    }
Exemple #11
0
        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));
    }
Exemple #13
0
        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);
            }
        }