github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/geo/geos/geos.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 geos is a wrapper around the spatial data types between the geo 12 // package and the GEOS C library. The GEOS library is dynamically loaded 13 // at init time. 14 // Operations will error if the GEOS library was not found. 15 package geos 16 17 import ( 18 "os" 19 "path/filepath" 20 "runtime" 21 "sync" 22 "unsafe" 23 24 "github.com/cockroachdb/cockroach/pkg/geo/geopb" 25 "github.com/cockroachdb/errors" 26 ) 27 28 // #cgo CXXFLAGS: -std=c++14 29 // #cgo !windows LDFLAGS: -ldl 30 // 31 // #include "geos.h" 32 import "C" 33 34 // EnsureInitErrorDisplay is used to control the error message displayed by 35 // EnsureInit. 36 type EnsureInitErrorDisplay int 37 38 const ( 39 // EnsureInitErrorDisplayPrivate displays the full error message, including 40 // path info. It is intended for log messages. 41 EnsureInitErrorDisplayPrivate EnsureInitErrorDisplay = iota 42 // EnsureInitErrorDisplayPublic displays a redacted error message, excluding 43 // path info. It is intended for errors to display for the client. 44 EnsureInitErrorDisplayPublic 45 ) 46 47 // maxArrayLen is the maximum safe length for this architecture. 48 const maxArrayLen = 1<<31 - 1 49 50 // geosOnce contains the global instance of CR_GEOS, to be initialized 51 // during at a maximum of once. 52 // If it has failed to open, the error will be populated in "err". 53 // This should only be touched by "fetchGEOSOrError". 54 var geosOnce struct { 55 geos *C.CR_GEOS 56 loc string 57 err error 58 once sync.Once 59 } 60 61 // EnsureInit attempts to start GEOS if it has not been opened already 62 // and returns the location if found, and an error if the CR_GEOS is not valid. 63 func EnsureInit(errDisplay EnsureInitErrorDisplay, flagGEOSLocationValue string) (string, error) { 64 _, err := ensureInit(errDisplay, flagGEOSLocationValue) 65 return geosOnce.loc, err 66 } 67 68 // ensureInitInternal ensures initialization has been done, always displaying 69 // errors privately and not assuming a flag has been set if initialized 70 // for the first time. 71 func ensureInitInternal() (*C.CR_GEOS, error) { 72 return ensureInit(EnsureInitErrorDisplayPrivate, "") 73 } 74 75 // ensureInits behaves as described in EnsureInit, but also returns the GEOS 76 // C object which should be hidden from the public eye. 77 func ensureInit( 78 errDisplay EnsureInitErrorDisplay, flagGEOSLocationValue string, 79 ) (*C.CR_GEOS, error) { 80 geosOnce.once.Do(func() { 81 geosOnce.geos, geosOnce.loc, geosOnce.err = initGEOS(findGEOSLocations(flagGEOSLocationValue)) 82 }) 83 if geosOnce.err != nil && errDisplay == EnsureInitErrorDisplayPublic { 84 return nil, errors.Newf("geos: this operation is not available") 85 } 86 return geosOnce.geos, geosOnce.err 87 } 88 89 // findGEOSLocations returns the default locations where GEOS is installed. 90 func findGEOSLocations(flagGEOSLocationValue string) []string { 91 var ext string 92 switch runtime.GOOS { 93 case "darwin": 94 ext = "dylib" 95 case "windows": 96 ext = "dll" 97 default: 98 ext = "so" 99 } 100 locs := []string{ 101 filepath.Join(flagGEOSLocationValue, "libgeos_c."+ext), 102 } 103 // For CI, they are always in a parenting directory where libgeos_c is set. 104 // For now, this will need to look at every given location 105 // TODO(otan): fix CI to always use a fixed location OR initialize GEOS 106 // correctly for each test suite that may need GEOS. 107 locs = append(locs, findGEOSLocationsInParentingDirectories(ext)...) 108 return locs 109 } 110 111 // findGEOSLocationsInParentingDirectories attempts to find GEOS by looking at 112 // parenting folders and looking inside `lib/lib_geos_c.*`. 113 func findGEOSLocationsInParentingDirectories(ext string) []string { 114 locs := []string{} 115 116 // Add the CI path by trying to find all parenting paths and appending 117 // `lib/libgeos_c.<ext>`. 118 cwd, err := os.Getwd() 119 if err != nil { 120 panic(err) 121 } 122 123 for { 124 nextPath := filepath.Join(cwd, "lib", "libgeos_c."+ext) 125 if _, err := os.Stat(nextPath); err == nil { 126 locs = append(locs, nextPath) 127 } 128 nextCWD := filepath.Dir(cwd) 129 if nextCWD == cwd { 130 break 131 } 132 cwd = nextCWD 133 } 134 return locs 135 } 136 137 // initGEOS initializes the CR_GEOS by attempting to dlopen all 138 // the paths as parsed in by locs. 139 func initGEOS(locs []string) (*C.CR_GEOS, string, error) { 140 var err error 141 for _, loc := range locs { 142 var ret *C.CR_GEOS 143 errStr := C.CR_GEOS_Init(goToCSlice([]byte(loc)), &ret) 144 if errStr.data == nil { 145 return ret, loc, nil 146 } 147 err = errors.CombineErrors( 148 err, 149 errors.Newf( 150 "geos: cannot load GEOS from %s: %s", 151 loc, 152 string(cSliceToUnsafeGoBytes(errStr)), 153 ), 154 ) 155 } 156 if err != nil { 157 return nil, "", errors.Wrap(err, "geos: error during GEOS init") 158 } 159 return nil, "", errors.Newf("geos: no locations to init GEOS") 160 } 161 162 // goToCSlice returns a CR_GEOS_Slice from a given Go byte slice. 163 func goToCSlice(b []byte) C.CR_GEOS_Slice { 164 if len(b) == 0 { 165 return C.CR_GEOS_Slice{data: nil, len: 0} 166 } 167 return C.CR_GEOS_Slice{ 168 data: (*C.char)(unsafe.Pointer(&b[0])), 169 len: C.size_t(len(b)), 170 } 171 } 172 173 // c{String,Slice}ToUnsafeGoBytes convert a CR_GEOS_{String,Slice} to a Go 174 // byte slice that refer to the underlying C memory. 175 func cStringToUnsafeGoBytes(s C.CR_GEOS_String) []byte { 176 return cToUnsafeGoBytes(s.data, s.len) 177 } 178 179 func cSliceToUnsafeGoBytes(s C.CR_GEOS_Slice) []byte { 180 return cToUnsafeGoBytes(s.data, s.len) 181 } 182 183 func cToUnsafeGoBytes(data *C.char, len C.size_t) []byte { 184 if data == nil { 185 return nil 186 } 187 // Interpret the C pointer as a pointer to a Go array, then slice. 188 return (*[maxArrayLen]byte)(unsafe.Pointer(data))[:len:len] 189 } 190 191 // cStringToSafeGoBytes converts a CR_GEOS_String to a Go byte slice. 192 // Additionally, it frees the C memory. 193 func cStringToSafeGoBytes(s C.CR_GEOS_String) []byte { 194 unsafeBytes := cStringToUnsafeGoBytes(s) 195 b := make([]byte, len(unsafeBytes)) 196 copy(b, unsafeBytes) 197 C.free(unsafe.Pointer(s.data)) 198 return b 199 } 200 201 // A Error wraps an error returned from a GEOS operation. 202 type Error struct { 203 msg string 204 } 205 206 // Error implements the error interface. 207 func (err *Error) Error() string { 208 return err.msg 209 } 210 211 func statusToError(s C.CR_GEOS_Status) error { 212 if s.data == nil { 213 return nil 214 } 215 return &Error{msg: string(cStringToSafeGoBytes(s))} 216 } 217 218 // WKTToEWKB parses a WKT into WKB using the GEOS library. 219 func WKTToEWKB(wkt geopb.WKT, srid geopb.SRID) (geopb.EWKB, error) { 220 g, err := ensureInitInternal() 221 if err != nil { 222 return nil, err 223 } 224 var cEWKB C.CR_GEOS_String 225 if err := statusToError(C.CR_GEOS_WKTToEWKB(g, goToCSlice([]byte(wkt)), C.int(srid), &cEWKB)); err != nil { 226 return nil, err 227 } 228 return cStringToSafeGoBytes(cEWKB), nil 229 } 230 231 // BufferParamsJoinStyle maps to the GEOSBufJoinStyles enum in geos_c.h.in. 232 type BufferParamsJoinStyle int 233 234 // These should be kept in sync with the geos_c.h.in corresponding enum definition. 235 const ( 236 BufferParamsJoinStyleRound = 1 237 BufferParamsJoinStyleMitre = 2 238 BufferParamsJoinStyleBevel = 3 239 ) 240 241 // BufferParamsEndCapStyle maps to the GEOSBufCapStyles enum in geos_c.h.in. 242 type BufferParamsEndCapStyle int 243 244 // These should be kept in sync with the geos_c.h.in corresponding enum definition. 245 const ( 246 BufferParamsEndCapStyleRound = 1 247 BufferParamsEndCapStyleFlat = 2 248 BufferParamsEndCapStyleSquare = 3 249 ) 250 251 // BufferParams are parameters to provide into the GEOS buffer function. 252 type BufferParams struct { 253 JoinStyle BufferParamsJoinStyle 254 EndCapStyle BufferParamsEndCapStyle 255 SingleSided bool 256 QuadrantSegments int 257 MitreLimit float64 258 } 259 260 // Buffer buffers the given geometry by the given distance and params. 261 func Buffer(ewkb geopb.EWKB, params BufferParams, distance float64) (geopb.EWKB, error) { 262 g, err := ensureInitInternal() 263 if err != nil { 264 return nil, err 265 } 266 singleSided := 0 267 if params.SingleSided { 268 singleSided = 1 269 } 270 cParams := C.CR_GEOS_BufferParamsInput{ 271 endCapStyle: C.int(params.EndCapStyle), 272 joinStyle: C.int(params.JoinStyle), 273 singleSided: C.int(singleSided), 274 quadrantSegments: C.int(params.QuadrantSegments), 275 mitreLimit: C.double(params.MitreLimit), 276 } 277 var cEWKB C.CR_GEOS_String 278 if err := statusToError(C.CR_GEOS_Buffer(g, goToCSlice(ewkb), cParams, C.double(distance), &cEWKB)); err != nil { 279 return nil, err 280 } 281 return cStringToSafeGoBytes(cEWKB), nil 282 } 283 284 // Area returns the area of an EWKB. 285 func Area(ewkb geopb.EWKB) (float64, error) { 286 g, err := ensureInitInternal() 287 if err != nil { 288 return 0, err 289 } 290 var area C.double 291 if err := statusToError(C.CR_GEOS_Area(g, goToCSlice(ewkb), &area)); err != nil { 292 return 0, err 293 } 294 return float64(area), nil 295 } 296 297 // Length returns the length of an EWKB. 298 func Length(ewkb geopb.EWKB) (float64, error) { 299 g, err := ensureInitInternal() 300 if err != nil { 301 return 0, err 302 } 303 var length C.double 304 if err := statusToError(C.CR_GEOS_Length(g, goToCSlice(ewkb), &length)); err != nil { 305 return 0, err 306 } 307 return float64(length), nil 308 } 309 310 // Centroid returns the centroid of an EWKB. 311 func Centroid(ewkb geopb.EWKB) (geopb.EWKB, error) { 312 g, err := ensureInitInternal() 313 if err != nil { 314 return nil, err 315 } 316 var cEWKB C.CR_GEOS_String 317 if err := statusToError(C.CR_GEOS_Centroid(g, goToCSlice(ewkb), &cEWKB)); err != nil { 318 return nil, err 319 } 320 return cStringToSafeGoBytes(cEWKB), nil 321 } 322 323 // InterpolateLine returns the point along the given LineString which is at 324 // a given distance from starting point. 325 // Note: For distance less than 0 it returns start point similarly for distance 326 // greater LineString's length. 327 // InterpolateLine also works with (Multi)LineString. However, the result is 328 // not appropriate as it combines all the LineString present in (MULTI)LineString, 329 // considering all the corner points of LineString overlaps each other. 330 func InterpolateLine(ewkb geopb.EWKB, distance float64) (geopb.EWKB, error) { 331 g, err := ensureInitInternal() 332 if err != nil { 333 return nil, err 334 } 335 var cEWKB C.CR_GEOS_String 336 if err := statusToError(C.CR_GEOS_Interpolate(g, goToCSlice(ewkb), C.double(distance), &cEWKB)); err != nil { 337 return nil, err 338 } 339 return cStringToSafeGoBytes(cEWKB), nil 340 } 341 342 // MinDistance returns the minimum distance between two EWKBs. 343 func MinDistance(a geopb.EWKB, b geopb.EWKB) (float64, error) { 344 g, err := ensureInitInternal() 345 if err != nil { 346 return 0, err 347 } 348 var distance C.double 349 if err := statusToError(C.CR_GEOS_Distance(g, goToCSlice(a), goToCSlice(b), &distance)); err != nil { 350 return 0, err 351 } 352 return float64(distance), nil 353 } 354 355 // ClipEWKBByRect clips a EWKB to the specified rectangle. 356 func ClipEWKBByRect( 357 ewkb geopb.EWKB, xMin float64, yMin float64, xMax float64, yMax float64, 358 ) (geopb.EWKB, error) { 359 g, err := ensureInitInternal() 360 if err != nil { 361 return nil, err 362 } 363 var cEWKB C.CR_GEOS_String 364 if err := statusToError(C.CR_GEOS_ClipEWKBByRect(g, goToCSlice(ewkb), C.double(xMin), 365 C.double(yMin), C.double(xMax), C.double(yMax), &cEWKB)); err != nil { 366 return nil, err 367 } 368 return cStringToSafeGoBytes(cEWKB), nil 369 } 370 371 // Covers returns whether the EWKB provided by A covers the EWKB provided by B. 372 func Covers(a geopb.EWKB, b geopb.EWKB) (bool, error) { 373 g, err := ensureInitInternal() 374 if err != nil { 375 return false, err 376 } 377 var ret C.char 378 if err := statusToError(C.CR_GEOS_Covers(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 379 return false, err 380 } 381 return ret == 1, nil 382 } 383 384 // CoveredBy returns whether the EWKB provided by A is covered by the EWKB provided by B. 385 func CoveredBy(a geopb.EWKB, b geopb.EWKB) (bool, error) { 386 g, err := ensureInitInternal() 387 if err != nil { 388 return false, err 389 } 390 var ret C.char 391 if err := statusToError(C.CR_GEOS_CoveredBy(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 392 return false, err 393 } 394 return ret == 1, nil 395 } 396 397 // Contains returns whether the EWKB provided by A contains the EWKB provided by B. 398 func Contains(a geopb.EWKB, b geopb.EWKB) (bool, error) { 399 g, err := ensureInitInternal() 400 if err != nil { 401 return false, err 402 } 403 var ret C.char 404 if err := statusToError(C.CR_GEOS_Contains(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 405 return false, err 406 } 407 return ret == 1, nil 408 } 409 410 // Crosses returns whether the EWKB provided by A crosses the EWKB provided by B. 411 func Crosses(a geopb.EWKB, b geopb.EWKB) (bool, error) { 412 g, err := ensureInitInternal() 413 if err != nil { 414 return false, err 415 } 416 var ret C.char 417 if err := statusToError(C.CR_GEOS_Crosses(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 418 return false, err 419 } 420 return ret == 1, nil 421 } 422 423 // Equals returns whether the EWKB provided by A equals the EWKB provided by B. 424 func Equals(a geopb.EWKB, b geopb.EWKB) (bool, error) { 425 g, err := ensureInitInternal() 426 if err != nil { 427 return false, err 428 } 429 var ret C.char 430 if err := statusToError(C.CR_GEOS_Equals(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 431 return false, err 432 } 433 return ret == 1, nil 434 } 435 436 // Intersects returns whether the EWKB provided by A intersects the EWKB provided by B. 437 func Intersects(a geopb.EWKB, b geopb.EWKB) (bool, error) { 438 g, err := ensureInitInternal() 439 if err != nil { 440 return false, err 441 } 442 var ret C.char 443 if err := statusToError(C.CR_GEOS_Intersects(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 444 return false, err 445 } 446 return ret == 1, nil 447 } 448 449 // Overlaps returns whether the EWKB provided by A overlaps the EWKB provided by B. 450 func Overlaps(a geopb.EWKB, b geopb.EWKB) (bool, error) { 451 g, err := ensureInitInternal() 452 if err != nil { 453 return false, err 454 } 455 var ret C.char 456 if err := statusToError(C.CR_GEOS_Overlaps(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 457 return false, err 458 } 459 return ret == 1, nil 460 } 461 462 // Touches returns whether the EWKB provided by A touches the EWKB provided by B. 463 func Touches(a geopb.EWKB, b geopb.EWKB) (bool, error) { 464 g, err := ensureInitInternal() 465 if err != nil { 466 return false, err 467 } 468 var ret C.char 469 if err := statusToError(C.CR_GEOS_Touches(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 470 return false, err 471 } 472 return ret == 1, nil 473 } 474 475 // Within returns whether the EWKB provided by A is within the EWKB provided by B. 476 func Within(a geopb.EWKB, b geopb.EWKB) (bool, error) { 477 g, err := ensureInitInternal() 478 if err != nil { 479 return false, err 480 } 481 var ret C.char 482 if err := statusToError(C.CR_GEOS_Within(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 483 return false, err 484 } 485 return ret == 1, nil 486 } 487 488 // 489 // DE-9IM related 490 // 491 492 // Relate returns the DE-9IM relation between A and B. 493 func Relate(a geopb.EWKB, b geopb.EWKB) (string, error) { 494 g, err := ensureInitInternal() 495 if err != nil { 496 return "", err 497 } 498 var ret C.CR_GEOS_String 499 if err := statusToError(C.CR_GEOS_Relate(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { 500 return "", err 501 } 502 if ret.data == nil { 503 return "", errors.Newf("expected DE-9IM string but found nothing") 504 } 505 return string(cStringToSafeGoBytes(ret)), nil 506 } 507 508 // RelatePattern whether A and B have a DE-9IM relation matching the given pattern. 509 func RelatePattern(a geopb.EWKB, b geopb.EWKB, pattern string) (bool, error) { 510 g, err := ensureInitInternal() 511 if err != nil { 512 return false, err 513 } 514 var ret C.char 515 if err := statusToError( 516 C.CR_GEOS_RelatePattern(g, goToCSlice(a), goToCSlice(b), goToCSlice([]byte(pattern)), &ret), 517 ); err != nil { 518 return false, err 519 } 520 return ret == 1, nil 521 }