github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/geo/geoindex/s2_geometry_index.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 geoindex 12 13 import ( 14 "context" 15 16 "github.com/cockroachdb/cockroach/pkg/geo" 17 "github.com/cockroachdb/cockroach/pkg/geo/geos" 18 "github.com/cockroachdb/errors" 19 "github.com/golang/geo/r3" 20 "github.com/golang/geo/s2" 21 "github.com/twpayne/go-geom" 22 ) 23 24 // s2GeometryIndex is an implementation of GeometryIndex that uses the S2 geometry 25 // library. 26 type s2GeometryIndex struct { 27 rc *s2.RegionCoverer 28 minX, maxX, minY, maxY float64 29 } 30 31 var _ GeometryIndex = (*s2GeometryIndex)(nil) 32 33 // NewS2GeometryIndex returns an index with the given configuration. All reads and 34 // writes on this index must use the same config. Writes must use the same 35 // config to correctly process deletions. Reads must use the same config since 36 // the bounds affect when a read needs to look at the exceedsBoundsCellID. 37 func NewS2GeometryIndex(cfg S2GeometryConfig) GeometryIndex { 38 // TODO(sumeer): Sanity check cfg. 39 return &s2GeometryIndex{ 40 rc: &s2.RegionCoverer{ 41 MinLevel: int(cfg.S2Config.MinLevel), 42 MaxLevel: int(cfg.S2Config.MaxLevel), 43 LevelMod: int(cfg.S2Config.LevelMod), 44 MaxCells: int(cfg.S2Config.MaxCells), 45 }, 46 minX: cfg.MinX, 47 maxX: cfg.MaxX, 48 minY: cfg.MinY, 49 maxY: cfg.MaxY, 50 } 51 } 52 53 // DefaultGeometryIndexConfig returns a default config for a geometry index. 54 func DefaultGeometryIndexConfig() *Config { 55 return &Config{ 56 S2Geometry: &S2GeometryConfig{ 57 // Arbitrary bounding box. 58 // TODO(sumeer): replace with parameters specified by CREATE INDEX. 59 MinX: -10000, 60 MaxX: 10000, 61 MinY: -10000, 62 MaxY: 10000, 63 S2Config: defaultS2Config()}, 64 } 65 } 66 67 // A cell id unused by S2. We use it to index geometries that exceed the 68 // configured bounds. 69 const exceedsBoundsCellID = s2.CellID(^uint64(0)) 70 71 // TODO(sumeer): adjust code to handle precision issues with floating point 72 // arithmetic. 73 74 // InvertedIndexKeys implements the GeometryIndex interface. 75 func (s *s2GeometryIndex) InvertedIndexKeys(c context.Context, g *geo.Geometry) ([]Key, error) { 76 // If the geometry exceeds the bounds, we index the clipped geometry in 77 // addition to the special cell, so that queries for geometries that don't 78 // exceed the bounds don't need to query the special cell (which would 79 // become a hotspot in the key space). 80 gt, clipped, err := s.convertToGeomTAndTryClip(g) 81 if err != nil { 82 return nil, err 83 } 84 var keys []Key 85 if gt != nil { 86 r := s.s2RegionsFromPlanarGeom(gt) 87 keys = invertedIndexKeys(c, s.rc, r) 88 } 89 if clipped { 90 keys = append(keys, Key(exceedsBoundsCellID)) 91 } 92 return keys, nil 93 } 94 95 // Covers implements the GeometryIndex interface. 96 func (s *s2GeometryIndex) Covers(c context.Context, g *geo.Geometry) (UnionKeySpans, error) { 97 return s.Intersects(c, g) 98 } 99 100 // CoveredBy implements the GeometryIndex interface. 101 func (s *s2GeometryIndex) CoveredBy(c context.Context, g *geo.Geometry) (RPKeyExpr, error) { 102 // If the geometry exceeds the bounds, we use the clipped geometry to 103 // restrict the search within the bounds. 104 gt, clipped, err := s.convertToGeomTAndTryClip(g) 105 if err != nil { 106 return nil, err 107 } 108 var expr RPKeyExpr 109 if gt != nil { 110 r := s.s2RegionsFromPlanarGeom(gt) 111 expr = coveredBy(c, s.rc, r) 112 } 113 if clipped { 114 // Intersect with the shapes that exceed the bounds. 115 expr = append(expr, Key(exceedsBoundsCellID)) 116 if len(expr) > 1 { 117 expr = append(expr, RPSetIntersection) 118 } 119 } 120 return expr, nil 121 } 122 123 // Intersects implements the GeometryIndex interface. 124 func (s *s2GeometryIndex) Intersects(c context.Context, g *geo.Geometry) (UnionKeySpans, error) { 125 // If the geometry exceeds the bounds, we use the clipped geometry to 126 // restrict the search within the bounds. 127 gt, clipped, err := s.convertToGeomTAndTryClip(g) 128 if err != nil { 129 return nil, err 130 } 131 var spans UnionKeySpans 132 if gt != nil { 133 r := s.s2RegionsFromPlanarGeom(gt) 134 spans = intersects(c, s.rc, r) 135 } 136 if clipped { 137 // And lookup all shapes that exceed the bounds. 138 spans = append(spans, KeySpan{Start: Key(exceedsBoundsCellID), End: Key(exceedsBoundsCellID)}) 139 } 140 return spans, nil 141 } 142 143 // Converts to geom.T and clips to the rectangle bounds of the index. 144 func (s *s2GeometryIndex) convertToGeomTAndTryClip(g *geo.Geometry) (geom.T, bool, error) { 145 gt, err := g.AsGeomT() 146 if err != nil { 147 return nil, false, err 148 } 149 clipped := false 150 if s.geomExceedsBounds(gt) { 151 clipped = true 152 clippedEWKB, err := 153 geos.ClipEWKBByRect(g.EWKB(), s.minX, s.minY, s.maxX, s.maxY) 154 if err != nil { 155 return nil, false, err 156 } 157 gt = nil 158 if clippedEWKB != nil { 159 g, err = geo.ParseGeometryFromEWKBRaw(clippedEWKB) 160 if err != nil { 161 return nil, false, err 162 } 163 if g == nil { 164 return nil, false, errors.Errorf("internal error: clippedWKB cannot be parsed") 165 } 166 gt, err = g.AsGeomT() 167 if err != nil { 168 return nil, false, err 169 } 170 } 171 } 172 return gt, clipped, nil 173 } 174 175 // Returns true if the point represented by (x, y) exceeds the rectangle 176 // bounds of the index. 177 func (s *s2GeometryIndex) xyExceedsBounds(x float64, y float64) bool { 178 if x < s.minX || x > s.maxX { 179 return true 180 } 181 if y < s.minY || y > s.maxY { 182 return true 183 } 184 return false 185 } 186 187 // Returns true if g exceeds the rectangle bounds of the index. 188 func (s *s2GeometryIndex) geomExceedsBounds(g geom.T) bool { 189 switch repr := g.(type) { 190 case *geom.Point: 191 return s.xyExceedsBounds(repr.X(), repr.Y()) 192 case *geom.LineString: 193 for i := 0; i < repr.NumCoords(); i++ { 194 p := repr.Coord(i) 195 if s.xyExceedsBounds(p.X(), p.Y()) { 196 return true 197 } 198 } 199 case *geom.Polygon: 200 if repr.NumLinearRings() > 0 { 201 lr := repr.LinearRing(0) 202 for i := 0; i < lr.NumCoords(); i++ { 203 if s.xyExceedsBounds(lr.Coord(i).X(), lr.Coord(i).Y()) { 204 return true 205 } 206 } 207 } 208 case *geom.GeometryCollection: 209 for _, geom := range repr.Geoms() { 210 if s.geomExceedsBounds(geom) { 211 return true 212 } 213 } 214 case *geom.MultiPoint: 215 for i := 0; i < repr.NumPoints(); i++ { 216 if s.geomExceedsBounds(repr.Point(i)) { 217 return true 218 } 219 } 220 case *geom.MultiLineString: 221 for i := 0; i < repr.NumLineStrings(); i++ { 222 if s.geomExceedsBounds(repr.LineString(i)) { 223 return true 224 } 225 } 226 case *geom.MultiPolygon: 227 for i := 0; i < repr.NumPolygons(); i++ { 228 if s.geomExceedsBounds(repr.Polygon(i)) { 229 return true 230 } 231 } 232 } 233 return false 234 } 235 236 // stToUV() and face0UVToXYZPoint() are adapted from unexported methods in 237 // github.com/golang/geo/s2/stuv.go 238 239 // stToUV converts an s or t value to the corresponding u or v value. 240 // This is a non-linear transformation from [-1,1] to [-1,1] that 241 // attempts to make the cell sizes more uniform. 242 // This uses what the C++ version calls 'the quadratic transform'. 243 func stToUV(s float64) float64 { 244 if s >= 0.5 { 245 return (1 / 3.) * (4*s*s - 1) 246 } 247 return (1 / 3.) * (1 - 4*(1-s)*(1-s)) 248 } 249 250 // Specialized version of faceUVToXYZ() for face 0 251 func face0UVToXYZPoint(u, v float64) s2.Point { 252 return s2.Point{Vector: r3.Vector{X: 1, Y: u, Z: v}} 253 } 254 255 func (s *s2GeometryIndex) planarPointToS2Point(x float64, y float64) s2.Point { 256 ss := (x - s.minX) / (s.maxX - s.minX) 257 tt := (y - s.minY) / (s.maxY - s.minY) 258 u := stToUV(ss) 259 v := stToUV(tt) 260 return face0UVToXYZPoint(u, v) 261 } 262 263 // TODO(sumeer): this is similar to s2RegionsFromGeom() but needs to do 264 // a different point conversion. If these functions do not diverge further, 265 // and turn out not to be performance critical, merge the two implementations. 266 func (s *s2GeometryIndex) s2RegionsFromPlanarGeom(geomRepr geom.T) []s2.Region { 267 var regions []s2.Region 268 switch repr := geomRepr.(type) { 269 case *geom.Point: 270 regions = []s2.Region{ 271 s.planarPointToS2Point(repr.X(), repr.Y()), 272 } 273 case *geom.LineString: 274 points := make([]s2.Point, repr.NumCoords()) 275 for i := 0; i < repr.NumCoords(); i++ { 276 p := repr.Coord(i) 277 points[i] = s.planarPointToS2Point(p.X(), p.Y()) 278 } 279 pl := s2.Polyline(points) 280 regions = []s2.Region{&pl} 281 case *geom.Polygon: 282 loops := make([]*s2.Loop, repr.NumLinearRings()) 283 // The first ring is a "shell", which is represented as CCW. 284 // Following rings are "holes", which are CW. For S2, they are CCW and automatically figured out. 285 for ringIdx := 0; ringIdx < repr.NumLinearRings(); ringIdx++ { 286 linearRing := repr.LinearRing(ringIdx) 287 points := make([]s2.Point, linearRing.NumCoords()) 288 for pointIdx := 0; pointIdx < linearRing.NumCoords(); pointIdx++ { 289 p := linearRing.Coord(pointIdx) 290 pt := s.planarPointToS2Point(p.X(), p.Y()) 291 if ringIdx == 0 { 292 points[pointIdx] = pt 293 } else { 294 points[len(points)-pointIdx-1] = pt 295 } 296 } 297 loops[ringIdx] = s2.LoopFromPoints(points) 298 } 299 regions = []s2.Region{ 300 s2.PolygonFromLoops(loops), 301 } 302 case *geom.GeometryCollection: 303 for _, geom := range repr.Geoms() { 304 regions = append(regions, s.s2RegionsFromPlanarGeom(geom)...) 305 } 306 case *geom.MultiPoint: 307 for i := 0; i < repr.NumPoints(); i++ { 308 regions = append(regions, s.s2RegionsFromPlanarGeom(repr.Point(i))...) 309 } 310 case *geom.MultiLineString: 311 for i := 0; i < repr.NumLineStrings(); i++ { 312 regions = append(regions, s.s2RegionsFromPlanarGeom(repr.LineString(i))...) 313 } 314 case *geom.MultiPolygon: 315 for i := 0; i < repr.NumPolygons(); i++ { 316 regions = append(regions, s.s2RegionsFromPlanarGeom(repr.Polygon(i))...) 317 } 318 } 319 return regions 320 } 321 322 func (s *s2GeometryIndex) TestingInnerCovering(g *geo.Geometry) s2.CellUnion { 323 gt, _, err := s.convertToGeomTAndTryClip(g) 324 if err != nil || gt == nil { 325 return nil 326 } 327 r := s.s2RegionsFromPlanarGeom(gt) 328 return innerCovering(s.rc, r) 329 }