github.com/cockroachdb/cockroachdb-parser@v0.23.3-0.20240213214944-911057d40c9a/pkg/geo/bbox.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package geo
    12  
    13  import (
    14  	"fmt"
    15  	"math"
    16  	"strconv"
    17  	"strings"
    18  
    19  	"github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb"
    20  	"github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode"
    21  	"github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror"
    22  	"github.com/cockroachdb/errors"
    23  	"github.com/golang/geo/s2"
    24  	geom "github.com/twpayne/go-geom"
    25  )
    26  
    27  // CartesianBoundingBox is the cartesian BoundingBox representation,
    28  // meant for use for GEOMETRY types.
    29  type CartesianBoundingBox struct {
    30  	geopb.BoundingBox
    31  }
    32  
    33  // NewCartesianBoundingBox returns a properly initialized empty bounding box
    34  // for cartesian plane types.
    35  func NewCartesianBoundingBox() *CartesianBoundingBox {
    36  	return nil
    37  }
    38  
    39  // Repr is the string representation of the CartesianBoundingBox.
    40  func (b *CartesianBoundingBox) Repr() string {
    41  	return string(b.AppendFormat(nil))
    42  }
    43  
    44  // AppendFormat appends string representation of the CartesianBoundingBox
    45  // to the buffer, and returns modified buffer.
    46  func (b *CartesianBoundingBox) AppendFormat(buf []byte) []byte {
    47  	buf = append(buf, "BOX("...)
    48  	buf = strconv.AppendFloat(buf, b.LoX, 'f', -1, 64)
    49  	buf = append(buf, ' ')
    50  	buf = strconv.AppendFloat(buf, b.LoY, 'f', -1, 64)
    51  	buf = append(buf, ',')
    52  	buf = strconv.AppendFloat(buf, b.HiX, 'f', -1, 64)
    53  	buf = append(buf, ' ')
    54  	buf = strconv.AppendFloat(buf, b.HiY, 'f', -1, 64)
    55  	return append(buf, ')')
    56  }
    57  
    58  // ParseCartesianBoundingBox parses a box2d string into a bounding box.
    59  func ParseCartesianBoundingBox(s string) (CartesianBoundingBox, error) {
    60  	b := CartesianBoundingBox{}
    61  	var prefix string
    62  	numScanned, err := fmt.Sscanf(s, "%3s(%f %f,%f %f)", &prefix, &b.LoX, &b.LoY, &b.HiX, &b.HiY)
    63  	if err != nil {
    64  		return b, errors.Wrapf(err, "error parsing box2d")
    65  	}
    66  	if numScanned != 5 || strings.ToLower(prefix) != "box" {
    67  		return b, pgerror.Newf(pgcode.InvalidParameterValue, "expected format 'box(min_x min_y,max_x max_y)'")
    68  	}
    69  	return b, nil
    70  }
    71  
    72  // Compare returns the comparison between two bounding boxes.
    73  // Compare lower dimensions before higher ones, i.e. X, then Y.
    74  // In SQL, NaN is treated as less than all other float values. In Go, any
    75  // comparison with NaN returns false.
    76  func (b *CartesianBoundingBox) Compare(o *CartesianBoundingBox) int {
    77  	if b.LoX < o.LoX || (math.IsNaN(b.LoX) && !math.IsNaN(o.LoX)) {
    78  		return -1
    79  	} else if b.LoX > o.LoX || (!math.IsNaN(b.LoX) && math.IsNaN(o.LoX)) {
    80  		return 1
    81  	}
    82  
    83  	if b.HiX < o.HiX || (math.IsNaN(b.HiX) && !math.IsNaN(o.HiX)) {
    84  		return -1
    85  	} else if b.HiX > o.HiX || (!math.IsNaN(b.HiX) && math.IsNaN(o.HiX)) {
    86  		return 1
    87  	}
    88  
    89  	if b.LoY < o.LoY || (math.IsNaN(b.LoY) && !math.IsNaN(o.LoY)) {
    90  		return -1
    91  	} else if b.LoY > o.LoY || (!math.IsNaN(b.LoY) && math.IsNaN(o.LoY)) {
    92  		return 1
    93  	}
    94  
    95  	if b.HiY < o.HiY || (math.IsNaN(b.HiY) && !math.IsNaN(o.HiY)) {
    96  		return -1
    97  	} else if b.HiY > o.HiY || (!math.IsNaN(b.HiY) && math.IsNaN(o.HiY)) {
    98  		return 1
    99  	}
   100  
   101  	return 0
   102  }
   103  
   104  // WithPoint includes a new point to the CartesianBoundingBox.
   105  // It will edit any bounding box in place.
   106  func (b *CartesianBoundingBox) WithPoint(x, y float64) *CartesianBoundingBox {
   107  	if b == nil {
   108  		return &CartesianBoundingBox{
   109  			BoundingBox: geopb.BoundingBox{
   110  				LoX: x,
   111  				HiX: x,
   112  				LoY: y,
   113  				HiY: y,
   114  			},
   115  		}
   116  	}
   117  	b.BoundingBox = geopb.BoundingBox{
   118  		LoX: math.Min(b.LoX, x),
   119  		HiX: math.Max(b.HiX, x),
   120  		LoY: math.Min(b.LoY, y),
   121  		HiY: math.Max(b.HiY, y),
   122  	}
   123  	return b
   124  }
   125  
   126  // AddPoint adds a point to the CartesianBoundingBox coordinates.
   127  // Returns a copy of the CartesianBoundingBox.
   128  func (b *CartesianBoundingBox) AddPoint(x, y float64) *CartesianBoundingBox {
   129  	if b == nil {
   130  		return &CartesianBoundingBox{
   131  			BoundingBox: geopb.BoundingBox{
   132  				LoX: x,
   133  				HiX: x,
   134  				LoY: y,
   135  				HiY: y,
   136  			},
   137  		}
   138  	}
   139  	return &CartesianBoundingBox{
   140  		BoundingBox: geopb.BoundingBox{
   141  			LoX: math.Min(b.LoX, x),
   142  			HiX: math.Max(b.HiX, x),
   143  			LoY: math.Min(b.LoY, y),
   144  			HiY: math.Max(b.HiY, y),
   145  		},
   146  	}
   147  }
   148  
   149  // Combine combines two bounding boxes together.
   150  // Returns a copy of the CartesianBoundingBox.
   151  func (b *CartesianBoundingBox) Combine(o *CartesianBoundingBox) *CartesianBoundingBox {
   152  	if o == nil {
   153  		return b
   154  	}
   155  	return b.AddPoint(o.LoX, o.LoY).AddPoint(o.HiX, o.HiY)
   156  }
   157  
   158  // Buffer adds deltaX and deltaY to the bounding box on both the Lo and Hi side.
   159  func (b *CartesianBoundingBox) Buffer(deltaX, deltaY float64) *CartesianBoundingBox {
   160  	if b == nil {
   161  		return nil
   162  	}
   163  	return &CartesianBoundingBox{
   164  		BoundingBox: geopb.BoundingBox{
   165  			LoX: b.LoX - deltaX,
   166  			HiX: b.HiX + deltaX,
   167  			LoY: b.LoY - deltaY,
   168  			HiY: b.HiY + deltaY,
   169  		},
   170  	}
   171  }
   172  
   173  // Intersects returns whether the BoundingBoxes intersect.
   174  // Empty bounding boxes never intersect.
   175  func (b *CartesianBoundingBox) Intersects(o *CartesianBoundingBox) bool {
   176  	// If either side is empty, they do not intersect.
   177  	if b == nil || o == nil {
   178  		return false
   179  	}
   180  	if b.LoY > o.HiY || o.LoY > b.HiY ||
   181  		b.LoX > o.HiX || o.LoX > b.HiX {
   182  		return false
   183  	}
   184  	return true
   185  }
   186  
   187  // Covers returns whether the BoundingBox covers the other bounding box.
   188  // Empty bounding boxes never cover.
   189  func (b *CartesianBoundingBox) Covers(o *CartesianBoundingBox) bool {
   190  	if b == nil || o == nil {
   191  		return false
   192  	}
   193  	return b.LoX <= o.LoX && o.LoX <= b.HiX &&
   194  		b.LoX <= o.HiX && o.HiX <= b.HiX &&
   195  		b.LoY <= o.LoY && o.LoY <= b.HiY &&
   196  		b.LoY <= o.HiY && o.HiY <= b.HiY
   197  }
   198  
   199  // ToGeomT converts a BoundingBox to a GeomT.
   200  func (b *CartesianBoundingBox) ToGeomT(srid geopb.SRID) geom.T {
   201  	if b.LoX == b.HiX && b.LoY == b.HiY {
   202  		return geom.NewPointFlat(geom.XY, []float64{b.LoX, b.LoY}).SetSRID(int(srid))
   203  	}
   204  	if b.LoX == b.HiX || b.LoY == b.HiY {
   205  		return geom.NewLineStringFlat(geom.XY, []float64{b.LoX, b.LoY, b.HiX, b.HiY}).SetSRID(int(srid))
   206  	}
   207  	return geom.NewPolygonFlat(
   208  		geom.XY,
   209  		[]float64{
   210  			b.LoX, b.LoY,
   211  			b.LoX, b.HiY,
   212  			b.HiX, b.HiY,
   213  			b.HiX, b.LoY,
   214  			b.LoX, b.LoY,
   215  		},
   216  		[]int{10},
   217  	).SetSRID(int(srid))
   218  }
   219  
   220  // boundingBoxFromGeomT returns a bounding box from a given geom.T.
   221  // Returns nil if no bounding box was found.
   222  func boundingBoxFromGeomT(g geom.T, soType geopb.SpatialObjectType) (*geopb.BoundingBox, error) {
   223  	switch soType {
   224  	case geopb.SpatialObjectType_GeometryType:
   225  		ret := BoundingBoxFromGeomTGeometryType(g)
   226  		if ret == nil {
   227  			return nil, nil
   228  		}
   229  		return &ret.BoundingBox, nil
   230  	case geopb.SpatialObjectType_GeographyType:
   231  		rect, err := boundingBoxFromGeomTGeographyType(g)
   232  		if err != nil {
   233  			return nil, err
   234  		}
   235  		if rect.IsEmpty() {
   236  			return nil, nil
   237  		}
   238  		return &geopb.BoundingBox{
   239  			LoX: rect.Lng.Lo,
   240  			HiX: rect.Lng.Hi,
   241  			LoY: rect.Lat.Lo,
   242  			HiY: rect.Lat.Hi,
   243  		}, nil
   244  	}
   245  	return nil, pgerror.Newf(pgcode.InvalidParameterValue, "unknown spatial type: %s", soType)
   246  }
   247  
   248  // BoundingBoxFromGeomTGeometryType returns an appropriate bounding box for a Geometry type.
   249  func BoundingBoxFromGeomTGeometryType(g geom.T) *CartesianBoundingBox {
   250  	if g.Empty() {
   251  		return nil
   252  	}
   253  	bbox := NewCartesianBoundingBox()
   254  	switch g := g.(type) {
   255  	case *geom.GeometryCollection:
   256  		for i := 0; i < g.NumGeoms(); i++ {
   257  			shapeBBox := BoundingBoxFromGeomTGeometryType(g.Geom(i))
   258  			if shapeBBox == nil {
   259  				continue
   260  			}
   261  			bbox = bbox.WithPoint(shapeBBox.LoX, shapeBBox.LoY).WithPoint(shapeBBox.HiX, shapeBBox.HiY)
   262  		}
   263  	default:
   264  		flatCoords := g.FlatCoords()
   265  		for i := 0; i < len(flatCoords); i += g.Stride() {
   266  			bbox = bbox.WithPoint(flatCoords[i], flatCoords[i+1])
   267  		}
   268  	}
   269  	return bbox
   270  }
   271  
   272  // boundingBoxFromGeomTGeographyType returns an appropriate bounding box for a
   273  // Geography type. There are marginally invalid shapes for which we want
   274  // bounding boxes that are correct regardless of the validity of the shape,
   275  // since validity checks may return slightly different results in S2 and the
   276  // other libraries we use. Therefore, instead of constructing s2.Region(s)
   277  // from the shape, which will expose us to S2's validity checks, we use the
   278  // points and lines directly to compute the bounding box.
   279  func boundingBoxFromGeomTGeographyType(g geom.T) (s2.Rect, error) {
   280  	if g.Empty() {
   281  		return s2.EmptyRect(), nil
   282  	}
   283  	rect := s2.EmptyRect()
   284  	switch g := g.(type) {
   285  	case *geom.Point:
   286  		return geogPointsBBox(g)
   287  	case *geom.MultiPoint:
   288  		return geogPointsBBox(g)
   289  	case *geom.LineString:
   290  		return geogLineBBox(g)
   291  	case *geom.MultiLineString:
   292  		for i := 0; i < g.NumLineStrings(); i++ {
   293  			r, err := geogLineBBox(g.LineString(i))
   294  			if err != nil {
   295  				return s2.EmptyRect(), err
   296  			}
   297  			rect = rect.Union(r)
   298  		}
   299  	case *geom.Polygon:
   300  		for i := 0; i < g.NumLinearRings(); i++ {
   301  			r, err := geogLineBBox(g.LinearRing(i))
   302  			if err != nil {
   303  				return s2.EmptyRect(), err
   304  			}
   305  			rect = rect.Union(r)
   306  		}
   307  	case *geom.MultiPolygon:
   308  		for i := 0; i < g.NumPolygons(); i++ {
   309  			polyRect, err := boundingBoxFromGeomTGeographyType(g.Polygon(i))
   310  			if err != nil {
   311  				return s2.EmptyRect(), err
   312  			}
   313  			rect = rect.Union(polyRect)
   314  		}
   315  	case *geom.GeometryCollection:
   316  		for i := 0; i < g.NumGeoms(); i++ {
   317  			collRect, err := boundingBoxFromGeomTGeographyType(g.Geom(i))
   318  			if err != nil {
   319  				return s2.EmptyRect(), err
   320  			}
   321  			rect = rect.Union(collRect)
   322  		}
   323  	default:
   324  		return s2.EmptyRect(), errors.Errorf("unknown type %T", g)
   325  	}
   326  	return rect, nil
   327  }
   328  
   329  // geogPointsBBox constructs a bounding box, represented as a s2.Rect, for the set
   330  // of points contained in g.
   331  func geogPointsBBox(g geom.T) (s2.Rect, error) {
   332  	rect := s2.EmptyRect()
   333  	flatCoords := g.FlatCoords()
   334  	for i := 0; i < len(flatCoords); i += g.Stride() {
   335  		point := s2.LatLngFromDegrees(flatCoords[i+1], flatCoords[i])
   336  		if !point.IsValid() {
   337  			return s2.EmptyRect(), OutOfRangeError()
   338  		}
   339  		rect = rect.AddPoint(point)
   340  	}
   341  	return rect, nil
   342  }
   343  
   344  // geogLineBBox constructs a bounding box, represented as a s2.Rect, for the line
   345  // or ring/loop represented by g.
   346  func geogLineBBox(g geom.T) (s2.Rect, error) {
   347  	bounder := s2.NewRectBounder()
   348  	flatCoords := g.FlatCoords()
   349  	for i := 0; i < len(flatCoords); i += g.Stride() {
   350  		point := s2.LatLngFromDegrees(flatCoords[i+1], flatCoords[i])
   351  		if !point.IsValid() {
   352  			return s2.EmptyRect(), OutOfRangeError()
   353  		}
   354  		bounder.AddPoint(s2.PointFromLatLng(point))
   355  	}
   356  	return bounder.RectBound(), nil
   357  }