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  }