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 }