github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/geo/geomfn/distance_test.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 geomfn
    12  
    13  import (
    14  	"fmt"
    15  	"math"
    16  	"testing"
    17  
    18  	"github.com/cockroachdb/cockroach/pkg/geo"
    19  	"github.com/cockroachdb/cockroach/pkg/geo/geos"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  var distanceTestCases = []struct {
    24  	desc                string
    25  	a                   string
    26  	b                   string
    27  	expectedMinDistance float64
    28  	expectedMaxDistance float64
    29  }{
    30  	{
    31  		"Same POINTs",
    32  		"POINT(1.0 1.0)",
    33  		"POINT(1.0 1.0)",
    34  		0,
    35  		0,
    36  	},
    37  	{
    38  		"Different POINTs",
    39  		"POINT(1.0 1.0)",
    40  		"POINT(2.0 1.0)",
    41  		1,
    42  		1,
    43  	},
    44  	{
    45  		"POINT on LINESTRING",
    46  		"POINT(0.5 0.5)",
    47  		"LINESTRING(0.0 0.0, 1.0 1.0, 2.0 2.0)",
    48  		0,
    49  		2.1213203435596424,
    50  	},
    51  	{
    52  		"POINT away from LINESTRING",
    53  		"POINT(3.0 3.0)",
    54  		"LINESTRING(0.0 0.0, 1.0 1.0, 2.0 2.0)",
    55  		1.4142135623730951,
    56  		4.242640687119285,
    57  	},
    58  	{
    59  		"Same LINESTRING",
    60  		"LINESTRING(0.0 0.0, 1.0 1.0, 2.0 2.0)",
    61  		"LINESTRING(0.0 0.0, 1.0 1.0, 2.0 2.0)",
    62  		0,
    63  		2.8284271247461903,
    64  	},
    65  	{
    66  		"Intersecting LINESTRING",
    67  		"LINESTRING(0.0 0.0, 1.0 1.0, 2.0 2.0)",
    68  		"LINESTRING(0.5 0.0, 0.5 3.0)",
    69  		0,
    70  		3.0413812651491097,
    71  	},
    72  	{
    73  		"LINESTRING does not meet",
    74  		"LINESTRING(6.0 6.0, 7.0 7.0, 8.0 8.0)",
    75  		"LINESTRING(0.0 0.0, 3.0 -3.0)",
    76  		8.48528137423857,
    77  		12.083045973594572,
    78  	},
    79  	{
    80  		"POINT in POLYGON",
    81  		"POINT(0.5 0.5)",
    82  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
    83  		0,
    84  		0.7071067811865476,
    85  	},
    86  	{
    87  		"POINT in POLYGON hole",
    88  		"POINT(0.5 0.5)",
    89  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.2 0.2, 0.6 0.2, 0.6 0.6, 0.2 0.6, 0.2 0.2))",
    90  		0.09999999999999998,
    91  		0.7071067811865476,
    92  	},
    93  	{
    94  		"POINT not in POLYGON hole",
    95  		"POINT(0.1 0.1)",
    96  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.2 0.2, 0.6 0.2, 0.6 0.6, 0.2 0.6, 0.2 0.2))",
    97  		0,
    98  		1.2727922061357855,
    99  	},
   100  	{
   101  		"POINT outside of POLYGON",
   102  		"POINT(1.5 1.5)",
   103  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
   104  		0.7071067811865476,
   105  		2.1213203435596424,
   106  	},
   107  	{
   108  		"LINESTRING intersects POLYGON",
   109  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
   110  		"LINESTRING(-0.5 -0.5, 0.5 0.5)",
   111  		0,
   112  		2.1213203435596424,
   113  	},
   114  	{
   115  		"LINESTRING intersects POLYGON, duplicate points",
   116  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 1.0 1.0, 0.0 1.0, 0.0 0.0, 0.0 0.0))",
   117  		"LINESTRING(-0.5 -0.5, 0.5 0.5, 0.5 0.5)",
   118  		0,
   119  		2.1213203435596424,
   120  	},
   121  	{
   122  		"LINESTRING outside of POLYGON",
   123  		"LINESTRING(-0.5 -0.5, -0.5 0.5)",
   124  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
   125  		0.5,
   126  		2.1213203435596424,
   127  	},
   128  	{
   129  		"LINESTRING outside of POLYGON, duplicate points",
   130  		"LINESTRING(-0.5 -0.5, -0.5 -0.5, -0.5 0.5, -0.5 0.5)",
   131  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
   132  		0.5,
   133  		2.1213203435596424,
   134  	},
   135  	{
   136  		"POLYGON is the same",
   137  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
   138  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
   139  		0,
   140  		1.4142135623730951,
   141  	},
   142  	{
   143  		"POLYGON inside POLYGON",
   144  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
   145  		"POLYGON((0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))",
   146  		0,
   147  		1.2727922061357855,
   148  	},
   149  	{
   150  		"POLYGON to POLYGON intersecting through its hole",
   151  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.2 0.2, 0.2 0.4, 0.4 0.4, 0.4 0.2, 0.2 0.2))",
   152  		"POLYGON((0.15 0.25, 0.35 0.25, 0.35 0.35, 0.25 0.35, 0.15 0.25))",
   153  		0,
   154  		1.1335784048754634,
   155  	},
   156  	{
   157  		"POLYGON to POLYGON intersecting through its hole, duplicate points",
   158  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.2 0.2, 0.2 0.4, 0.4 0.4, 0.4 0.4, 0.4 0.2, 0.2 0.2))",
   159  		"POLYGON((0.15 0.25, 0.15 0.25, 0.35 0.25, 0.35 0.35, 0.35 0.35,0.25 0.35, 0.15 0.25))",
   160  		0,
   161  		1.1335784048754634,
   162  	},
   163  	{
   164  		"POLYGON inside POLYGON hole",
   165  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))",
   166  		"POLYGON((0.2 0.2, 0.8 0.2, 0.8 0.8, 0.2 0.8, 0.2 0.2))",
   167  		0.09999999999999998,
   168  		1.1313708498984762,
   169  	},
   170  	{
   171  		"POLYGON outside POLYGON",
   172  		"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))",
   173  		"POLYGON((3.0 3.0, 4.0 3.0, 4.0 4.0, 3.0 4.0, 3.0 3.0))",
   174  		2.8284271247461903,
   175  		5.656854249492381,
   176  	},
   177  	{
   178  		"MULTIPOINT to MULTIPOINT",
   179  		"MULTIPOINT((1.0 1.0), (2.0 2.0))",
   180  		"MULTIPOINT((2.5 2.5), (3.0 3.0))",
   181  		0.7071067811865476,
   182  		2.8284271247461903,
   183  	},
   184  	{
   185  		"MULTIPOINT to MULTILINESTRING",
   186  		"MULTILINESTRING((1.0 1.0, 1.1 1.1), (2.0 2.0, 2.1 2.1))",
   187  		"MULTIPOINT(2.0 2.0, 1.0 1.0, 3.0 3.0)",
   188  		0,
   189  		2.8284271247461903,
   190  	},
   191  	{
   192  		"MULTIPOINT to MULTIPOLYGON",
   193  		"MULTIPOINT ((2.0 3.0), (10 42))",
   194  		"MULTIPOLYGON (((15 5, 40 10, 10 20, 5 10, 15 5)),((30 20, 45 40, 10 40, 30 20)))",
   195  		2,
   196  		56.72741841473134,
   197  	},
   198  	{
   199  		"MULTILINESTRING to MULTILINESTRING",
   200  		"MULTILINESTRING((1.0 1.0, 1.1 1.1), (2.0 2.0, 2.1 2.1), (3.0 3.0, 3.1 3.1))",
   201  		"MULTILINESTRING((2.0 2.0, 2.1 2.1), (4.0 3.0, 3.1 3.1))",
   202  		0,
   203  		3.605551275463989,
   204  	},
   205  	{
   206  		"MULTILINESTRING to MULTIPOLYGON",
   207  		"MULTIPOLYGON (((15 5, 40 10, 10 20, 5 10, 15 5)),((30 20, 45 40, 10 40, 30 20)))",
   208  		"MULTILINESTRING((3 3, -4 -4), (45 41, 48 48, 52 52))",
   209  		1,
   210  		65.85590330410783,
   211  	},
   212  	{
   213  		"MULTIPOLYGON to MULTIPOLYGON",
   214  		"MULTIPOLYGON (((15 5, 40 10, 10 20, 5 10, 15 5)),((30 20, 45 40, 10 40, 30 20)))",
   215  		"MULTIPOLYGON (((30 20, 45 40, 15 45, 30 20)))",
   216  		0,
   217  		50,
   218  	},
   219  	{
   220  		"GEOMETRYCOLLECTION (POINT, EMPTY) with POINT",
   221  		"GEOMETRYCOLLECTION ( POINT(1.0 2.0), LINESTRING EMPTY )",
   222  		"POINT(1.0 2.0)",
   223  		0,
   224  		0,
   225  	},
   226  	{
   227  		"GEOMETRYCOLLECTION (POINT, EMPTY) with DIFFERENT POINT",
   228  		"GEOMETRYCOLLECTION ( POINT(1.0 2.0), LINESTRING EMPTY )",
   229  		"POINT(1.0 3.0)",
   230  		1,
   231  		1,
   232  	},
   233  }
   234  
   235  // TODO(otan): delete after https://github.com/cockroachdb/cockroach/issues/49209
   236  var knownGEOSPanics = map[string]struct{}{
   237  	"GEOMETRYCOLLECTION (POINT, EMPTY) with POINT":           {},
   238  	"GEOMETRYCOLLECTION (POINT, EMPTY) with DIFFERENT POINT": {},
   239  }
   240  
   241  var falseDWithinTestCases = map[string]struct{}{
   242  	"GEOMETRYCOLLECTION (POINT, EMPTY) with POINT":           {},
   243  	"GEOMETRYCOLLECTION (POINT, EMPTY) with DIFFERENT POINT": {},
   244  }
   245  
   246  var emptyDistanceTestCases = []struct {
   247  	a string
   248  	b string
   249  }{
   250  	{"GEOMETRYCOLLECTION EMPTY", "GEOMETRYCOLLECTION EMPTY"},
   251  	{"GEOMETRYCOLLECTION EMPTY", "GEOMETRYCOLLECTION (LINESTRING EMPTY)"},
   252  	{"GEOMETRYCOLLECTION EMPTY", "POINT(1.0 1.0)"},
   253  	{"POINT(1.0 1.0)", "GEOMETRYCOLLECTION EMPTY"},
   254  }
   255  
   256  func TestMinDistance(t *testing.T) {
   257  	for _, tc := range distanceTestCases {
   258  		t.Run(tc.desc, func(t *testing.T) {
   259  			a, err := geo.ParseGeometry(tc.a)
   260  			require.NoError(t, err)
   261  			b, err := geo.ParseGeometry(tc.b)
   262  			require.NoError(t, err)
   263  
   264  			// Try in both directions.
   265  			ret, err := MinDistance(a, b)
   266  			require.NoError(t, err)
   267  			require.Equal(t, tc.expectedMinDistance, ret)
   268  
   269  			ret, err = MinDistance(b, a)
   270  			require.NoError(t, err)
   271  			require.Equal(t, tc.expectedMinDistance, ret)
   272  
   273  			// Check distance roughly the same as GEOS.
   274  			if _, panicsInGEOS := knownGEOSPanics[tc.desc]; !panicsInGEOS {
   275  				ret, err = geos.MinDistance(a.EWKB(), b.EWKB())
   276  				require.NoError(t, err)
   277  				require.LessOrEqualf(
   278  					t,
   279  					math.Abs(tc.expectedMinDistance-ret),
   280  					0.0000001, // GEOS and PostGIS/CRDB can return results close by.
   281  					"expected distance within %f, GEOS returns %f",
   282  					tc.expectedMinDistance,
   283  					ret,
   284  				)
   285  			}
   286  		})
   287  	}
   288  
   289  	t.Run("errors for EMPTY geometries", func(t *testing.T) {
   290  		for _, tc := range emptyDistanceTestCases {
   291  			t.Run(fmt.Sprintf("%s to %s", tc.a, tc.b), func(t *testing.T) {
   292  				a, err := geo.ParseGeometry(tc.a)
   293  				require.NoError(t, err)
   294  				b, err := geo.ParseGeometry(tc.b)
   295  				require.NoError(t, err)
   296  				_, err = MinDistance(a, b)
   297  				require.Error(t, err)
   298  				require.True(t, geo.IsEmptyGeometryError(err))
   299  			})
   300  		}
   301  	})
   302  
   303  	t.Run("errors if SRIDs mismatch", func(t *testing.T) {
   304  		_, err := MinDistance(mismatchingSRIDGeometryA, mismatchingSRIDGeometryB)
   305  		requireMismatchingSRIDError(t, err)
   306  	})
   307  }
   308  
   309  func TestMaxDistance(t *testing.T) {
   310  	for _, tc := range distanceTestCases {
   311  		t.Run(tc.desc, func(t *testing.T) {
   312  			a, err := geo.ParseGeometry(tc.a)
   313  			require.NoError(t, err)
   314  			b, err := geo.ParseGeometry(tc.b)
   315  			require.NoError(t, err)
   316  
   317  			// Try in both directions.
   318  			ret, err := MaxDistance(a, b)
   319  			require.NoError(t, err)
   320  			require.Equal(t, tc.expectedMaxDistance, ret)
   321  
   322  			ret, err = MaxDistance(b, a)
   323  			require.NoError(t, err)
   324  			require.Equal(t, tc.expectedMaxDistance, ret)
   325  		})
   326  	}
   327  
   328  	t.Run("errors if SRIDs mismatch", func(t *testing.T) {
   329  		_, err := MinDistance(mismatchingSRIDGeometryA, mismatchingSRIDGeometryB)
   330  		requireMismatchingSRIDError(t, err)
   331  	})
   332  }
   333  
   334  func TestDWithin(t *testing.T) {
   335  	for _, tc := range distanceTestCases {
   336  		t.Run(tc.desc, func(t *testing.T) {
   337  			a, err := geo.ParseGeometry(tc.a)
   338  			require.NoError(t, err)
   339  			b, err := geo.ParseGeometry(tc.b)
   340  			require.NoError(t, err)
   341  
   342  			// empty geometries should always return false.
   343  			expected := true
   344  			if _, ok := falseDWithinTestCases[tc.desc]; ok {
   345  				expected = false
   346  			}
   347  
   348  			for _, val := range []float64{
   349  				tc.expectedMinDistance,
   350  				tc.expectedMinDistance + 0.1,
   351  				tc.expectedMinDistance + 1,
   352  				tc.expectedMinDistance * 2,
   353  			} {
   354  				t.Run(fmt.Sprintf("dwithin:%f", val), func(t *testing.T) {
   355  					dwithin, err := DWithin(a, b, val)
   356  					require.NoError(t, err)
   357  					require.Equal(t, expected, dwithin)
   358  
   359  					dwithin, err = DWithin(a, b, val)
   360  					require.NoError(t, err)
   361  					require.Equal(t, expected, dwithin)
   362  				})
   363  			}
   364  
   365  			for _, val := range []float64{
   366  				tc.expectedMinDistance - 0.1,
   367  				tc.expectedMinDistance - 1,
   368  				tc.expectedMinDistance / 2,
   369  			} {
   370  				if val > 0 {
   371  					t.Run(fmt.Sprintf("dwithin:%f", val), func(t *testing.T) {
   372  						dwithin, err := DWithin(a, b, val)
   373  						require.NoError(t, err)
   374  						require.False(t, dwithin)
   375  
   376  						dwithin, err = DWithin(a, b, val)
   377  						require.NoError(t, err)
   378  						require.False(t, dwithin)
   379  					})
   380  				}
   381  			}
   382  		})
   383  	}
   384  
   385  	t.Run("returns false for EMPTY geometries", func(t *testing.T) {
   386  		for _, tc := range emptyDistanceTestCases {
   387  			t.Run(fmt.Sprintf("%s to %s", tc.a, tc.b), func(t *testing.T) {
   388  				a, err := geo.ParseGeometry(tc.a)
   389  				require.NoError(t, err)
   390  				b, err := geo.ParseGeometry(tc.b)
   391  				require.NoError(t, err)
   392  				dwithin, err := DWithin(a, b, 0)
   393  				require.NoError(t, err)
   394  				require.False(t, dwithin)
   395  			})
   396  		}
   397  	})
   398  
   399  	t.Run("errors if SRIDs mismatch", func(t *testing.T) {
   400  		_, err := MinDistance(mismatchingSRIDGeometryA, mismatchingSRIDGeometryB)
   401  		requireMismatchingSRIDError(t, err)
   402  	})
   403  
   404  	t.Run("errors if distance < 0", func(t *testing.T) {
   405  		_, err := DWithin(geo.MustParseGeometry("POINT(1.0 2.0)"), geo.MustParseGeometry("POINT(3.0 4.0)"), -0.01)
   406  		require.Error(t, err)
   407  	})
   408  }
   409  
   410  func TestDFullyWithin(t *testing.T) {
   411  	for _, tc := range distanceTestCases {
   412  		t.Run(tc.desc, func(t *testing.T) {
   413  			a, err := geo.ParseGeometry(tc.a)
   414  			require.NoError(t, err)
   415  			b, err := geo.ParseGeometry(tc.b)
   416  			require.NoError(t, err)
   417  
   418  			// empty geometries should always return false.
   419  			expected := true
   420  			if _, ok := falseDWithinTestCases[tc.desc]; ok {
   421  				expected = false
   422  			}
   423  
   424  			for _, val := range []float64{
   425  				tc.expectedMaxDistance,
   426  				tc.expectedMaxDistance + 0.1,
   427  				tc.expectedMaxDistance + 1,
   428  				tc.expectedMaxDistance * 2,
   429  			} {
   430  				t.Run(fmt.Sprintf("dfullywithin:%f", val), func(t *testing.T) {
   431  					dfullywithin, err := DFullyWithin(a, b, val)
   432  					require.NoError(t, err)
   433  					require.Equal(t, expected, dfullywithin)
   434  
   435  					dfullywithin, err = DFullyWithin(a, b, val)
   436  					require.NoError(t, err)
   437  					require.Equal(t, expected, dfullywithin)
   438  				})
   439  			}
   440  
   441  			for _, val := range []float64{
   442  				tc.expectedMaxDistance - 0.1,
   443  				tc.expectedMaxDistance - 1,
   444  				tc.expectedMaxDistance / 2,
   445  			} {
   446  				if val > 0 {
   447  					t.Run(fmt.Sprintf("dfullywithin:%f", val), func(t *testing.T) {
   448  						dfullywithin, err := DFullyWithin(a, b, val)
   449  						require.NoError(t, err)
   450  						require.False(t, dfullywithin)
   451  
   452  						dfullywithin, err = DFullyWithin(a, b, val)
   453  						require.NoError(t, err)
   454  						require.False(t, dfullywithin)
   455  					})
   456  				}
   457  			}
   458  		})
   459  	}
   460  
   461  	t.Run("errors if SRIDs mismatch", func(t *testing.T) {
   462  		_, err := MinDistance(mismatchingSRIDGeometryA, mismatchingSRIDGeometryB)
   463  		requireMismatchingSRIDError(t, err)
   464  	})
   465  
   466  	t.Run("errors if distance < 0", func(t *testing.T) {
   467  		_, err := DWithin(geo.MustParseGeometry("POINT(1.0 2.0)"), geo.MustParseGeometry("POINT(3.0 4.0)"), -0.01)
   468  		require.Error(t, err)
   469  	})
   470  }