Commit 0b65365f authored by craig[bot]'s avatar craig[bot]

Merge #50765

50765: geo: fix BoundingBox for Geography types r=sumeerbhola a=otan

Our previous assumption that bounding boxes for geography types was
wrong -- we assumed that we can just compare min / max coordinates, but
for spherical geometries bounding boxes can traverse latitude/longitude
bounds and these min / max checks do not hold.

As such, we need to replace these bounding boxes with the s2.Rect
representation, which has complex logic that accounts for this
behaviour. This means bounding boxes are different between GEOMETRY and
GEOGRAPHY types.

The following changes have been made:
* Rename BoundingBox vars to Lo/Hi instead of Min/Max, to be more true
  to the name.
* Move BoundingBox comparison logic to the geo package from the geopb
  package, using CartesianBoundingBox as the name for GEOMETRY types.
* Change logic that converts between GEOMETRY and GEOGRAPHY types to
  change cached bounding boxes.
* Add SpatialObjectType to the SpatialObject protobuf. This allows us to
  differentiate geo vs geom types (PostGIS does this too). We in theory
  don't need this, but I think it stops programmer error so I'd rather
  keep it around.
* Added regression tests for this observed behaviour in the geogfn
  package.

Release note: None
Co-authored-by: default avatarOliver Tan <[email protected]>
parents 9cecf3b3 86ad9e76
......@@ -11,33 +11,120 @@
package geo
import (
"math"
"github.com/cockroachdb/cockroach/pkg/geo/geopb"
"github.com/cockroachdb/errors"
"github.com/golang/geo/s2"
"github.com/twpayne/go-geom"
)
// boundingBoxFromGeom returns a bounding box from a given geom.T.
// CartesianBoundingBox is the cartesian BoundingBox representation,
// meant for use for GEOMETRY types.
type CartesianBoundingBox struct {
geopb.BoundingBox
}
// NewCartesianBoundingBox returns a properly initialized empty bounding box
// for carestian plane types.
func NewCartesianBoundingBox() *CartesianBoundingBox {
return &CartesianBoundingBox{
BoundingBox: geopb.BoundingBox{
LoX: math.MaxFloat64,
HiX: -math.MaxFloat64,
LoY: math.MaxFloat64,
HiY: -math.MaxFloat64,
},
}
}
// AddPoint adds a point to the BoundingBox coordinates.
func (b *CartesianBoundingBox) AddPoint(x, y float64) {
b.LoX = math.Min(b.LoX, x)
b.HiX = math.Max(b.HiX, x)
b.LoY = math.Min(b.LoY, y)
b.HiY = math.Max(b.HiY, y)
}
// Intersects returns whether the BoundingBoxes intersect.
// Empty bounding boxes never intersect.
func (b *CartesianBoundingBox) Intersects(o *CartesianBoundingBox) bool {
// If either side is empty, they do not intersect.
if b == nil || o == nil {
return false
}
if b.LoY > o.HiY || o.LoY > b.HiY ||
b.LoX > o.HiX || o.LoX > b.HiX {
return false
}
return true
}
// boundingBoxFromGeomT returns a bounding box from a given geom.T.
// Returns nil if no bounding box was found.
func boundingBoxFromGeom(g geom.T) *geopb.BoundingBox {
bbox := geopb.NewBoundingBox()
func boundingBoxFromGeomT(g geom.T, soType geopb.SpatialObjectType) (*geopb.BoundingBox, error) {
switch soType {
case geopb.SpatialObjectType_GeometryType:
ret := boundingBoxFromGeomTGeometryType(g)
if ret == nil {
return nil, nil
}
return &ret.BoundingBox, nil
case geopb.SpatialObjectType_GeographyType:
rect, err := boundingBoxFromGeomTGeographyType(g)
if err != nil {
return nil, err
}
if rect.IsEmpty() {
return nil, nil
}
return &geopb.BoundingBox{
LoX: rect.Lng.Lo,
HiX: rect.Lng.Hi,
LoY: rect.Lat.Lo,
HiY: rect.Lat.Hi,
}, nil
}
return nil, errors.Newf("unknown spatial type: %s", soType)
}
// boundingBoxFromGeomTGeometryType returns an appropriate bounding box for a Geometry type.
func boundingBoxFromGeomTGeometryType(g geom.T) *CartesianBoundingBox {
if g.Empty() {
return nil
}
bbox := NewCartesianBoundingBox()
switch g := g.(type) {
case *geom.GeometryCollection:
for i := 0; i < g.NumGeoms(); i++ {
shapeBBox := boundingBoxFromGeom(g.Geom(i))
shapeBBox := boundingBoxFromGeomTGeometryType(g.Geom(i))
if shapeBBox == nil {
continue
}
bbox.Update(shapeBBox.MinX, shapeBBox.MinY)
bbox.Update(shapeBBox.MaxX, shapeBBox.MaxY)
bbox.AddPoint(shapeBBox.LoX, shapeBBox.LoY)
bbox.AddPoint(shapeBBox.HiX, shapeBBox.HiY)
}
default:
flatCoords := g.FlatCoords()
for i := 0; i < len(flatCoords); i += g.Stride() {
// i and i+1 always represent x and y.
bbox.Update(flatCoords[i], flatCoords[i+1])
bbox.AddPoint(flatCoords[i], flatCoords[i+1])
}
}
return bbox
}
// boundingBoxFromGeomTGeographyType returns an appropriate bounding box for a Geography type.
func boundingBoxFromGeomTGeographyType(g geom.T) (s2.Rect, error) {
if g.Empty() {
return s2.EmptyRect(), nil
}
regions, err := S2RegionsFromGeom(g, EmptyBehaviorOmit)
if err != nil {
return s2.EmptyRect(), err
}
rect := s2.EmptyRect()
for _, region := range regions {
rect = rect.Union(region.RectBound())
}
return rect, nil
}
......@@ -19,36 +19,116 @@ import (
"github.com/twpayne/go-geom"
)
func TestBoundingBoxFromGeom(t *testing.T) {
func TestBoundingBoxFromGeomT(t *testing.T) {
testCases := []struct {
soType geopb.SpatialObjectType
g geom.T
expected *geopb.BoundingBox
}{
{geom.NewPointFlat(geom.XY, []float64{-15, -20}), &geopb.BoundingBox{MinX: -15, MaxX: -15, MinY: -20, MaxY: -20}},
{geom.NewPointFlat(geom.XY, []float64{0, 0}), &geopb.BoundingBox{MinX: 0, MaxX: 0, MinY: 0, MaxY: 0}},
{testGeomPoint, &geopb.BoundingBox{MinX: 1, MaxX: 1, MinY: 2, MaxY: 2}},
{testGeomLineString, &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2}},
{geom.NewLineStringFlat(geom.XY, []float64{-15, -20, -30, -40}), &geopb.BoundingBox{MinX: -30, MaxX: -15, MinY: -40, MaxY: -20}},
{testGeomPolygon, &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2}},
{testGeomMultiPoint, &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2}},
{testGeomMultiLineString, &geopb.BoundingBox{MinX: 1, MaxX: 4, MinY: 1, MaxY: 4}},
{testGeomMultiPolygon, &geopb.BoundingBox{MinX: 1, MaxX: 4, MinY: 1, MaxY: 4}},
{testGeomGeometryCollection, &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2}},
{emptyGeomPoint, nil},
{emptyGeomLineString, nil},
{emptyGeomPolygon, nil},
{emptyGeomMultiPoint, nil},
{emptyGeomMultiLineString, nil},
{emptyGeomMultiPolygon, nil},
{emptyGeomGeometryCollection, nil},
{emptyGeomPointInGeometryCollection, &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2}},
{emptyGeomObjectsInGeometryCollection, nil},
{geopb.SpatialObjectType_GeometryType, geom.NewPointFlat(geom.XY, []float64{-15, -20}), &geopb.BoundingBox{LoX: -15, HiX: -15, LoY: -20, HiY: -20}},
{geopb.SpatialObjectType_GeometryType, geom.NewPointFlat(geom.XY, []float64{0, 0}), &geopb.BoundingBox{LoX: 0, HiX: 0, LoY: 0, HiY: 0}},
{geopb.SpatialObjectType_GeometryType, testGeomPoint, &geopb.BoundingBox{LoX: 1, HiX: 1, LoY: 2, HiY: 2}},
{geopb.SpatialObjectType_GeometryType, testGeomLineString, &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2}},
{geopb.SpatialObjectType_GeometryType, geom.NewLineStringFlat(geom.XY, []float64{-15, -20, -30, -40}), &geopb.BoundingBox{LoX: -30, HiX: -15, LoY: -40, HiY: -20}},
{geopb.SpatialObjectType_GeometryType, testGeomPolygon, &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2}},
{geopb.SpatialObjectType_GeometryType, testGeomMultiPoint, &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2}},
{geopb.SpatialObjectType_GeometryType, testGeomMultiLineString, &geopb.BoundingBox{LoX: 1, HiX: 4, LoY: 1, HiY: 4}},
{geopb.SpatialObjectType_GeometryType, testGeomMultiPolygon, &geopb.BoundingBox{LoX: 1, HiX: 4, LoY: 1, HiY: 4}},
{geopb.SpatialObjectType_GeometryType, testGeomGeometryCollection, &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2}},
{geopb.SpatialObjectType_GeometryType, emptyGeomPoint, nil},
{geopb.SpatialObjectType_GeometryType, emptyGeomLineString, nil},
{geopb.SpatialObjectType_GeometryType, emptyGeomPolygon, nil},
{geopb.SpatialObjectType_GeometryType, emptyGeomMultiPoint, nil},
{geopb.SpatialObjectType_GeometryType, emptyGeomMultiLineString, nil},
{geopb.SpatialObjectType_GeometryType, emptyGeomMultiPolygon, nil},
{geopb.SpatialObjectType_GeometryType, emptyGeomGeometryCollection, nil},
{geopb.SpatialObjectType_GeometryType, emptyGeomPointInGeometryCollection, &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2}},
{geopb.SpatialObjectType_GeometryType, emptyGeomObjectsInGeometryCollection, nil},
{geopb.SpatialObjectType_GeographyType, geom.NewPointFlat(geom.XY, []float64{-15, -20}), &geopb.BoundingBox{LoX: -0.2617993877991494, LoY: -0.3490658503988659, HiX: -0.2617993877991494, HiY: -0.3490658503988659}},
{geopb.SpatialObjectType_GeographyType, geom.NewPointFlat(geom.XY, []float64{0, 0}), &geopb.BoundingBox{LoX: 0, LoY: 0, HiX: 0, HiY: 0}},
{geopb.SpatialObjectType_GeographyType, testGeomPoint, &geopb.BoundingBox{LoX: 0.017453292519943292, LoY: 0.03490658503988659, HiX: 0.017453292519943292, HiY: 0.03490658503988659}},
{geopb.SpatialObjectType_GeographyType, testGeomLineString, &geopb.BoundingBox{LoX: 0.017453292519943292, LoY: 0.01745329251994285, HiX: 0.03490658503988659, HiY: 0.03490658503988703}},
{geopb.SpatialObjectType_GeographyType, geom.NewLineStringFlat(geom.XY, []float64{-15, -20, -30, -40}), &geopb.BoundingBox{LoX: -0.5235987755982988, LoY: -0.6981317007977321, HiX: -0.2617993877991494, HiY: -0.34906585039886545}},
{geopb.SpatialObjectType_GeographyType, testGeomPolygon, &geopb.BoundingBox{LoX: 0.017453292519943292, LoY: 0.01745329251994285, HiX: 0.03490658503988659, HiY: 0.03490791314678354}},
{geopb.SpatialObjectType_GeographyType, testGeomMultiPoint, &geopb.BoundingBox{LoX: 0.017453292519943292, LoY: 0.017453292519943295, HiX: 0.03490658503988659, HiY: 0.034906585039886584}},
{geopb.SpatialObjectType_GeographyType, testGeomMultiLineString, &geopb.BoundingBox{LoX: 0.017453292519943292, LoY: 0.01745329251994285, HiX: 0.06981317007977318, HiY: 0.06981317007977363}},
{geopb.SpatialObjectType_GeographyType, testGeomMultiPolygon, &geopb.BoundingBox{LoX: 0.017453292519943292, LoY: 0.01745329251994285, HiX: 0.06981317007977318, HiY: 0.06981581982279463}},
{geopb.SpatialObjectType_GeographyType, testGeomGeometryCollection, &geopb.BoundingBox{LoX: 0.017453292519943292, LoY: 0.017453292519943295, HiX: 0.03490658503988659, HiY: 0.03490658503988659}},
{geopb.SpatialObjectType_GeographyType, emptyGeomPoint, nil},
{geopb.SpatialObjectType_GeographyType, emptyGeomLineString, nil},
{geopb.SpatialObjectType_GeographyType, emptyGeomPolygon, nil},
{geopb.SpatialObjectType_GeographyType, emptyGeomMultiPoint, nil},
{geopb.SpatialObjectType_GeographyType, emptyGeomMultiLineString, nil},
{geopb.SpatialObjectType_GeographyType, emptyGeomMultiPolygon, nil},
{geopb.SpatialObjectType_GeographyType, emptyGeomGeometryCollection, nil},
{geopb.SpatialObjectType_GeographyType, emptyGeomPointInGeometryCollection, &geopb.BoundingBox{LoX: 0.017453292519943292, LoY: 0.01745329251994285, HiX: 0.03490658503988659, HiY: 0.03490658503988703}},
{geopb.SpatialObjectType_GeographyType, emptyGeomObjectsInGeometryCollection, nil},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%#v", tc.g), func(t *testing.T) {
bbox := boundingBoxFromGeom(tc.g)
t.Run(fmt.Sprintf("%s: %#v", tc.soType, tc.g), func(t *testing.T) {
bbox, err := boundingBoxFromGeomT(tc.g, tc.soType)
require.NoError(t, err)
require.Equal(t, tc.expected, bbox)
})
}
}
func TestCartesianBoundingBoxIntersects(t *testing.T) {
testCases := []struct {
desc string
a *CartesianBoundingBox
b *CartesianBoundingBox
expected bool
}{
{
desc: "same bounding box intersects",
a: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 0, HiY: 1}},
b: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 0, HiY: 1}},
expected: true,
},
{
desc: "overlapping bounding box intersects",
a: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 0, HiY: 1}},
b: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0.5, HiX: 1.5, LoY: 0.5, HiY: 1.5}},
expected: true,
},
{
desc: "overlapping bounding box intersects",
a: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 0, HiY: 1}},
b: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0.5, HiX: 1.5, LoY: 0.5, HiY: 1.5}},
expected: true,
},
{
desc: "touching bounding box intersects",
a: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 0, HiY: 1}},
b: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2}},
expected: true,
},
{
desc: "bounding box that is left does not intersect",
a: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 0, HiY: 1}},
b: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 1.5, HiX: 2, LoY: 0, HiY: 1}},
expected: false,
},
{
desc: "higher bounding box does not intersect",
a: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 0, HiY: 1}},
b: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 1.5, HiY: 2}},
expected: false,
},
{
desc: "completely disjoint bounding box does not intersect",
a: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: 0, HiX: 1, LoY: 0, HiY: 1}},
b: &CartesianBoundingBox{BoundingBox: geopb.BoundingBox{LoX: -3, HiX: -2, LoY: 1.5, HiY: 2}},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
require.Equal(t, tc.expected, tc.a.Intersects(tc.b))
})
}
}
......@@ -30,7 +30,7 @@ func TestEWKBToWKT(t *testing.T) {
for _, tc := range testCases {
t.Run(string(tc.ewkt), func(t *testing.T) {
so, err := parseEWKT(tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
so, err := parseEWKT(geopb.SpatialObjectType_GeometryType, tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
require.NoError(t, err)
encoded, err := EWKBToWKT(so.EWKB, tc.maxDecimalDigits)
require.NoError(t, err)
......@@ -52,7 +52,7 @@ func TestEWKBToEWKT(t *testing.T) {
for _, tc := range testCases {
t.Run(string(tc.ewkt), func(t *testing.T) {
so, err := parseEWKT(tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
so, err := parseEWKT(geopb.SpatialObjectType_GeometryType, tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
require.NoError(t, err)
encoded, err := EWKBToEWKT(so.EWKB, tc.maxDecimalDigits)
require.NoError(t, err)
......@@ -72,7 +72,7 @@ func TestEWKBToWKB(t *testing.T) {
for _, tc := range testCases {
t.Run(string(tc.ewkt), func(t *testing.T) {
so, err := parseEWKT(tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
so, err := parseEWKT(geopb.SpatialObjectType_GeometryType, tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
require.NoError(t, err)
encoded, err := EWKBToWKB(so.EWKB, DefaultEWKBEncodingFormat)
require.NoError(t, err)
......@@ -106,7 +106,7 @@ func TestEWKBToGeoJSON(t *testing.T) {
for _, tc := range testCases {
t.Run(string(tc.ewkt), func(t *testing.T) {
so, err := parseEWKT(tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
so, err := parseEWKT(geopb.SpatialObjectType_GeometryType, tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
require.NoError(t, err)
encoded, err := EWKBToGeoJSON(so.EWKB, 6, tc.flag)
require.NoError(t, err)
......@@ -126,7 +126,7 @@ func TestEWKBToWKBHex(t *testing.T) {
for _, tc := range testCases {
t.Run(string(tc.ewkt), func(t *testing.T) {
so, err := parseEWKT(tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
so, err := parseEWKT(geopb.SpatialObjectType_GeometryType, tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
require.NoError(t, err)
encoded, err := EWKBToWKBHex(so.EWKB)
require.NoError(t, err)
......@@ -148,7 +148,7 @@ func TestEWKBToKML(t *testing.T) {
for _, tc := range testCases {
t.Run(string(tc.ewkt), func(t *testing.T) {
so, err := parseEWKT(tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
so, err := parseEWKT(geopb.SpatialObjectType_GeometryType, tc.ewkt, geopb.DefaultGeometrySRID, DefaultSRIDIsHint)
require.NoError(t, err)
encoded, err := EWKBToKML(so.EWKB)
require.NoError(t, err)
......
This diff is collapsed.
......@@ -96,7 +96,7 @@ func TestGeospatialTypeFitsColumnMetadata(t *testing.T) {
testCases := []struct {
t GeospatialType
srid geopb.SRID
shapeType geopb.ShapeType
shape geopb.ShapeType
errorContains string
}{
{MustParseGeometry("POINT(1.0 1.0)"), 0, geopb.ShapeType_Geometry, ""},
......@@ -108,8 +108,8 @@ func TestGeospatialTypeFitsColumnMetadata(t *testing.T) {
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%#v_fits_%d_%s", tc.t, tc.srid, tc.shapeType), func(t *testing.T) {
err := GeospatialTypeFitsColumnMetadata(tc.t, tc.srid, tc.shapeType)
t.Run(fmt.Sprintf("%#v_fits_%d_%s", tc.t, tc.srid, tc.shape), func(t *testing.T) {
err := GeospatialTypeFitsColumnMetadata(tc.t, tc.srid, tc.shape)
if tc.errorContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
......@@ -120,86 +120,101 @@ func TestGeospatialTypeFitsColumnMetadata(t *testing.T) {
}
}
func TestSpatialObjectFromGeom(t *testing.T) {
func TestSpatialObjectFromGeomT(t *testing.T) {
testCases := []struct {
desc string
g geom.T
ret geopb.SpatialObject
desc string
soType geopb.SpatialObjectType
g geom.T
ret geopb.SpatialObject
}{
{
"point",
geopb.SpatialObjectType_GeometryType,
testGeomPoint,
geopb.SpatialObject{
Type: geopb.SpatialObjectType_GeometryType,
EWKB: mustDecodeEWKBFromString(t, "0101000000000000000000F03F0000000000000040"),
SRID: 0,
ShapeType: geopb.ShapeType_Point,
BoundingBox: &geopb.BoundingBox{MinX: 1, MaxX: 1, MinY: 2, MaxY: 2},
BoundingBox: &geopb.BoundingBox{LoX: 1, HiX: 1, LoY: 2, HiY: 2},
},
},
{
"linestring",
geopb.SpatialObjectType_GeometryType,
testGeomLineString,
geopb.SpatialObject{
Type: geopb.SpatialObjectType_GeometryType,
EWKB: mustDecodeEWKBFromString(t, "0102000020E610000002000000000000000000F03F000000000000F03F00000000000000400000000000000040"),
SRID: 4326,
ShapeType: geopb.ShapeType_LineString,
BoundingBox: &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2},
BoundingBox: &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2},
},
},
{
"polygon",
geopb.SpatialObjectType_GeometryType,
testGeomPolygon,
geopb.SpatialObject{
Type: geopb.SpatialObjectType_GeometryType,
EWKB: mustDecodeEWKBFromString(t, "01030000000100000004000000000000000000F03F000000000000F03F00000000000000400000000000000040000000000000F03F0000000000000040000000000000F03F000000000000F03F"),
SRID: 0,
ShapeType: geopb.ShapeType_Polygon,
BoundingBox: &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2},
BoundingBox: &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2},
},
},
{
"multipoint",
geopb.SpatialObjectType_GeometryType,
testGeomMultiPoint,
geopb.SpatialObject{
Type: geopb.SpatialObjectType_GeometryType,
EWKB: mustDecodeEWKBFromString(t, "0104000000020000000101000000000000000000F03F000000000000F03F010100000000000000000000400000000000000040"),
SRID: 0,
ShapeType: geopb.ShapeType_MultiPoint,
BoundingBox: &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2},
BoundingBox: &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2},
},
},
{
"multilinestring",
geopb.SpatialObjectType_GeometryType,
testGeomMultiLineString,
geopb.SpatialObject{
Type: geopb.SpatialObjectType_GeometryType,
EWKB: mustDecodeEWKBFromString(t, "010500000002000000010200000002000000000000000000F03F000000000000F03F000000000000004000000000000000400102000000020000000000000000000840000000000000084000000000000010400000000000001040"),
SRID: 0,
ShapeType: geopb.ShapeType_MultiLineString,
BoundingBox: &geopb.BoundingBox{MinX: 1, MaxX: 4, MinY: 1, MaxY: 4},
BoundingBox: &geopb.BoundingBox{LoX: 1, HiX: 4, LoY: 1, HiY: 4},
},
},
{
"multipolygon",
geopb.SpatialObjectType_GeometryType,
testGeomMultiPolygon,
geopb.SpatialObject{
Type: geopb.SpatialObjectType_GeometryType,
EWKB: mustDecodeEWKBFromString(t, "01060000000200000001030000000100000004000000000000000000F03F000000000000F03F00000000000000400000000000000040000000000000F03F0000000000000040000000000000F03F000000000000F03F0103000000010000000400000000000000000008400000000000000840000000000000104000000000000010400000000000000840000000000000104000000000000008400000000000000840"),
SRID: 0,
ShapeType: geopb.ShapeType_MultiPolygon,
BoundingBox: &geopb.BoundingBox{MinX: 1, MaxX: 4, MinY: 1, MaxY: 4},
BoundingBox: &geopb.BoundingBox{LoX: 1, HiX: 4, LoY: 1, HiY: 4},
},
},
{
"geometrycollection",
geopb.SpatialObjectType_GeometryType,
testGeomGeometryCollection,
geopb.SpatialObject{
Type: geopb.SpatialObjectType_GeometryType,
EWKB: mustDecodeEWKBFromString(t, "0107000000020000000101000000000000000000F03F00000000000000400104000000020000000101000000000000000000F03F000000000000F03F010100000000000000000000400000000000000040"),
SRID: 0,
ShapeType: geopb.ShapeType_GeometryCollection,
BoundingBox: &geopb.BoundingBox{MinX: 1, MaxX: 2, MinY: 1, MaxY: 2},
BoundingBox: &geopb.BoundingBox{LoX: 1, HiX: 2, LoY: 1, HiY: 2},
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
so, err := spatialObjectFromGeomT(tc.g)
so, err := spatialObjectFromGeomT(tc.g, tc.soType)
require.NoError(t, err)
require.Equal(t, tc.ret, so)
})
......@@ -471,3 +486,45 @@ func TestGeographyAsS2(t *testing.T) {
})
}
}
func TestGeometryAsGeography(t *testing.T) {
for _, tc := range []struct {
geom string
geog string
}{
{"POINT(1 0)", "SRID=4326;POINT(1 0)"},
{"SRID=4004;POINT(1 0)", "SRID=4004;POINT(1 0)"},
} {
t.Run(tc.geom, func(t *testing.T) {
geom, err := ParseGeometry(tc.geom)
require.NoError(t, err)
geog, err := ParseGeography(tc.geog)
require.NoError(t, err)
to, err := geom.AsGeography()
require.NoError(t, err)
require.Equal(t, geog, to)
})
}
}
func TestGeographyAsGeometry(t *testing.T) {
for _, tc := range []struct {
geom string
geog string
}{
{"SRID=4326;POINT(1 0)", "SRID=4326;POINT(1 0)"},
{"SRID=4004;POINT(1 0)", "SRID=4004;POINT(1 0)"},
} {
t.Run(tc.geom, func(t *testing.T) {
geom, err := ParseGeometry(tc.geom)
require.NoError(t, err)
geog, err := ParseGeography(tc.geog)
require.NoError(t, err)
to, err := geog.AsGeometry()
require.NoError(t, err)
require.Equal(t, geom, to)
})
}
}
......@@ -48,7 +48,8 @@ func Covers(a *geo.Geography, b *geo.Geography) (bool, error) {
// covers is the internal calculation for Covers.
func covers(a *geo.Geography, b *geo.Geography) (bool, error) {
if !a.BoundingBoxIntersects(b) {
// Rect "contains" is a version of covers.
if !a.BoundingRect().Contains(b.BoundingRect()) {
return false, nil
}
......
......@@ -378,6 +378,42 @@ func TestCovers(t *testing.T) {
"POINT(1.0 2.0)",
true,
},
{
"LINESTRING covers POINT across the longitudinal boundary",
"LINESTRING(179 0, -179 0)",
"POINT(179.1 0)",
true,
},
{
"reversed LINESTRING covers POINT across the longitudinal boundary",
"LINESTRING(-179 0, 179 0)",
"POINT(179.1 0)",
true,
},
{
"LINESTRING does not cover POINT with linestring crossing the longitudinal boundary but POINT on the other side",
"LINESTRING(179 0, -179 0)",
"POINT(170 0)",
false,
},
{
"reversed LINESTRING does not cover POINT with linestring crossing the longitudinal boundary but POINT on the other side",
"LINESTRING(-179 0, 179 0)",
"POINT(170 0)",
false,
},
{
"POLYGON covers POINT lying inside latitudinal boundary",
"POLYGON((150 85, 160 85, -20 85, -30 85, 150 85))",
"POINT (150 88)",
true,
},
{
"POLYGON does not cover POINT lying outside latitudinal boundary",
"POLYGON((150 85, 160 85, -20 85, -30 85, 150 85))",
"POINT (170 88)",
false,
},
}
for _, tc := range testCases {
......
......@@ -257,6 +257,48 @@ var distanceTestCases = []struct {
0,
0,
},
{
"LINESTRING to POINT intersecting across the longitudinal boundary",
"LINESTRING(179 0, -179 0)",
"POINT(179.1 0)",
0,
0,
},
{
"reversed LINESTRING to POINT intersecting across the longitudinal boundary",
"LINESTRING(-179 0, 179 0)",
"POINT(179.1 0)",
0,
0,
},
{
"reversed LINESTRING to POINT not intersecting the linestring crossing the longitudinal boundary but POINT on the other side",
"LINESTRING(-179 0, 179 0)",
"POINT(170 0)",
1000755.71761168,
1001875.41713946,
},
{
"LINESTRING to POINT not intersecting the linestring crossing the longitudinal boundary but POINT on the other side",
"LINESTRING(179 0, -179 0)",
"POINT(170 0)",
1000755.71761168,
1001875.41713946,
},
{
"POLYGON to POINT lying inside latitudinal boundary",
"POLYGON((150 85, 160 85, -20 85, -30 85, 150 85))",
"POINT (150 88)",
0,
0,
},
{
"POLYGON to POINT lying outside latitudinal boundary",
"POLYGON((150 85, 160 85, -20 85, -30 85, 150 85))",
"POINT (170 88)",
38610.04033289,
38783.11312354,
},
}
func TestDistance(t *testing.T) {
......
......@@ -21,7 +21,7 @@ import (
// This calculation is done on the sphere.
// Precision of intersect measurements is up to 1cm.
func Intersects(a *geo.Geography, b *geo.Geography) (bool, error) {
if !a.BoundingBoxIntersects(b) {
if !a.BoundingRect().Intersects(b.BoundingRect()) {
return false, nil
}
if a.SRID() != b.SRID() {
......
......@@ -162,6 +162,42 @@ func TestIntersects(t *testing.T) {
"LINESTRING(0.4 0.3, 0.6 0.3)",
true,
},
{
"LINESTRING intersects POINT across the longitudinal boundary",
"LINESTRING(179 0, -179 0)",
"POINT(179.1 0)",
true,
},
{
"reversed LINESTRING intersects POINT across the longitudinal boundary",
"LINESTRING(-179 0, 179 0)",
"POINT(179.1 0)",
true,
},
{
"LINESTRING does not intersect POINT with linestring crossing the longitudinal boundary but POINT on the other side",
"LINESTRING(179 0, -179 0)",
"POINT(170 0)",
false,
},
{
"reversed LINESTRING does not intersect POINT with linestring crossing the longitudinal boundary but POINT on the other side",
"LINESTRING(-179 0, 179 0)",
"POINT(170 0)",
false,
},
{
"POLYGON intersects POINT lying inside latitudinal boundary",
"POLYGON((150 85, 160 85, -20 85, -30 85, 150 85))",
"POINT (150 88)",
true,
},
{
"POLYGON does not intersect POINT lying outside latitudinal boundary",
"POLYGON((150 85, 160 85, -20 85, -30 85, 150 85))",
"POINT (170 88)",
false,
},
{
"POLYGON intersects itself",
"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
......
......@@ -20,7 +20,7 @@ func Covers(a *geo.Geometry, b *geo.Geometry) (bool, error) {
if a.SRID() != b.SRID() {
return false, geo.NewMismatchingSRIDsError(a, b)
}
if !a.BoundingBoxIntersects(b) {
if !a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox()) {
return false, nil
}
return geos.Covers(a.EWKB(), b.EWKB())
......@@ -31,7 +31,7 @@ func CoveredBy(a *geo.Geometry, b *geo.Geometry) (bool, error) {
if a.SRID() != b.SRID() {
return false, geo.NewMismatchingSRIDsError(a, b)
}
if !a.BoundingBoxIntersects(b) {
if !a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox()) {
return false, nil