void TestProjectUnproject(Projection projection, R2Point px, S2Point x) { // The arguments are chosen such that projection is exact, but // unprojection may not be. Assert.Equal(px, projection.Project(x)); Assert.True(S2.ApproxEquals(x, projection.Unproject(px))); }
public void Test_R2Rect_SimplePredicates() { // GetCenter(), GetVertex(), Contains(R2Point), InteriorContains(R2Point). R2Point sw1 = new(0, 0.25); R2Point ne1 = new(0.5, 0.75); R2Rect r1 = new(sw1, ne1); Assert.Equal(new R2Point(0.25, 0.5), r1.GetCenter()); Assert.Equal(new R2Point(0, 0.25), r1.GetVertex(0)); Assert.Equal(new R2Point(0.5, 0.25), r1.GetVertex(1)); Assert.Equal(new R2Point(0.5, 0.75), r1.GetVertex(2)); Assert.Equal(new R2Point(0, 0.75), r1.GetVertex(3)); Assert.True(r1.Contains(new R2Point(0.2, 0.4))); Assert.False(r1.Contains(new R2Point(0.2, 0.8))); Assert.False(r1.Contains(new R2Point(-0.1, 0.4))); Assert.False(r1.Contains(new R2Point(0.6, 0.1))); Assert.True(r1.Contains(sw1)); Assert.True(r1.Contains(ne1)); Assert.False(r1.InteriorContains(sw1)); Assert.False(r1.InteriorContains(ne1)); // Make sure that GetVertex() returns vertices in CCW order. for (int k = 0; k < 4; ++k) { R2Point a = r1.GetVertex(k - 1); R2Point b = r1.GetVertex(k); R2Point c = r1.GetVertex(k + 1); Assert.True((b - a).GetOrtho().DotProd(c - a) > 0); } }
private static void TestIntervalOps(S1Interval x, S1Interval y, string expected_relation, S1Interval expected_union, S1Interval expected_intersection) { // Test all of the interval operations on the given pair of intervals. // "expected_relation" is a sequence of "T" and "F" characters corresponding // to the expected results of Contains(), InteriorContains(), Intersects(), // and InteriorIntersects() respectively. Assert.Equal(x.Contains(y), expected_relation[0] == 'T'); Assert.Equal(x.InteriorContains(y), expected_relation[1] == 'T'); Assert.Equal(x.Intersects(y), expected_relation[2] == 'T'); Assert.Equal(x.InteriorIntersects(y), expected_relation[3] == 'T'); // bounds() returns a reference to a member variable, so we need to // make a copy when invoking it on a temporary object. Assert.Equal(R2Point.FromCoords(x.Union(y).Bounds()).Bounds(), expected_union.Bounds()); Assert.Equal(R2Point.FromCoords(x.Intersection(y).Bounds()).Bounds(), expected_intersection.Bounds()); Assert.Equal(x.Contains(y), x.Union(y) == x); Assert.Equal(x.Intersects(y), !x.Intersection(y).IsEmpty()); if (y.Lo == y.Hi) { var r = S1Interval.AddPoint(x, y.Lo); Assert.Equal(r.Bounds(), expected_union.Bounds()); } }
// Given a point P representing a possibly clipped endpoint A of an edge AB, // verify that "clip" contains P, and that if clipping occurred (i.e., P != A) // then P is on the boundary of "clip". private static void CheckPointOnBoundary(R2Point p, R2Point a, R2Rect clip) { Assert.True(clip.Contains(p)); if (p != a) { Assert.False(clip.Contains(new R2Point(MathUtils.NextAfter(p[0], a[0]), MathUtils.NextAfter(p[1], a[1])))); } }
public override S2LatLng ToLatLng(R2Point p) { // This formula is more accurate near zero than the atan(exp()) version. double x = to_radians_ * Math.IEEERemainder(p.X, x_wrap_); double k = Math.Exp(2 * to_radians_ * p.Y); double y = double.IsInfinity(k) ? S2.M_PI_2 : Math.Asin((k - 1) / (k + 1)); return(S2LatLng.FromRadians(y, x)); }
// Given an edge AB and a rectangle "clip", verify that IntersectsRect(), // ClipEdge(), and ClipEdgeBound() produce consistent results. private static void TestClipEdge(R2Point a, R2Point b, R2Rect clip) { // A bound for the error in edge clipping plus the error in the // IntersectsRect calculation below. double kError = kEdgeClipErrorUVDist + kIntersectsRectErrorUVDist; if (!ClipEdge(a, b, clip, out var a_clipped, out var b_clipped)) { Assert.False(IntersectsRect(a, b, clip.Expanded(-kError))); }
// Given a point X on the line AB (which is checked), return the fraction "t" // such that x = (1-t)*a + t*b. Return 0 if A = B. private static double GetFraction(R2Point x, R2Point a, R2Point b) { // A bound for the error in edge clipping plus the error in the calculation // below (which is similar to IntersectsRect). double kError = kEdgeClipErrorUVDist + kIntersectsRectErrorUVDist; if (a == b) { return(0.0); } R2Point dir = R2Point.Normalize(b - a); Assert.True(Math.Abs((x - a).DotProd(dir.GetOrtho())) <= kError); return((x - a).DotProd(dir)); }
// Given a geodesic edge AB, split the edge as necessary and append all // projected vertices except the first to "vertices". // // The maximum recursion depth is (Math.PI / kMinTolerance()) < 45, and the // frame size is small so stack overflow should not be an issue. private void AppendProjected(R2Point pa, S2Point a, R2Point pb, S2Point b, List <R2Point> vertices) { R2Point pbTmp = proj_.WrapDestination(pa, pb); if (EstimateMaxError(pa, a, pbTmp, b) <= scaled_tolerance_) { vertices.Add(pbTmp); } else { var mid = (a + b).Normalize(); var pmid = proj_.WrapDestination(pa, proj_.Project(mid)); AppendProjected(pa, a, pmid, mid, vertices); AppendProjected(pmid, mid, pbTmp, b, vertices); } }
// Helper function that wraps the coordinates of B if necessary in order to // obtain the shortest edge AB. For example, suppose that A = [170, 20], // B = [-170, 20], and the projection wraps so that [x, y] == [x + 360, y]. // Then this function would return [190, 20] for point B (reducing the edge // length in the "x" direction from 340 to 20). public R2Point WrapDestination(R2Point a, R2Point b) { R2Point wrap = WrapDistance(); double x = b.X, y = b.Y; // The code below ensures that "b" is unmodified unless wrapping is required. if (wrap.X > 0 && Math.Abs(x - a.X) > 0.5 * wrap.X) { x = a.X + Math.IEEERemainder(x - a.X, wrap.X); } if (wrap.Y > 0 && Math.Abs(y - a.Y) > 0.5 * wrap.Y) { y = a.Y + Math.IEEERemainder(y - a.Y, wrap.Y); } return(new R2Point(x, y)); }
// Like AppendProjected, but interpolates a projected edge and appends the // corresponding points on the sphere. private void AppendUnprojected(R2Point pa, S2Point a, R2Point pb, S2Point b, List <S2Point> vertices) { // See notes above regarding measuring the interpolation error. var pbTmp = proj_.WrapDestination(pa, pb); if (EstimateMaxError(pa, a, pbTmp, b) <= scaled_tolerance_) { vertices.Add(b); } else { R2Point pmid = Projection.Interpolate(0.5, pa, pbTmp); S2Point mid = proj_.Unproject(pmid); AppendUnprojected(pa, a, pmid, mid, vertices); AppendUnprojected(pmid, mid, pbTmp, b, vertices); } }
// Choose a random point in the rectangle defined by points A and B, sometimes // returning a point on the edge AB or the points A and B themselves. private static R2Point ChooseRectPoint(R2Point a, R2Point b) { if (S2Testing.Random.OneIn(5)) { return(S2Testing.Random.OneIn(2) ? a : b); } else if (S2Testing.Random.OneIn(3)) { return(a + S2Testing.Random.RandDouble() * (b - a)); } else { // a[i] may be >, <, or == b[i], so we write it like this instead // of using UniformDouble. return(new R2Point(a[0] + S2Testing.Random.RandDouble() * (b[0] - a[0]), a[1] + S2Testing.Random.RandDouble() * (b[1] - a[1]))); } }
public void Test_PlateCarreeProjection_Interpolate() { PlateCarreeProjection proj = new(180); // Test that coordinates and/or arguments are not accidentally reversed. Assert.Equal(new R2Point(1.5, 6), Projection.Interpolate(0.25, new R2Point(1, 5), new R2Point(3, 9))); // Test extrapolation. Assert.Equal(new R2Point(-3, 0), Projection.Interpolate(-2, new R2Point(1, 0), new R2Point(3, 0))); // Check that interpolation is exact at both endpoints. var a = new R2Point(1.234, -5.456e-20); var b = new R2Point(2.1234e-20, 7.456); Assert.Equal(a, Projection.Interpolate(0, a, b)); Assert.Equal(b, Projection.Interpolate(1, a, b)); }
public void Test_S2PaddedCell_ShrinkToFit() { const int kIters = 1000; for (int iter = 0; iter < kIters; ++iter) { // Start with the desired result and work backwards. S2CellId result = S2Testing.GetRandomCellId(); R2Rect result_uv = result.BoundUV(); R2Point size_uv = result_uv.GetSize(); // Find the biggest rectangle that fits in "result" after padding. // (These calculations ignore numerical errors.) double max_padding = 0.5 * Math.Min(size_uv[0], size_uv[1]); double padding = max_padding * S2Testing.Random.RandDouble(); R2Rect max_rect = result_uv.Expanded(-padding); // Start with a random subset of the maximum rectangle. R2Point a = new(SampleInterval(max_rect[0]), SampleInterval(max_rect[1])); R2Point b = new(SampleInterval(max_rect[0]), SampleInterval(max_rect[1])); if (!result.IsLeaf()) { // If the result is not a leaf cell, we must ensure that no child of // "result" also satisfies the conditions of ShrinkToFit(). We do this // by ensuring that "rect" intersects at least two children of "result" // (after padding). int axis = S2Testing.Random.Uniform(2); double center = result.CenterUV()[axis]; // Find the range of coordinates that are shared between child cells // along that axis. R1Interval shared = new(center - padding, center + padding); double mid = SampleInterval(shared.Intersection(max_rect[axis])); a = a.SetAxis(axis, SampleInterval(new R1Interval(max_rect[axis].Lo, mid))); b = b.SetAxis(axis, SampleInterval(new R1Interval(mid, max_rect[axis].Hi))); } R2Rect rect = R2Rect.FromPointPair(a, b); // Choose an arbitrary ancestor as the S2PaddedCell. S2CellId initial_id = result.Parent(S2Testing.Random.Uniform(result.Level() + 1)); Assert.Equal(result, new S2PaddedCell(initial_id, padding).ShrinkToFit(rect)); } }
private S1ChordAngle EstimateMaxError(R2Point pa, S2Point a, R2Point pb, S2Point b) { // See the algorithm description at the top of this file. // We always tessellate edges longer than 90 degrees on the sphere, since the // approximation below is not robust enough to handle such edges. if (a.DotProd(b) < -1e-14) { return(S1ChordAngle.Infinity); } const double t1 = kInterpolationFraction; const double t2 = 1 - kInterpolationFraction; S2Point mid1 = S2.Interpolate(a, b, t1); S2Point mid2 = S2.Interpolate(a, b, t2); S2Point pmid1 = proj_.Unproject(Projection.Interpolate(t1, pa, pb)); S2Point pmid2 = proj_.Unproject(Projection.Interpolate(t2, pa, pb)); return(S1ChordAngle.Max(new S1ChordAngle(mid1, pmid1), new S1ChordAngle(mid2, pmid2))); }
// Converts the planar edge AB in the given projection to a chain of // spherical geodesic edges and appends the vertices to "vertices". // // This method can be called multiple times with the same output vector to // convert an entire polyline or loop. All vertices of the first edge are // appended, but the first vertex of each subsequent edge is omitted (and is // required to match that last vertex of the previous edge). // // Note that to construct an S2Loop, you must call vertices.pop_back() at // the very end to eliminate the duplicate first and last vertex. Note also // that if the given projection involves coordinate "wrapping" (e.g. across // the 180 degree meridian) then the first and last vertices may not be // exactly the same. public void AppendUnprojected(R2Point a, R2Point b, List <S2Point> vertices) { var pointA = proj_.Unproject(a); var pointB = proj_.Unproject(b); if (!vertices.Any()) { vertices.Add(pointA); } else { // Note that coordinate wrapping can create a small amount of error. For // example in the edge chain "0:-175, 0:179, 0:-177", the first edge is // transformed into "0:-175, 0:-181" while the second is transformed into // "0:179, 0:183". The two coordinate pairs for the middle vertex // ("0:-181" and "0:179") may not yield exactly the same S2Point. System.Diagnostics.Debug.Assert(S2.ApproxEquals(vertices.Last(), pointA)); // Appended edges must form a chain } AppendUnprojected(a, pointA, b, pointB, vertices); }
// Returns the point obtained by interpolating the given fraction of the // distance along the line from A to B. Almost all projections should // use the default implementation of this method, which simply interpolates // linearly in R2 space. Fractions < 0 or > 1 result in extrapolation // instead. // // The only reason to override this method is if you want edges to be // defined as something other than straight lines in the 2D projected // coordinate system. For example, using a third-party library such as // GeographicLib you could define edges as geodesics over an ellipsoid model // of the Earth. (Note that very few data sets define edges this way.) // // Also note that there is no reason to define a projection where edges are // geodesics over the sphere, because this is the native S2 interpretation. public static R2Point Interpolate(double f, R2Point a, R2Point b) { // Default implementation, suitable for any projection where edges are defined // as straight lines in the 2D projected space. return((1.0 - f) * a + f * b); }
// Convenience function equivalent to S2LatLng(Unproject(p)), but the // implementation may be more efficient. public abstract S2LatLng ToLatLng(R2Point p);
public override S2LatLng ToLatLng(R2Point p) { var rem = Math.IEEERemainder(p.X, x_wrap_); return(S2LatLng.FromRadians(to_radians_ * p.Y, to_radians_ * rem)); }
// Converts a projected 2D point to a point on the sphere. // // If wrapping is defined for a given axis (see below), then this method // should accept any real number for the corresponding coordinate. public abstract S2Point Unproject(R2Point p);
public override S2Point Unproject(R2Point p) { return(ToLatLng(p).ToPoint()); }