// RectBound returns a bounding latitude-longitude rectangle. // The bounds are not guaranteed to be tight. func (c Cap) RectBound() Rect { if c.IsEmpty() { return EmptyRect() } capAngle := c.Radius().Radians() allLongitudes := false lat := r1.Interval{ Lo: latitude(c.center).Radians() - capAngle, Hi: latitude(c.center).Radians() + capAngle, } lng := s1.FullInterval() // Check whether cap includes the south pole. if lat.Lo <= -math.Pi/2 { lat.Lo = -math.Pi / 2 allLongitudes = true } // Check whether cap includes the north pole. if lat.Hi >= math.Pi/2 { lat.Hi = math.Pi / 2 allLongitudes = true } if !allLongitudes { // Compute the range of longitudes covered by the cap. We use the law // of sines for spherical triangles. Consider the triangle ABC where // A is the north pole, B is the center of the cap, and C is the point // of tangency between the cap boundary and a line of longitude. Then // C is a right angle, and letting a,b,c denote the sides opposite A,B,C, // we have sin(a)/sin(A) = sin(c)/sin(C), or sin(A) = sin(a)/sin(c). // Here "a" is the cap angle, and "c" is the colatitude (90 degrees // minus the latitude). This formula also works for negative latitudes. // // The formula for sin(a) follows from the relationship h = 1 - cos(a). sinA := math.Sqrt(c.height * (2 - c.height)) sinC := math.Cos(latitude(c.center).Radians()) if sinA <= sinC { angleA := math.Asin(sinA / sinC) lng.Lo = math.Remainder(longitude(c.center).Radians()-angleA, math.Pi*2) lng.Hi = math.Remainder(longitude(c.center).Radians()+angleA, math.Pi*2) } } return Rect{lat, lng} }
// initBound sets up the approximate bounding Rects for this loop. func (l *Loop) initBound() { // Check for the special "empty" and "full" loops. if l.isEmptyOrFull() { if l.IsEmpty() { l.bound = EmptyRect() } else { l.bound = FullRect() } l.subregionBound = l.bound return } // The bounding rectangle of a loop is not necessarily the same as the // bounding rectangle of its vertices. First, the maximal latitude may be // attained along the interior of an edge. Second, the loop may wrap // entirely around the sphere (e.g. a loop that defines two revolutions of a // candy-cane stripe). Third, the loop may include one or both poles. // Note that a small clockwise loop near the equator contains both poles. bounder := NewRectBounder() for _, p := range l.vertices { bounder.AddPoint(p) } bounder.AddPoint(l.vertices[0]) b := bounder.RectBound() if l.ContainsPoint(Point{r3.Vector{0, 0, 1}}) { b = Rect{r1.Interval{b.Lat.Lo, math.Pi / 2}, s1.FullInterval()} } // If a loop contains the south pole, then either it wraps entirely // around the sphere (full longitude range), or it also contains the // north pole in which case b.Lng.IsFull() due to the test above. // Either way, we only need to do the south pole containment test if // b.Lng.IsFull(). if b.Lng.IsFull() && l.ContainsPoint(Point{r3.Vector{0, 0, -1}}) { b.Lat.Lo = -math.Pi / 2 } l.bound = b l.subregionBound = ExpandForSubregions(l.bound) }
func TestExpandForSubregions(t *testing.T) { // Test the full and empty bounds. if !ExpandForSubregions(FullRect()).IsFull() { t.Errorf("Subregion Bound of full rect should be full") } if !ExpandForSubregions(EmptyRect()).IsEmpty() { t.Errorf("Subregion Bound of empty rect should be empty") } tests := []struct { xLat, xLng, yLat, yLng float64 wantFull bool }{ // Cases where the bound does not straddle the equator (but almost does), // and spans nearly 180 degrees in longitude. {3e-16, 0, 1e-14, math.Pi, true}, {9e-16, 0, 1e-14, math.Pi, false}, {1e-16, 7e-16, 1e-14, math.Pi, true}, {3e-16, 14e-16, 1e-14, math.Pi, false}, {1e-100, 14e-16, 1e-14, math.Pi, true}, {1e-100, 22e-16, 1e-14, math.Pi, false}, // Cases where the bound spans at most 90 degrees in longitude, and almost // 180 degrees in latitude. Note that DBL_EPSILON is about 2.22e-16, which // implies that the double-precision value just below Pi/2 can be written as // (math.Pi/2 - 2e-16). {-math.Pi / 2, -1e-15, math.Pi/2 - 7e-16, 0, true}, {-math.Pi / 2, -1e-15, math.Pi/2 - 30e-16, 0, false}, {-math.Pi/2 + 4e-16, 0, math.Pi/2 - 2e-16, 1e-7, true}, {-math.Pi/2 + 30e-16, 0, math.Pi / 2, 1e-7, false}, {-math.Pi/2 + 4e-16, 0, math.Pi/2 - 4e-16, math.Pi / 2, true}, {-math.Pi / 2, 0, math.Pi/2 - 30e-16, math.Pi / 2, false}, // Cases where the bound straddles the equator and spans more than 90 // degrees in longitude. These are the cases where the critical distance is // between a corner of the bound and the opposite longitudinal edge. Unlike // the cases above, here the bound may contain nearly-antipodal points (to // within 3.055 * DBL_EPSILON) even though the latitude and longitude ranges // are both significantly less than (math.Pi - 3.055 * DBL_EPSILON). {-math.Pi / 2, 0, math.Pi/2 - 1e-8, math.Pi - 1e-7, true}, {-math.Pi / 2, 0, math.Pi/2 - 1e-7, math.Pi - 1e-7, false}, {-math.Pi/2 + 1e-12, -math.Pi + 1e-4, math.Pi / 2, 0, true}, {-math.Pi/2 + 1e-11, -math.Pi + 1e-4, math.Pi / 2, 0, true}, } for _, tc := range tests { in := RectFromLatLng(LatLng{s1.Angle(tc.xLat), s1.Angle(tc.xLng)}) in = in.AddPoint(LatLng{s1.Angle(tc.yLat), s1.Angle(tc.yLng)}) got := ExpandForSubregions(in) // Test that the bound is actually expanded. if !got.Contains(in) { t.Errorf("Subregion bound of (%f, %f, %f, %f) should contain original rect", tc.xLat, tc.xLng, tc.yLat, tc.yLng) } if in.Lat == validRectLatRange && in.Lat.ContainsInterval(got.Lat) { t.Errorf("Subregion bound of (%f, %f, %f, %f) shouldn't be contained by original rect", tc.xLat, tc.xLng, tc.yLat, tc.yLng) } // We check the various situations where the bound contains nearly-antipodal points. The tests are organized into pairs // where the two bounds are similar except that the first bound meets the nearly-antipodal criteria while the second does not. if got.IsFull() != tc.wantFull { t.Errorf("Subregion Bound of (%f, %f, %f, %f).IsFull should be %t", tc.xLat, tc.xLng, tc.yLat, tc.yLng, tc.wantFull) } } rectTests := []struct { xLat, xLng, yLat, yLng float64 wantRect Rect }{ {1.5, -math.Pi / 2, 1.5, math.Pi/2 - 2e-16, Rect{r1.Interval{1.5, 1.5}, s1.FullInterval()}}, {1.5, -math.Pi / 2, 1.5, math.Pi/2 - 7e-16, Rect{r1.Interval{1.5, 1.5}, s1.Interval{-math.Pi / 2, math.Pi/2 - 7e-16}}}, // Check for cases where the bound is expanded to include one of the poles {-math.Pi/2 + 1e-15, 0, -math.Pi/2 + 1e-15, 0, Rect{r1.Interval{-math.Pi / 2, -math.Pi/2 + 1e-15}, s1.FullInterval()}}, {math.Pi/2 - 1e-15, 0, math.Pi/2 - 1e-15, 0, Rect{r1.Interval{math.Pi/2 - 1e-15, math.Pi / 2}, s1.FullInterval()}}, } for _, tc := range rectTests { // Now we test cases where the bound does not contain nearly-antipodal // points, but it does contain points that are approximately 180 degrees // apart in latitude. in := RectFromLatLng(LatLng{s1.Angle(tc.xLat), s1.Angle(tc.xLng)}) in = in.AddPoint(LatLng{s1.Angle(tc.yLat), s1.Angle(tc.yLng)}) got := ExpandForSubregions(in) if !rectsApproxEqual(got, tc.wantRect, rectErrorLat, rectErrorLng) { t.Errorf("Subregion Bound of (%f, %f, %f, %f) = (%v) should be %v", tc.xLat, tc.xLng, tc.yLat, tc.yLng, got, tc.wantRect) } } }
// 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.EmptyInterval().AddPoint(c.longitude(i, 1-j)).AddPoint(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)}) }
// PolarClosure returns the rectangle unmodified if it does not include either pole. // If it includes either pole, PolarClosure returns an expansion of the rectangle along // the longitudinal range to include all possible representations of the contained poles. func (r Rect) PolarClosure() Rect { if r.Lat.Lo == -math.Pi/2 || r.Lat.Hi == math.Pi/2 { return Rect{r.Lat, s1.FullInterval()} } return r }
"fmt" "math" "github.com/golang/geo/r1" "github.com/golang/geo/s1" ) // Rect represents a closed latitude-longitude rectangle. type Rect struct { Lat r1.Interval Lng s1.Interval } var ( validRectLatRange = r1.Interval{-math.Pi / 2, math.Pi / 2} validRectLngRange = s1.FullInterval() ) // EmptyRect returns the empty rectangle. func EmptyRect() Rect { return Rect{r1.EmptyInterval(), s1.EmptyInterval()} } // FullRect returns the full rectangle. func FullRect() Rect { return Rect{validRectLatRange, validRectLngRange} } // RectFromLatLng constructs a rectangle containing a single point p. func RectFromLatLng(p LatLng) Rect { return Rect{ Lat: r1.Interval{p.Lat.Radians(), p.Lat.Radians()}, Lng: s1.Interval{p.Lng.Radians(), p.Lng.Radians()}, } }
// AddPoint adds the given point to the chain. The Point must be unit length. func (r *RectBounder) AddPoint(b Point) { bLL := LatLngFromPoint(b) if r.bound.IsEmpty() { r.a = b r.aLL = bLL r.bound = r.bound.AddPoint(bLL) return } // 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 RobustSign // 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. n := r.a.Sub(b.Vector).Cross(r.a.Add(b.Vector)) // 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 * dblEpsilon, and it is desirable to // have the total error be a multiple of dblEpsilon, we have chosen to // limit the maximum error in the normal to be 3.84 * dblEpsilon. // It is possible to show that the error is less than this when // // n.Norm() >= 8 * sqrt(3) / (3.84 - 0.5 - sqrt(3)) * dblEpsilon // = 1.91346e-15 (about 8.618 * dblEpsilon) nNorm := n.Norm() if nNorm < 1.91346e-15 { // A and B are either nearly identical or nearly antipodal (to within // 4.309 * dblEpsilon, or about 6 nanometers on the earth's surface). if r.a.Dot(b.Vector) < 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. r.bound = FullRect() } else { // The two points are nearly identical (to within 4.309 * dblEpsilon). // In this case we can just use the bounding rectangle of the points, // since after the expansion done by GetBound this Rect is // guaranteed to include the (lat,lng) values of all points along AB. r.bound = r.bound.Union(RectFromLatLng(r.aLL).AddPoint(bLL)) } r.a = b r.aLL = bLL return } // Compute the longitude range spanned by AB. lngAB := s1.EmptyInterval().AddPoint(r.aLL.Lng.Radians()).AddPoint(bLL.Lng.Radians()) if lngAB.Length() >= math.Pi-2*dblEpsilon { // The points lie on nearly opposite lines of longitude to within the // maximum error of the calculation. The easiest solution is to assume // that AB could go on either side of the pole. lngAB = s1.FullInterval() } // Next we compute the latitude range spanned by the edge AB. We start // with the range spanning the two endpoints of the edge: latAB := r1.IntervalFromPoint(r.aLL.Lat.Radians()).AddPoint(bLL.Lat.Radians()) // 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. m := n.Cross(PointFromCoords(0, 0, 1).Vector) mA := m.Dot(r.a.Vector) mB := m.Dot(b.Vector) // We want to test the signs of "mA" and "mB", so we need to bound // the error in these calculations. It is possible to show that the // total error is bounded by // // (1 + sqrt(3)) * dblEpsilon * nNorm + 8 * sqrt(3) * (dblEpsilon**2) // = 6.06638e-16 * nNorm + 6.83174e-31 mError := 6.06638e-16*nNorm + 6.83174e-31 if mA*mB < 0 || math.Abs(mA) <= mError || math.Abs(mB) <= mError { // 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 * dblEpsilon) // - 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 * dblEpsilon // individually, but it is possible to show by a more complex analysis // that together they can add up to at most 1.16 * dblEpsilon, for a // total error of 5 * dblEpsilon. // // We add 3 * dblEpsilon to the bound here, and GetBound() will pad // the bound by another 2 * dblEpsilon. maxLat := math.Min( math.Atan2(math.Sqrt(n.X*n.X+n.Y*n.Y), math.Abs(n.Z))+3*dblEpsilon, math.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. latBudget := 2 * math.Asin(0.5*(r.a.Sub(b.Vector)).Norm()*math.Sin(maxLat)) maxDelta := 0.5*(latBudget-latAB.Length()) + dblEpsilon // 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 mA <= mError && mB >= -mError { latAB.Hi = math.Min(maxLat, latAB.Hi+maxDelta) } if mB <= mError && mA >= -mError { latAB.Lo = math.Max(-maxLat, latAB.Lo-maxDelta) } } r.a = b r.aLL = bLL r.bound = r.bound.Union(Rect{latAB, lngAB}) }