func TestRectBounderMaxLatitudeSimple(t *testing.T) { cubeLat := math.Asin(1 / math.Sqrt(3)) // 35.26 degrees cubeLatRect := Rect{r1.IntervalFromPoint(-cubeLat).AddPoint(cubeLat), s1.IntervalFromEndpoints(-math.Pi/4, math.Pi/4)} tests := []struct { a, b Point want Rect }{ // Check cases where the min/max latitude is attained at a vertex. { a: PointFromCoords(1, 1, 1), b: PointFromCoords(1, -1, -1), want: cubeLatRect, }, { a: PointFromCoords(1, -1, 1), b: PointFromCoords(1, 1, -1), want: cubeLatRect, }, } for _, test := range tests { if got := rectBoundForPoints(test.a, test.b); !rectsApproxEqual(got, test.want, rectErrorLat, rectErrorLng) { t.Errorf("RectBounder for points (%v, %v) near max lat failed: got %v, want %v", test.a, test.b, got, test.want) } } }
// intersectsLatEdge reports if the edge AB intersects the given edge of constant // latitude. Requires the points to have unit length. func intersectsLatEdge(a, b Point, lat s1.Angle, lng s1.Interval) bool { // Unfortunately, lines of constant latitude are curves on // the sphere. They can intersect a straight edge in 0, 1, or 2 points. // First, compute the normal to the plane AB that points vaguely north. z := a.PointCross(b) if z.Z < 0 { z = Point{z.Mul(-1)} } // Extend this to an orthonormal frame (x,y,z) where x is the direction // where the great circle through AB achieves its maximium latitude. y := z.PointCross(PointFromCoords(0, 0, 1)) x := y.Cross(z.Vector) // Compute the angle "theta" from the x-axis (in the x-y plane defined // above) where the great circle intersects the given line of latitude. sinLat := math.Sin(float64(lat)) if math.Abs(sinLat) >= x.Z { // The great circle does not reach the given latitude. return false } cosTheta := sinLat / x.Z sinTheta := math.Sqrt(1 - cosTheta*cosTheta) theta := math.Atan2(sinTheta, cosTheta) // The candidate intersection points are located +/- theta in the x-y // plane. For an intersection to be valid, we need to check that the // intersection point is contained in the interior of the edge AB and // also that it is contained within the given longitude interval "lng". // Compute the range of theta values spanned by the edge AB. abTheta := s1.IntervalFromEndpoints( math.Atan2(a.Dot(y.Vector), a.Dot(x)), math.Atan2(b.Dot(y.Vector), b.Dot(x))) if abTheta.Contains(theta) { // Check if the intersection point is also in the given lng interval. isect := x.Mul(cosTheta).Add(y.Mul(sinTheta)) if lng.Contains(math.Atan2(isect.Y, isect.X)) { return true } } if abTheta.Contains(-theta) { // Check if the other intersection point is also in the given lng interval. isect := x.Mul(cosTheta).Sub(y.Mul(sinTheta)) if lng.Contains(math.Atan2(isect.Y, isect.X)) { return true } } return false }
func rectFromDegrees(latLo, lngLo, latHi, lngHi float64) Rect { // Convenience method to construct a rectangle. This method is // intentionally *not* in the S2LatLngRect interface because the // argument order is ambiguous, but is fine for the test. return Rect{ Lat: r1.Interval{ Lo: (s1.Angle(latLo) * s1.Degree).Radians(), Hi: (s1.Angle(latHi) * s1.Degree).Radians(), }, Lng: s1.IntervalFromEndpoints( (s1.Angle(lngLo) * s1.Degree).Radians(), (s1.Angle(lngHi) * s1.Degree).Radians(), ), } }
func TestRectVertex(t *testing.T) { r1 := Rect{r1.Interval{0, math.Pi / 2}, s1.IntervalFromEndpoints(-math.Pi, 0)} tests := []struct { r Rect i int want LatLng }{ {r1, 0, LatLng{0, math.Pi}}, {r1, 1, LatLng{0, 0}}, {r1, 2, LatLng{math.Pi / 2, 0}}, {r1, 3, LatLng{math.Pi / 2, math.Pi}}, } for _, test := range tests { if got := test.r.Vertex(test.i); got != test.want { t.Errorf("%v.Vertex(%d) = %v, want %v", test.r, test.i, got, test.want) } } }
// testClipToPaddedFace performs a comprehensive set of tests across all faces and // with random padding for the given points. // // 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. func testClipToPaddedFace(t *testing.T, a, b Point) { a = Point{a.Normalize()} b = Point{b.Normalize()} if a.Vector == b.Mul(-1) { return } norm := Point{a.PointCross(b).Normalize()} aTan := Point{norm.Cross(a.Vector)} padding := 0.0 if !oneIn(10) { padding = 1e-10 * math.Pow(1e-5, randomFloat64()) } xAxis := a yAxis := aTan // Given the points A and B, we expect all angles generated from the clipping // to fall within this range. expectedAngles := s1.Interval{0, float64(a.Angle(b.Vector))} if expectedAngles.IsInverted() { expectedAngles = s1.Interval{expectedAngles.Hi, expectedAngles.Lo} } maxAngles := expectedAngles.Expanded(faceClipErrorRadians) var actualAngles s1.Interval for face := 0; face < 6; face++ { aUV, bUV, intersects := ClipToPaddedFace(a, b, face, padding) if !intersects { continue } aClip := Point{faceUVToXYZ(face, aUV.X, aUV.Y).Normalize()} bClip := Point{faceUVToXYZ(face, bUV.X, bUV.Y).Normalize()} desc := fmt.Sprintf("on face %d, a=%v, b=%v, aClip=%v, bClip=%v,", face, a, b, aClip, bClip) if got := math.Abs(aClip.Dot(norm.Vector)); got > faceClipErrorRadians { t.Errorf("%s abs(%v.Dot(%v)) = %v, want <= %v", desc, aClip, norm, got, faceClipErrorRadians) } if got := math.Abs(bClip.Dot(norm.Vector)); got > faceClipErrorRadians { t.Errorf("%s abs(%v.Dot(%v)) = %v, want <= %v", desc, bClip, norm, got, faceClipErrorRadians) } if float64(aClip.Angle(a.Vector)) > faceClipErrorRadians { if got := math.Max(math.Abs(aUV.X), math.Abs(aUV.Y)); !float64Eq(got, 1+padding) { t.Errorf("%s the largest component of %v = %v, want %v", desc, aUV, got, 1+padding) } } if float64(bClip.Angle(b.Vector)) > faceClipErrorRadians { if got := math.Max(math.Abs(bUV.X), math.Abs(bUV.Y)); !float64Eq(got, 1+padding) { t.Errorf("%s the largest component of %v = %v, want %v", desc, bUV, got, 1+padding) } } aAngle := math.Atan2(aClip.Dot(yAxis.Vector), aClip.Dot(xAxis.Vector)) bAngle := math.Atan2(bClip.Dot(yAxis.Vector), bClip.Dot(xAxis.Vector)) // Rounding errors may cause bAngle to be slightly less than aAngle. // We handle this by constructing the interval with FromPointPair, // which is okay since the interval length is much less than math.Pi. faceAngles := s1.IntervalFromEndpoints(aAngle, bAngle) if faceAngles.IsInverted() { faceAngles = s1.Interval{faceAngles.Hi, faceAngles.Lo} } if !maxAngles.ContainsInterval(faceAngles) { t.Errorf("%s %v.ContainsInterval(%v) = false, but should have contained this interval", desc, maxAngles, faceAngles) } actualAngles = actualAngles.Union(faceAngles) } if !actualAngles.Expanded(faceClipErrorRadians).ContainsInterval(expectedAngles) { t.Errorf("the union of all angle segments should be larger than the expected angle") } }
// RectBound returns the bounding rectangle of this cell. func (c Cell) RectBound() Rect { if c.level > 0 { // Except for cells at level 0, the latitude and longitude extremes are // attained at the vertices. Furthermore, the latitude range is // determined by one pair of diagonally opposite vertices and the // longitude range is determined by the other pair. // // We first determine which corner (i,j) of the cell has the largest // absolute latitude. To maximize latitude, we want to find the point in // the cell that has the largest absolute z-coordinate and the smallest // absolute x- and y-coordinates. To do this we look at each coordinate // (u and v), and determine whether we want to minimize or maximize that // coordinate based on the axis direction and the cell's (u,v) quadrant. u := c.uv.X.Lo + c.uv.X.Hi v := c.uv.Y.Lo + c.uv.Y.Hi var i, j int if uAxis(int(c.face)).Z == 0 { if u < 0 { i = 1 } } else if u > 0 { i = 1 } if vAxis(int(c.face)).Z == 0 { if v < 0 { j = 1 } } else if v > 0 { j = 1 } lat := r1.IntervalFromPoint(c.latitude(i, j)).AddPoint(c.latitude(1-i, 1-j)) lng := s1.IntervalFromEndpoints(c.longitude(i, 1-j), c.longitude(1-i, j)) // We grow the bounds slightly to make sure that the bounding rectangle // contains LatLngFromPoint(P) for any point P inside the loop L defined by the // four *normalized* vertices. Note that normalization of a vector can // change its direction by up to 0.5 * dblEpsilon radians, and it is not // enough just to add Normalize calls to the code above because the // latitude/longitude ranges are not necessarily determined by diagonally // opposite vertex pairs after normalization. // // We would like to bound the amount by which the latitude/longitude of a // contained point P can exceed the bounds computed above. In the case of // longitude, the normalization error can change the direction of rounding // leading to a maximum difference in longitude of 2 * dblEpsilon. In // the case of latitude, the normalization error can shift the latitude by // up to 0.5 * dblEpsilon and the other sources of error can cause the // two latitudes to differ by up to another 1.5 * dblEpsilon, which also // leads to a maximum difference of 2 * dblEpsilon. return Rect{lat, lng}.expanded(LatLng{s1.Angle(2 * dblEpsilon), s1.Angle(2 * dblEpsilon)}).PolarClosure() } // The 4 cells around the equator extend to +/-45 degrees latitude at the // midpoints of their top and bottom edges. The two cells covering the // poles extend down to +/-35.26 degrees at their vertices. The maximum // error in this calculation is 0.5 * dblEpsilon. var bound Rect switch c.face { case 0: bound = Rect{r1.Interval{-math.Pi / 4, math.Pi / 4}, s1.Interval{-math.Pi / 4, math.Pi / 4}} case 1: bound = Rect{r1.Interval{-math.Pi / 4, math.Pi / 4}, s1.Interval{math.Pi / 4, 3 * math.Pi / 4}} case 2: bound = Rect{r1.Interval{poleMinLat, math.Pi / 2}, s1.FullInterval()} case 3: bound = Rect{r1.Interval{-math.Pi / 4, math.Pi / 4}, s1.Interval{3 * math.Pi / 4, -3 * math.Pi / 4}} case 4: bound = Rect{r1.Interval{-math.Pi / 4, math.Pi / 4}, s1.Interval{-3 * math.Pi / 4, -math.Pi / 4}} default: bound = Rect{r1.Interval{-math.Pi / 2, -poleMinLat}, s1.FullInterval()} } // Finally, we expand the bound to account for the error when a point P is // converted to an LatLng to test for containment. (The bound should be // large enough so that it contains the computed LatLng of any contained // point, not just the infinite-precision version.) We don't need to expand // longitude because longitude is calculated via a single call to math.Atan2, // which is guaranteed to be semi-monotonic. return bound.expanded(LatLng{s1.Angle(dblEpsilon), s1.Angle(0)}) }
// IntersectsCell reports whether this rectangle intersects the given cell. This is an // exact test and may be fairly expensive. func (r Rect) IntersectsCell(c Cell) bool { // First we eliminate the cases where one region completely contains the // other. Once these are disposed of, then the regions will intersect // if and only if their boundaries intersect. if r.IsEmpty() { return false } if r.ContainsPoint(Point{c.id.rawPoint()}) { return true } if c.ContainsPoint(PointFromLatLng(r.Center())) { return true } // Quick rejection test (not required for correctness). if !r.Intersects(c.RectBound()) { return false } // Precompute the cell vertices as points and latitude-longitudes. We also // check whether the Cell contains any corner of the rectangle, or // vice-versa, since the edge-crossing tests only check the edge interiors. vertices := [4]Point{} latlngs := [4]LatLng{} for i := range vertices { vertices[i] = c.Vertex(i) latlngs[i] = LatLngFromPoint(vertices[i]) if r.ContainsLatLng(latlngs[i]) { return true } if c.ContainsPoint(PointFromLatLng(r.Vertex(i))) { return true } } // Now check whether the boundaries intersect. Unfortunately, a // latitude-longitude rectangle does not have straight edges: two edges // are curved, and at least one of them is concave. for i := range vertices { edgeLng := s1.IntervalFromEndpoints(latlngs[i].Lng.Radians(), latlngs[(i+1)&3].Lng.Radians()) if !r.Lng.Intersects(edgeLng) { continue } a := vertices[i] b := vertices[(i+1)&3] if edgeLng.Contains(r.Lng.Lo) && intersectsLngEdge(a, b, r.Lat, s1.Angle(r.Lng.Lo)) { return true } if edgeLng.Contains(r.Lng.Hi) && intersectsLngEdge(a, b, r.Lat, s1.Angle(r.Lng.Hi)) { return true } if intersectsLatEdge(a, b, s1.Angle(r.Lat.Lo), r.Lng) { return true } if intersectsLatEdge(a, b, s1.Angle(r.Lat.Hi), r.Lng) { return true } } return false }