public void Test_S1IntervalTestBase_FromPointPair() { Assert.Equal(S1Interval.FromPointPair(-Math.PI, Math.PI), pi); Assert.Equal(S1Interval.FromPointPair(Math.PI, -Math.PI), pi); Assert.Equal(S1Interval.FromPointPair(mid34.Hi, mid34.Lo), mid34); Assert.Equal(S1Interval.FromPointPair(mid23.Lo, mid23.Hi), mid23); }
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*/); }
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)); }
// 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; }