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) } } }
// 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)}) }
// 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}) }