Пример #1
0
    /////////////////////////////////////////////////////////////////////////////
    ///////////////         (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));
    }
Пример #2
0
        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);
        }
Пример #3
0
        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);
            }
        }
Пример #4
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)));
    }
Пример #5
0
        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);
            }
        }
Пример #7
0
    // 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))))));
    }
Пример #8
0
        /**
   * 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;
        }
Пример #9
0
        /**
   * 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)))));
        }
Пример #10
0
    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));
    }
Пример #11
0
    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);
        }