func (l *Loop) initBound() { // The bounding rectangle of a loop is not necessarily the same as the // bounding rectangle of its vertices. First, the loop may wrap entirely // around the sphere (e.g. a loop that defines two revolutions of a // candy-cane stripe). Second, the loop may include one or both poles. // Note that a small clockwise loop near the equator contains both poles. bounder := NewRectBounder() for i := 0; i <= l.NumVertices(); i++ { bounder.AddPoint(l.Vertex(i)) } b := bounder.GetBound() // Note that we need to initialize bound with a temporary value since // contains() does a bounding rectangle check before doing anything else. l.bound = FullRect() if l.ContainsPoint(PointFromCoordsRaw(0, 0, 1)) { b = Rect{r1.IntervalFromPointPair(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. if b.Lng.IsFull() && l.ContainsPoint(PointFromCoordsRaw(0, 0, -1)) { b = Rect{r1.IntervalFromPointPair(-math.Pi/2, b.Lat.Hi), b.Lng} } l.bound = b }
func TestLoopBounds(t *testing.T) { if !(candyCane.RectBound().Lng.IsFull()) { t.Fatal("ttttt") } if !(s1.Angle(candyCane.RectBound().Lat.Lo).Degrees() < -20) { t.Fatal("") } if !(s1.Angle(candyCane.RectBound().Lat.Hi).Degrees() > 10) { t.Fatal("") } if !(smallNeCw.RectBound().IsFull()) { t.Fatal("") } if arctic80.RectBound() != RectFromLatLngLoHi(LatLngFromDegrees(80, -180), LatLngFromDegrees(90, 180)) { t.Fatal("") } if antarctic80.RectBound() != RectFromLatLngLoHi(LatLngFromDegrees(-90, -180), LatLngFromDegrees(-80, 180)) { t.Fatal("") } arctic80.Invert() // The highest latitude of each edge is attained at its midpoint. mid := Point{arctic80.Vertex(0).Add(arctic80.Vertex(1).Vector).Mul(0.5)} if math.Abs(s1.Angle(arctic80.RectBound().Lat.Hi).Radians()-LatLngFromPoint(mid).Lat.Radians()) > EPSILON { t.Fatal("") } arctic80.Invert() if !(southHemi.RectBound().Lng.IsFull()) { t.Fatal("") } if !(southHemi.RectBound().Lat == r1.IntervalFromPointPair(-math.Pi/2, 0)) { t.Fatal("") } }
// RectBound returns a bounding latitude-longitude rectangle that contains // the region. The bounds are not guaranteed to be tight. 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.IntervalFromPointPair(c.latitude(i, j), c.latitude(1-i, 1-j)) lat = lat.Expanded(MAX_ERROR).Intersection(validRectLatRange) if lat.Lo == validRectLatRange.Lo || lat.Hi == validRectLatRange.Hi { return Rect{lat, s1.FullInterval()} } lng := s1.IntervalFromPointPair(c.longitude(i, 1-j), c.longitude(1-i, j)) return Rect{lat, lng.Expanded(MAX_ERROR)} } switch c.face { case 0: return Rect{r1.Interval{-math.Pi / 4, math.Pi / 4}, s1.Interval{-math.Pi / 4, math.Pi / 4}} case 1: return Rect{r1.Interval{-math.Pi / 4, math.Pi / 4}, s1.Interval{math.Pi / 4, 3 * math.Pi / 4}} case 2: return Rect{r1.Interval{POLE_MIN_LAT, math.Pi / 2}, s1.Interval{-math.Pi, math.Pi}} case 3: return Rect{r1.Interval{-math.Pi / 4, math.Pi / 4}, s1.Interval{3 * math.Pi / 4, -3 * math.Pi / 4}} case 4: return Rect{r1.Interval{-math.Pi / 4, math.Pi / 4}, s1.Interval{-3 * math.Pi / 4, -math.Pi / 4}} default: return Rect{r1.Interval{-math.Pi / 2, -POLE_MIN_LAT}, s1.Interval{-math.Pi, math.Pi}} } }
func (rb *RectBounder) AddPoint(b Point) { bLatLng := LatLngFromPoint(b) if rb.bound.IsEmpty() { rb.bound = rb.bound.AddPoint(bLatLng) } else { // We can't just call bound.addPoint(bLatLng) here, since we need to // ensure that all the longitudes between "a" and "b" are included. rb.bound = rb.bound.Union(RectFromLatLngPointPair(rb.aLatLng, bLatLng)) // Check whether the min/max latitude occurs in the edge interior. // We find the normal to the plane containing AB, and then a vector // "dir" in this plane that also passes through the equator. We use // RobustCrossProd to ensure that the edge normal is accurate even // when the two points are very close together. aCrossB := rb.a.PointCross(b) dir := aCrossB.Cross(PointFromCoordsRaw(0, 0, 1).Vector) da := dir.Dot(rb.a.Vector) db := dir.Dot(b.Vector) if da*db < 0 { // Minimum/maximum latitude occurs in the edge interior. This affects // the latitude bounds but not the longitude bounds. absLat := math.Acos(math.Abs(aCrossB.Z / aCrossB.Norm())) lat := rb.bound.Lat if da < 0 { // It's possible that absLat < lat.lo() due to numerical errors. lat = r1.IntervalFromPointPair(lat.Lo, math.Max(absLat, rb.bound.Lat.Hi)) } else { lat = r1.IntervalFromPointPair(math.Min(-absLat, rb.bound.Lat.Lo), lat.Hi) } rb.bound = Rect{lat, rb.bound.Lng} } } rb.a = b rb.aLatLng = bLatLng }