예제 #1
0
        public void Test_S2Cell_CellVsLoopRectBound()
        {
            // This test verifies that the S2Cell and S2Loop bounds contain each other
            // to within their maximum errors.
            //
            // The S2Cell and S2Loop calculations for the latitude of a vertex can differ
            // by up to 2 * S2Constants.DoubleEpsilon, therefore the S2Cell bound should never exceed
            // the S2Loop bound by more than this (the reverse is not true, because the
            // S2Loop code sometimes thinks that the maximum occurs along an edge).
            // Similarly, the longitude bounds can differ by up to 4 * S2Constants.DoubleEpsilon since
            // the S2Cell bound has an error of 2 * S2Constants.DoubleEpsilon and then expands by this
            // amount, while the S2Loop bound does no expansion at all.

            for (int iter = 0; iter < 1000; ++iter)
            {
                S2Cell       cell       = new(S2Testing.GetRandomCellId());
                S2Loop       loop       = new(cell);
                S2LatLngRect cell_bound = cell.GetRectBound();
                S2LatLngRect loop_bound = loop.GetRectBound();
                Assert.True(loop_bound.Expanded(kCellError).Contains(cell_bound));
                Assert.True(cell_bound.Expanded(kLoopError).Contains(loop_bound));
            }
        }
예제 #2
0
    // Expands a bound returned by GetBound() so that it is guaranteed to
    // contain the bounds of any subregion whose bounds are computed using
    // this class.  For example, consider a loop L that defines a square.
    // GetBound() ensures that if a point P is contained by this square, then
    // S2LatLng(P) is contained by the bound.  But now consider a diamond
    // shaped loop S contained by L.  It is possible that GetBound() returns a
    // *larger* bound for S than it does for L, due to rounding errors.  This
    // method expands the bound for L so that it is guaranteed to contain the
    // bounds of any subregion S.
    //
    // More precisely, if L is a loop that does not contain either pole, and S
    // is a loop such that L.Contains(S), then
    //
    //   ExpandForSubregions(RectBound(L)).Contains(RectBound(S)).
    public static S2LatLngRect ExpandForSubregions(S2LatLngRect bound)
    {
        // Empty bounds don't need expansion.
        if (bound.IsEmpty())
        {
            return(bound);
        }

        // First we need to check whether the bound B contains any nearly-antipodal
        // points (to within 4.309 * S2Constants.DoubleEpsilon).  If so then we need to return
        // S2LatLngRect.Full(), since the subregion might have an edge between two
        // such points, and AddPoint() returns Full() for such edges.  Note that
        // this can happen even if B is not Full(); for example, consider a loop
        // that defines a 10km strip straddling the equator extending from
        // longitudes -100 to +100 degrees.
        //
        // It is easy to check whether B contains any antipodal points, but checking
        // for nearly-antipodal points is trickier.  Essentially we consider the
        // original bound B and its reflection through the origin B', and then test
        // whether the minimum distance between B and B' is less than 4.309 *
        // S2Constants.DoubleEpsilon.

        // "lng_gap" is a lower bound on the longitudinal distance between B and its
        // reflection B'.  (2.5 * S2Constants.DoubleEpsilon is the maximum combined error of the
        // endpoint longitude calculations and the GetLength() call.)
        double lng_gap = Math.Max(0.0, Math.PI - bound.Lng.GetLength() - 2.5 * S2.DoubleEpsilon);

        // "min_abs_lat" is the minimum distance from B to the equator (if zero or
        // negative, then B straddles the equator).
        double min_abs_lat = Math.Max(bound.Lat.Lo, -bound.Lat.Hi);

        // "lat_gap1" and "lat_gap2" measure the minimum distance from B to the
        // south and north poles respectively.
        double lat_gap1 = S2.M_PI_2 + bound.Lat.Lo;
        double lat_gap2 = S2.M_PI_2 - bound.Lat.Hi;

        if (min_abs_lat >= 0)
        {
            // The bound B does not straddle the equator.  In this case the minimum
            // distance is between one endpoint of the latitude edge in B closest to
            // the equator and the other endpoint of that edge in B'.  The latitude
            // distance between these two points is 2*min_abs_lat, and the longitude
            // distance is lng_gap.  We could compute the distance exactly using the
            // Haversine formula, but then we would need to bound the errors in that
            // calculation.  Since we only need accuracy when the distance is very
            // small (close to 4.309 * S2Constants.DoubleEpsilon), we substitute the Euclidean
            // distance instead.  This gives us a right triangle XYZ with two edges of
            // length x = 2*min_abs_lat and y ~= lng_gap.  The desired distance is the
            // length of the third edge "z", and we have
            //
            //         z  ~=  Math.Sqrt(x^2 + y^2)  >=  (x + y) / Math.Sqrt(2)
            //
            // Therefore the region may contain nearly antipodal points only if
            //
            //  2*min_abs_lat + lng_gap  <  Math.Sqrt(2) * 4.309 * S2Constants.DoubleEpsilon
            //                           ~= 1.354e-15
            //
            // Note that because the given bound B is conservative, "min_abs_lat" and
            // "lng_gap" are both lower bounds on their true values so we do not need
            // to make any adjustments for their errors.
            if (2 * min_abs_lat + lng_gap < 1.354e-15)
            {
                return(S2LatLngRect.Full);
            }
        }
        else if (lng_gap >= S2.M_PI_2)
        {
            // B spans at most Pi/2 in longitude.  The minimum distance is always
            // between one corner of B and the diagonally opposite corner of B'.  We
            // use the same distance approximation that we used above; in this case
            // we have an obtuse triangle XYZ with two edges of length x = lat_gap1
            // and y = lat_gap2, and angle Z >= Pi/2 between them.  We then have
            //
            //         z  >=  Math.Sqrt(x^2 + y^2)  >=  (x + y) / Math.Sqrt(2)
            //
            // Unlike the case above, "lat_gap1" and "lat_gap2" are not lower bounds
            // (because of the extra addition operation, and because S2Constants.M_PI_2 is not
            // exactly equal to Pi/2); they can exceed their true values by up to
            // 0.75 * S2Constants.DoubleEpsilon.  Putting this all together, the region may
            // contain nearly antipodal points only if
            //
            //   lat_gap1 + lat_gap2  <  (Math.Sqrt(2) * 4.309 + 1.5) * S2Constants.DoubleEpsilon
            //                        ~= 1.687e-15
            if (lat_gap1 + lat_gap2 < 1.687e-15)
            {
                return(S2LatLngRect.Full);
            }
        }
        else
        {
            // Otherwise we know that (1) the bound straddles the equator and (2) its
            // width in longitude is at least Pi/2.  In this case the minimum
            // distance can occur either between a corner of B and the diagonally
            // opposite corner of B' (as in the case above), or between a corner of B
            // and the opposite longitudinal edge reflected in B'.  It is sufficient
            // to only consider the corner-edge case, since this distance is also a
            // lower bound on the corner-corner distance when that case applies.

            // Consider the spherical triangle XYZ where X is a corner of B with
            // minimum absolute latitude, Y is the closest pole to X, and Z is the
            // point closest to X on the opposite longitudinal edge of B'.  This is a
            // right triangle (Z = Pi/2), and from the spherical law of sines we have
            //
            //     sin(z) / sin(Z)  =  sin(y) / sin(Y)
            //     sin(max_lat_gap) / 1  =  sin(d_min) / sin(lng_gap)
            //     sin(d_min)  =  sin(max_lat_gap) * sin(lng_gap)
            //
            // where "max_lat_gap" = Math.Max(lat_gap1, lat_gap2) and "d_min" is the
            // desired minimum distance.  Now using the facts that sin(t) >= (2/Pi)*t
            // for 0 <= t <= Pi/2, that we only need an accurate approximation when
            // at least one of "max_lat_gap" or "lng_gap" is extremely small (in
            // which case sin(t) ~= t), and recalling that "max_lat_gap" has an error
            // of up to 0.75 * S2Constants.DoubleEpsilon, we want to test whether
            //
            //   max_lat_gap * lng_gap  <  (4.309 + 0.75) * (Pi/2) * S2Constants.DoubleEpsilon
            //                          ~= 1.765e-15
            if (Math.Max(lat_gap1, lat_gap2) * lng_gap < 1.765e-15)
            {
                return(S2LatLngRect.Full);
            }
        }
        // Next we need to check whether the subregion might contain any edges that
        // span (Math.PI - 2 * S2Constants.DoubleEpsilon) radians or more in longitude, since AddPoint
        // sets the longitude bound to Full() in that case.  This corresponds to
        // testing whether (lng_gap <= 0) in "lng_expansion" below.

        // Otherwise, the maximum latitude error in AddPoint is 4.8 * S2Constants.DoubleEpsilon.
        // In the worst case, the errors when computing the latitude bound for a
        // subregion could go in the opposite direction as the errors when computing
        // the bound for the original region, so we need to double this value.
        // (More analysis shows that it's okay to round down to a multiple of
        // S2Constants.DoubleEpsilon.)
        //
        // For longitude, we rely on the fact that atan2 is correctly rounded and
        // therefore no additional bounds expansion is necessary.

        double lat_expansion = 9 * S2.DoubleEpsilon;
        double lng_expansion = (lng_gap <= 0) ? Math.PI : 0;

        return(bound.Expanded(S2LatLng.FromRadians(lat_expansion,
                                                   lng_expansion)).PolarClosure());
    }