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 }