public void Test_S2LatLngRect_GetCentroid() { // Empty and full rectangles. Assert.Equal(new S2Point(), S2LatLngRect.Empty.Centroid()); Assert.True(S2LatLngRect.Full.Centroid().Norm() <= 1e-15); // Rectangles that cover the full longitude range. for (int i = 0; i < 100; ++i) { double lat1 = S2Testing.Random.UniformDouble(-S2.M_PI_2, S2.M_PI_2); double lat2 = S2Testing.Random.UniformDouble(-S2.M_PI_2, S2.M_PI_2); S2LatLngRect r = new(R1Interval.FromPointPair(lat1, lat2), S1Interval.Full); S2Point centroid = r.Centroid(); Assert2.Near(0.5 * (Math.Sin(lat1) + Math.Sin(lat2)) * r.Area(), centroid.Z, S2.DoubleError); Assert.True(new R2Point(centroid.X, centroid.Y).GetNorm() <= 1e-15); } // Rectangles that cover the full latitude range. for (int i = 0; i < 100; ++i) { double lng1 = S2Testing.Random.UniformDouble(-Math.PI, Math.PI); double lng2 = S2Testing.Random.UniformDouble(-Math.PI, Math.PI); S2LatLngRect r = new(S2LatLngRect.FullLat, S1Interval.FromPointPair(lng1, lng2)); S2Point centroid = r.Centroid(); Assert.True(Math.Abs(centroid.Z) <= 1e-15); Assert2.Near(r.Lng.GetCenter(), new S2LatLng(centroid).LngRadians, S2.DoubleError); double alpha = 0.5 * r.Lng.GetLength(); // TODO(Alas): the next Assert fails sometimes Assert2.Near(0.25 * Math.PI * Math.Sin(alpha) / alpha * r.Area(), new R2Point(centroid.X, centroid.Y).GetNorm(), S2.DoubleError); } // Finally, verify that when a rectangle is recursively split into pieces, // the centroids of the pieces add to give the centroid of their parent. // To make the code simpler we avoid rectangles that cross the 180 degree // line of longitude. TestCentroidSplitting( new S2LatLngRect(S2LatLngRect.FullLat, new S1Interval(-3.14, 3.14)), 10 /*splits_left*/); }
public void Test_R1Interval_TestBasic() { // Constructors and accessors. R1Interval unit = new(0, 1); R1Interval negunit = new(-1, 0); Assert.Equal(0, unit.Lo); Assert.Equal(1, unit.Hi); Assert.Equal(-1, negunit[0]); Assert.Equal(0, negunit[1]); R1Interval ten = new(0, 10); Assert.Equal(new R1Interval(0, 10), ten); ten = new R1Interval(-10, ten.Hi); Assert.Equal(new R1Interval(-10, 10), ten); ten = new R1Interval(ten.Lo, 0); Assert.True(new R1Interval(-10, 0) == ten); ten = new R1Interval(0, 10); Assert.Equal(new R1Interval(0, 10), ten); // IsEmpty R1Interval half = new(0.5, 0.5); Assert.False(unit.IsEmpty()); Assert.False(half.IsEmpty()); R1Interval empty = R1Interval.Empty; Assert.True(empty.IsEmpty()); // == and != Assert.True(empty == R1Interval.Empty); Assert.True(unit == new R1Interval(0, 1)); Assert.True(unit != empty); Assert.True(new R1Interval(1, 2) != new R1Interval(1, 3)); // Check that the default R1Interval is identical to Empty(). R1Interval default_empty = R1Interval.Empty; Assert.True(default_empty.IsEmpty()); Assert.Equal(empty.Lo, default_empty.Lo); Assert.Equal(empty.Hi, default_empty.Hi); // GetCenter(), GetLength() Assert.Equal(0.5, unit.GetCenter()); Assert.Equal(0.5, half.GetCenter()); Assert.Equal(1.0, negunit.GetLength()); Assert.Equal(0, half.GetLength()); Assert.True(empty.GetLength() < 0); // Contains(double), InteriorContains(double) Assert.True(unit.Contains(0.5)); Assert.True(unit.InteriorContains(0.5)); Assert.True(unit.Contains(0)); Assert.False(unit.InteriorContains(0)); Assert.True(unit.Contains(1)); Assert.False(unit.InteriorContains(1)); // Contains(R1Interval), InteriorContains(R1Interval) // Intersects(R1Interval), InteriorIntersects(R1Interval) TestIntervalOps(empty, empty, "TTFF"); TestIntervalOps(empty, unit, "FFFF"); TestIntervalOps(unit, half, "TTTT"); TestIntervalOps(unit, unit, "TFTT"); TestIntervalOps(unit, empty, "TTFF"); TestIntervalOps(unit, negunit, "FFTF"); TestIntervalOps(unit, new R1Interval(0, 0.5), "TFTT"); TestIntervalOps(half, new R1Interval(0, 0.5), "FFTF"); // AddPoint() R1Interval r = empty; r = R1Interval.AddPoint(r, 5); Assert.Equal(5, r.Lo); Assert.Equal(5, r.Hi); r = R1Interval.AddPoint(r, -1); Assert.Equal(-1, r.Lo); Assert.Equal(5, r.Hi); r = R1Interval.AddPoint(r, 0); Assert.Equal(-1, r.Lo); Assert.Equal(5, r.Hi); // Project() Assert.Equal(0.3, new R1Interval(0.1, 0.4).Project(0.3)); Assert.Equal(0.1, new R1Interval(0.1, 0.4).Project(-7.0)); Assert.Equal(0.4, new R1Interval(0.1, 0.4).Project(0.6)); // FromPointPair() Assert.Equal(new R1Interval(4, 4), R1Interval.FromPointPair(4, 4)); Assert.Equal(new R1Interval(-2, -1), R1Interval.FromPointPair(-1, -2)); Assert.Equal(new R1Interval(-5, 3), R1Interval.FromPointPair(-5, 3)); // Expanded() Assert.Equal(empty, empty.Expanded(0.45)); Assert.Equal(new R1Interval(-0.5, 1.5), unit.Expanded(0.5)); Assert.Equal(new R1Interval(0.5, 0.5), unit.Expanded(-0.5)); Assert.True(unit.Expanded(-0.51).IsEmpty()); Assert.True(unit.Expanded(-0.51).Expanded(0.51).IsEmpty()); // Union(), Intersection() Assert.Equal(new R1Interval(99, 100), new R1Interval(99, 100).Union(empty)); Assert.Equal(new R1Interval(99, 100), empty.Union(new R1Interval(99, 100))); Assert.True(new R1Interval(5, 3).Union(new R1Interval(0, -2)).IsEmpty()); Assert.True(new R1Interval(0, -2).Union(new R1Interval(5, 3)).IsEmpty()); Assert.Equal(unit, unit.Union(unit)); Assert.Equal(new R1Interval(-1, 1), unit.Union(negunit)); Assert.Equal(new R1Interval(-1, 1), negunit.Union(unit)); Assert.Equal(unit, half.Union(unit)); Assert.Equal(half, unit.Intersection(half)); Assert.Equal(new R1Interval(0, 0), unit.Intersection(negunit)); Assert.True(negunit.Intersection(half).IsEmpty()); Assert.True(unit.Intersection(empty).IsEmpty()); Assert.True(empty.Intersection(unit).IsEmpty()); }
public void R1IntervalBasicTest() { // Constructors and accessors. var unit = new R1Interval(0, 1); var negunit = new R1Interval(-1, 0); JavaAssert.Equal(unit.Lo, 0.0); JavaAssert.Equal(unit.Hi, 1.0); JavaAssert.Equal(negunit.Lo, -1.0); JavaAssert.Equal(negunit.Hi, 0.0); // is_empty() var half = new R1Interval(0.5, 0.5); Assert.True(!unit.IsEmpty); Assert.True(!half.IsEmpty); var empty = R1Interval.Empty; Assert.True(empty.IsEmpty); // GetCenter(), GetLength() JavaAssert.Equal(unit.Center, 0.5); JavaAssert.Equal(half.Center, 0.5); JavaAssert.Equal(negunit.Length, 1.0); JavaAssert.Equal(half.Length, 0.0); Assert.True(empty.Length < 0); // contains(double), interiorContains(double) Assert.True(unit.Contains(0.5)); Assert.True(unit.InteriorContains(0.5)); Assert.True(unit.Contains(0)); Assert.True(!unit.InteriorContains(0)); Assert.True(unit.Contains(1)); Assert.True(!unit.InteriorContains(1)); // contains(R1Interval), interiorContains(R1Interval) // Intersects(R1Interval), InteriorIntersects(R1Interval) testIntervalOps(empty, empty, "TTFF"); testIntervalOps(empty, unit, "FFFF"); testIntervalOps(unit, half, "TTTT"); testIntervalOps(unit, unit, "TFTT"); testIntervalOps(unit, empty, "TTFF"); testIntervalOps(unit, negunit, "FFTF"); testIntervalOps(unit, new R1Interval(0, 0.5), "TFTT"); testIntervalOps(half, new R1Interval(0, 0.5), "FFTF"); // addPoint() R1Interval r; r = empty.AddPoint(5); Assert.True(r.Lo == 5.0 && r.Hi == 5.0); r = r.AddPoint(-1); Assert.True(r.Lo == -1.0 && r.Hi == 5.0); r = r.AddPoint(0); Assert.True(r.Lo == -1.0 && r.Hi == 5.0); // fromPointPair() JavaAssert.Equal(R1Interval.FromPointPair(4, 4), new R1Interval(4, 4)); JavaAssert.Equal(R1Interval.FromPointPair(-1, -2), new R1Interval(-2, -1)); JavaAssert.Equal(R1Interval.FromPointPair(-5, 3), new R1Interval(-5, 3)); // expanded() JavaAssert.Equal(empty.Expanded(0.45), empty); JavaAssert.Equal(unit.Expanded(0.5), new R1Interval(-0.5, 1.5)); // union(), intersection() Assert.True(new R1Interval(99, 100).Union(empty).Equals(new R1Interval(99, 100))); Assert.True(empty.Union(new R1Interval(99, 100)).Equals(new R1Interval(99, 100))); Assert.True(new R1Interval(5, 3).Union(new R1Interval(0, -2)).IsEmpty); Assert.True(new R1Interval(0, -2).Union(new R1Interval(5, 3)).IsEmpty); Assert.True(unit.Union(unit).Equals(unit)); Assert.True(unit.Union(negunit).Equals(new R1Interval(-1, 1))); Assert.True(negunit.Union(unit).Equals(new R1Interval(-1, 1))); Assert.True(half.Union(unit).Equals(unit)); Assert.True(unit.Intersection(half).Equals(half)); Assert.True(unit.Intersection(negunit).Equals(new R1Interval(0, 0))); Assert.True(negunit.Intersection(half).IsEmpty); Assert.True(unit.Intersection(empty).IsEmpty); Assert.True(empty.Intersection(unit).IsEmpty); }
// Common back end for AddPoint() and AddLatLng(). b and b_latlng // must refer to the same vertex. private void AddInternal(S2Point b, S2LatLng b_latlng) { // Simple consistency check to verify that b and b_latlng are alternate // representations of the same vertex. System.Diagnostics.Debug.Assert(S2.ApproxEquals(b, b_latlng.ToPoint())); if (bound_.IsEmpty()) { bound_ = bound_.AddPoint(b_latlng); } else { // First compute the cross product N = A x B robustly. This is the normal // to the great circle through A and B. We don't use S2.RobustCrossProd() // since that method returns an arbitrary vector orthogonal to A if the two // vectors are proportional, and we want the zero vector in that case. var n = (a_ - b).CrossProd(a_ + b); // N = 2 * (A x B) // The relative error in N gets large as its norm gets very small (i.e., // when the two points are nearly identical or antipodal). We handle this // by choosing a maximum allowable error, and if the error is greater than // this we fall back to a different technique. Since it turns out that // the other sources of error in converting the normal to a maximum // latitude add up to at most 1.16 * S2Constants.DoubleEpsilon (see below), and it is // desirable to have the total error be a multiple of S2Constants.DoubleEpsilon, we have // chosen to limit the maximum error in the normal to 3.84 * S2Constants.DoubleEpsilon. // It is possible to show that the error is less than this when // // n.Norm >= 8 * Math.Sqrt(3) / (3.84 - 0.5 - Math.Sqrt(3)) * S2Constants.DoubleEpsilon // = 1.91346e-15 (about 8.618 * S2Constants.DoubleEpsilon) var n_norm = n.Norm(); if (n_norm < 1.91346e-15) { // A and B are either nearly identical or nearly antipodal (to within // 4.309 * S2Constants.DoubleEpsilon, or about 6 nanometers on the earth's surface). if (a_.DotProd(b) < 0) { // The two points are nearly antipodal. The easiest solution is to // assume that the edge between A and B could go in any direction // around the sphere. bound_ = S2LatLngRect.Full; } else { // The two points are nearly identical (to within 4.309 * S2Constants.DoubleEpsilon). // In this case we can just use the bounding rectangle of the points, // since after the expansion done by GetBound() this rectangle is // guaranteed to include the (lat,lng) values of all points along AB. bound_ = bound_.Union(S2LatLngRect.FromPointPair(a_latlng_, b_latlng)); } } else { // Compute the longitude range spanned by AB. var lng_ab = S1Interval.FromPointPair(a_latlng_.LngRadians, b_latlng.LngRadians); if (lng_ab.GetLength() >= Math.PI - 2 * S2.DoubleEpsilon) { // The points lie on nearly opposite lines of longitude to within the // maximum error of the calculation. (Note that this test relies on // the fact that Math.PI is slightly less than the true value of Pi, and // that representable values near Math.PI are 2 * S2Constants.DoubleEpsilon apart.) // The easiest solution is to assume that AB could go on either side // of the pole. lng_ab = S1Interval.Full; } // Next we compute the latitude range spanned by the edge AB. We start // with the range spanning the two endpoints of the edge: var lat_ab = R1Interval.FromPointPair(a_latlng_.LatRadians, b_latlng.LatRadians); // This is the desired range unless the edge AB crosses the plane // through N and the Z-axis (which is where the great circle through A // and B attains its minimum and maximum latitudes). To test whether AB // crosses this plane, we compute a vector M perpendicular to this // plane and then project A and B onto it. var m = n.CrossProd(new S2Point(0, 0, 1)); var m_a = m.DotProd(a_); var m_b = m.DotProd(b); // We want to test the signs of "m_a" and "m_b", so we need to bound // the error in these calculations. It is possible to show that the // total error is bounded by // // (1 + Math.Sqrt(3)) * S2Constants.DoubleEpsilon * n_norm + 8 * Math.Sqrt(3) * (S2Constants.DoubleEpsilon**2) // = 6.06638e-16 * n_norm + 6.83174e-31 double m_error = 6.06638e-16 * n_norm + 6.83174e-31; if (m_a * m_b < 0 || Math.Abs(m_a) <= m_error || Math.Abs(m_b) <= m_error) { // Minimum/maximum latitude *may* occur in the edge interior. // // The maximum latitude is 90 degrees minus the latitude of N. We // compute this directly using atan2 in order to get maximum accuracy // near the poles. // // Our goal is compute a bound that contains the computed latitudes of // all S2Points P that pass the point-in-polygon containment test. // There are three sources of error we need to consider: // - the directional error in N (at most 3.84 * S2Constants.DoubleEpsilon) // - converting N to a maximum latitude // - computing the latitude of the test point P // The latter two sources of error are at most 0.955 * S2Constants.DoubleEpsilon // individually, but it is possible to show by a more complex analysis // that together they can add up to at most 1.16 * S2Constants.DoubleEpsilon, for a // total error of 5 * S2Constants.DoubleEpsilon. // // We add 3 * S2Constants.DoubleEpsilon to the bound here, and GetBound() will pad // the bound by another 2 * S2Constants.DoubleEpsilon. var max_lat = Math.Min( Math.Atan2(Math.Sqrt(n[0] * n[0] + n[1] * n[1]), Math.Abs(n[2])) + 3 * S2.DoubleEpsilon, S2.M_PI_2); // In order to get tight bounds when the two points are close together, // we also bound the min/max latitude relative to the latitudes of the // endpoints A and B. First we compute the distance between A and B, // and then we compute the maximum change in latitude between any two // points along the great circle that are separated by this distance. // This gives us a latitude change "budget". Some of this budget must // be spent getting from A to B; the remainder bounds the round-trip // distance (in latitude) from A or B to the min or max latitude // attained along the edge AB. // // There is a maximum relative error of 4.5 * DBL_EPSILON in computing // the squared distance (a_ - b), which means a maximum error of (4.5 // / 2 + 0.5) == 2.75 * DBL_EPSILON in computing Norm(). The sin() // and multiply each have a relative error of 0.5 * DBL_EPSILON which // we round up to a total of 4 * DBL_EPSILON. var lat_budget_z = 0.5 * (a_ - b).Norm() * Math.Sin(max_lat); const double folded = (1 + 4 * S2.DoubleEpsilon); var lat_budget = 2 * Math.Asin(Math.Min(folded * lat_budget_z, 1.0)); var max_delta = 0.5 * (lat_budget - lat_ab.GetLength()) + S2.DoubleEpsilon; // Test whether AB passes through the point of maximum latitude or // minimum latitude. If the dot product(s) are small enough then the // result may be ambiguous. if (m_a <= m_error && m_b >= -m_error) { lat_ab = new R1Interval(lat_ab.Lo, Math.Min(max_lat, lat_ab.Hi + max_delta)); } if (m_b <= m_error && m_a >= -m_error) { lat_ab = new R1Interval(Math.Max(-max_lat, lat_ab.Lo - max_delta), lat_ab.Lo); } } bound_ = bound_.Union(new S2LatLngRect(lat_ab, lng_ab)); } } a_ = b; a_latlng_ = b_latlng; }